From 672760bb2533ad189659b09812cefc1a6a29bef1 Mon Sep 17 00:00:00 2001 From: yovinchen Date: Mon, 30 Jun 2025 10:36:41 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E5=9F=BA=E6=9C=AC=E5=AE=8C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/TableSynthesis.iml | 8 + .idea/inspectionProfiles/Project_Default.xml | 6 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 6 + .idea/modules.xml | 8 + README.md | 146 +++ build_workflow.yml | 89 ++ cross_platform_build.py | 326 +++++++ seat_allocation_system.py | 898 ++++++++++++++++++ simple_build.py | 232 +++++ 人员信息.xlsx | Bin 0 -> 16191 bytes 座位信息.xlsx | Bin 0 -> 15328 bytes 12 files changed, 1725 insertions(+) create mode 100644 .idea/TableSynthesis.iml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 README.md create mode 100644 build_workflow.yml create mode 100644 cross_platform_build.py create mode 100644 seat_allocation_system.py create mode 100644 simple_build.py create mode 100644 人员信息.xlsx create mode 100644 座位信息.xlsx diff --git a/.idea/TableSynthesis.iml b/.idea/TableSynthesis.iml new file mode 100644 index 0000000..f0c4c3e --- /dev/null +++ b/.idea/TableSynthesis.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..ac21435 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..35f11bb --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..aace106 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f5a2588 --- /dev/null +++ b/README.md @@ -0,0 +1,146 @@ +# 座位分配系统 + +智能座位分配工具,支持1-10人连坐需求,能够处理不连续座位的复杂情况。 + +## 🎯 功能特点 + +- ✅ **智能分配**: 支持1-10人的各种连坐需求 +- ✅ **数据校验**: 完整的文件格式和逻辑校验 +- ✅ **不连续座位**: 自动处理座位号间隙 +- ✅ **详细日志**: 完整的操作记录和结果验证 +- ✅ **跨平台**: 支持Windows、macOS、Linux + +## 📋 使用方法 + +### 准备Excel文件 + +1. **人员信息.xlsx** - 包含以下列: + - 姓名、证件类型、证件号、手机号、备注 + - 备注数字表示连坐人数(如:备注4表示当前行+后3行共4人连坐) + +2. **座位信息.xlsx** - 包含以下列: + - 区域、楼层、排号、座位号 + +### 运行程序 + +#### Python环境运行 +```bash +# 安装依赖 +pip install pandas openpyxl + +# 运行程序 +python seat_allocation_system.py +``` + +#### 构建可执行文件 +```bash +# 在当前平台构建 +python simple_build.py + +# 跨平台构建(macOS构建Windows版本) +python cross_platform_build.py +``` + +### 查看结果 + +程序运行后会生成: +- `座位信息_最终分配.xlsx` - 最终分配结果 +- `最终座位分配日志.xlsx` - 详细分配记录 +- `seat_allocation_log.txt` - 完整运行日志 + +## 🔧 构建选项 + +### 本地构建 +```bash +# 简单构建(推荐) +python simple_build.py +``` + +### 跨平台构建 +```bash +# 在macOS上构建Windows版本 +python cross_platform_build.py +``` + +### 自动化构建 +使用GitHub Actions自动构建多平台版本: +1. 将 `build_workflow.yml` 放入 `.github/workflows/` 目录 +2. 推送到GitHub仓库 +3. 自动构建Windows x86/x64和macOS版本 + +## 📁 文件说明 + +### 核心文件 +- `seat_allocation_system.py` - 主程序 +- `simple_build.py` - 简化构建脚本 +- `cross_platform_build.py` - 跨平台构建脚本 +- `requirements.txt` - Python依赖 + +### 配置文件 +- `build_workflow.yml` - GitHub Actions工作流 +- `人员信息.xlsx` - 示例人员数据 +- `座位信息.xlsx` - 示例座位数据 + +## 🎯 支持的平台 + +### 直接运行 +- Windows 7/8/10/11 (x86/x64/ARM64) +- macOS 10.14+ (Intel/Apple Silicon) +- Linux (x86_64/ARM64) + +### 构建目标 +- **Windows x86** - 32位兼容版本 +- **Windows x64** - 64位主流版本 +- **macOS** - Intel和Apple Silicon +- **Linux** - 主流发行版 + +## 📊 数据格式要求 + +### 人员信息格式 +``` +姓名 | 证件类型 | 证件号 | 手机号 | 备注 +张三 | 身份证 | 123456789012345678 | 13800138000 | 2 +李四 | 身份证 | 123456789012345679 | 13800138001 | +王五 | 身份证 | 12345678901234567X | 13800138002 | 3 +赵六 | 身份证 | 123456789012345680 | 13800138003 | +钱七 | 身份证 | 123456789012345681 | 13800138004 | +``` + +### 座位信息格式 +``` +区域 | 楼层 | 排号 | 座位号 +A区通道 | 一层 | 1排 | 1号 +A区通道 | 一层 | 1排 | 2号 +A区通道 | 一层 | 1排 | 3号 +``` + +## ⚠️ 注意事项 + +1. **备注逻辑**: 备注数字表示连坐人数,只有组长填写备注,成员留空 +2. **证件号格式**: 支持包含X的身份证号,自动处理为字符串格式 +3. **文字清理**: 自动清除姓名等字段的多余空格 +4. **座位连续性**: 支持不连续座位号,算法会自动寻找合适的连续段 + +## 🔍 故障排除 + +### 常见问题 +1. **文件格式错误**: 确保Excel文件包含必需的列 +2. **编码问题**: 确保Excel文件使用UTF-8编码 +3. **权限问题**: 确保有读写Excel文件的权限 +4. **内存不足**: 处理大量数据时可能需要更多内存 + +### 技术支持 +如遇问题,请查看生成的日志文件: +- `seat_allocation_log.txt` - 详细的运行日志 +- 控制台输出 - 实时处理信息 + +## 📈 版本历史 + +- **v1.0** - 初始版本,支持基本座位分配 +- **v1.1** - 添加数据校验和错误处理 +- **v1.2** - 支持不连续座位处理 +- **v1.3** - 优化数据类型处理,支持跨平台构建 + +## 📄 许可证 + +本项目采用MIT许可证,详见LICENSE文件。 diff --git a/build_workflow.yml b/build_workflow.yml new file mode 100644 index 0000000..b2cfde3 --- /dev/null +++ b/build_workflow.yml @@ -0,0 +1,89 @@ +# GitHub Actions工作流 - 多平台构建 +# 将此文件放在 .github/workflows/ 目录下 + +name: Build Multi-Platform Executables + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + +jobs: + build-windows: + runs-on: windows-latest + strategy: + matrix: + arch: [x64, x86] + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + architecture: ${{ matrix.arch }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pandas openpyxl pyinstaller + + - name: Build executable + run: | + pyinstaller --onefile --console --name "座位分配系统_${{ matrix.arch }}" seat_allocation_system.py + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: windows-${{ matrix.arch }} + path: dist/座位分配系统_${{ matrix.arch }}.exe + + build-macos: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pandas openpyxl pyinstaller + + - name: Build executable + run: | + pyinstaller --onefile --console --name "座位分配系统_macos" seat_allocation_system.py + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: macos + path: dist/座位分配系统_macos + + create-release: + needs: [build-windows, build-macos] + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + + steps: + - name: Download all artifacts + uses: actions/download-artifact@v3 + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + tag_name: v${{ github.run_number }} + name: Release v${{ github.run_number }} + files: | + windows-x64/座位分配系统_x64.exe + windows-x86/座位分配系统_x86.exe + macos/座位分配系统_macos + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/cross_platform_build.py b/cross_platform_build.py new file mode 100644 index 0000000..5a660a5 --- /dev/null +++ b/cross_platform_build.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +跨平台构建脚本 +在macOS上构建Windows x86/x64版本的exe文件 +使用Wine和PyInstaller实现跨平台编译 +""" + +import os +import sys +import subprocess +import platform +import shutil +from pathlib import Path + +def check_dependencies(): + """检查必要的依赖""" + print("检查构建依赖...") + + # 检查是否在macOS上 + if platform.system() != 'Darwin': + print(f"当前系统: {platform.system()}") + print("此脚本专为macOS设计,用于构建Windows版本") + return False + + # 检查Python包 + required_packages = ['pandas', 'openpyxl', 'pyinstaller'] + missing_packages = [] + + for package in required_packages: + try: + __import__(package) + print(f"✅ {package} 已安装") + except ImportError: + missing_packages.append(package) + print(f"❌ {package} 未安装") + + if missing_packages: + print(f"\n安装缺失的包...") + for package in missing_packages: + try: + subprocess.check_call([sys.executable, '-m', 'pip', 'install', package]) + print(f"✅ {package} 安装成功") + except subprocess.CalledProcessError: + print(f"❌ {package} 安装失败") + return False + + return True + +def install_wine(): + """安装Wine(如果需要)""" + print("\n检查Wine环境...") + + # 检查是否已安装Wine + try: + result = subprocess.run(['wine', '--version'], capture_output=True, text=True) + if result.returncode == 0: + print(f"✅ Wine已安装: {result.stdout.strip()}") + return True + except FileNotFoundError: + pass + + print("Wine未安装,开始安装...") + print("请按照以下步骤手动安装Wine:") + print("1. 安装Homebrew (如果未安装): /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"") + print("2. 安装Wine: brew install --cask wine-stable") + print("3. 配置Wine: winecfg") + + response = input("是否已完成Wine安装? (y/N): ") + return response.lower() == 'y' + +def setup_wine_python(): + """在Wine中设置Python环境""" + print("\n设置Wine Python环境...") + + # 检查Wine中是否已安装Python + try: + result = subprocess.run(['wine', 'python', '--version'], capture_output=True, text=True) + if result.returncode == 0: + print(f"✅ Wine Python已安装: {result.stdout.strip()}") + return True + except: + pass + + print("需要在Wine中安装Python...") + print("请按照以下步骤:") + print("1. 下载Windows版Python: https://www.python.org/downloads/windows/") + print("2. 使用Wine安装: wine python-3.10.x-amd64.exe") + print("3. 安装pip包: wine python -m pip install pandas openpyxl pyinstaller") + + response = input("是否已完成Wine Python安装? (y/N): ") + return response.lower() == 'y' + +def create_build_spec(target_arch='x64'): + """创建构建规格文件""" + exe_name = f"座位分配系统_{target_arch}" + + spec_content = f'''# -*- mode: python ; coding: utf-8 -*- + +block_cipher = None + +a = Analysis( + ['seat_allocation_system.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=[ + 'pandas', + 'openpyxl', + 'numpy', + 'xlsxwriter', + 'xlrd', + 'datetime', + 'pathlib' + ], + hookspath=[], + hooksconfig={{}}, + runtime_hooks=[], + excludes=[ + 'matplotlib', + 'scipy', + 'IPython', + 'jupyter', + 'notebook', + 'tkinter' + ], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) + +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='{exe_name}', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=None, +) +''' + + spec_file = f'{exe_name}.spec' + with open(spec_file, 'w', encoding='utf-8') as f: + f.write(spec_content) + + return spec_file, exe_name + +def build_with_docker(): + """使用Docker构建Windows版本""" + print("\n使用Docker构建Windows版本...") + + # 创建Dockerfile + dockerfile_content = '''FROM python:3.10-windowsservercore + +# 设置工作目录 +WORKDIR /app + +# 复制文件 +COPY requirements.txt . +COPY seat_allocation_system.py . + +# 安装依赖 +RUN pip install -r requirements.txt + +# 构建exe +RUN pyinstaller --onefile --console --name "座位分配系统_x64" seat_allocation_system.py + +# 输出目录 +VOLUME ["/app/dist"] +''' + + with open('Dockerfile.windows', 'w') as f: + f.write(dockerfile_content) + + print("Docker方法需要Docker Desktop和Windows容器支持") + print("这是一个高级选项,建议使用其他方法") + return False + +def build_native_macos(): + """在macOS上直接构建(生成macOS版本)""" + print("\n在macOS上构建本地版本...") + + # 清理之前的构建 + if os.path.exists('dist'): + shutil.rmtree('dist') + if os.path.exists('build'): + shutil.rmtree('build') + + # 构建命令 + cmd = [ + sys.executable, '-m', 'PyInstaller', + '--onefile', + '--console', + '--clean', + '--name', '座位分配系统_macos', + 'seat_allocation_system.py' + ] + + try: + print(f"执行命令: {' '.join(cmd)}") + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode == 0: + print("✅ macOS版本构建成功!") + + # 检查生成的文件 + app_path = Path('dist') / '座位分配系统_macos' + if app_path.exists(): + file_size = app_path.stat().st_size / (1024 * 1024) # MB + print(f"生成文件: {app_path}") + print(f"文件大小: {file_size:.1f} MB") + return True, app_path + else: + print("❌ 构建失败!") + print("错误输出:") + print(result.stderr) + return False, None + + except Exception as e: + print(f"❌ 构建过程中出现错误: {e}") + return False, None + +def create_cross_compile_script(): + """创建跨编译脚本""" + script_content = '''#!/bin/bash +# Windows交叉编译脚本 + +echo "座位分配系统 - Windows交叉编译" +echo "================================" + +# 检查是否安装了mingw-w64 +if ! command -v x86_64-w64-mingw32-gcc &> /dev/null; then + echo "安装mingw-w64..." + brew install mingw-w64 +fi + +# 设置交叉编译环境 +export CC=x86_64-w64-mingw32-gcc +export CXX=x86_64-w64-mingw32-g++ + +# 构建Windows版本 +echo "开始构建Windows x64版本..." +python -m PyInstaller \\ + --onefile \\ + --console \\ + --clean \\ + --name "座位分配系统_x64" \\ + --target-arch x86_64 \\ + seat_allocation_system.py + +echo "构建完成!" +''' + + with open('cross_compile.sh', 'w') as f: + f.write(script_content) + + # 设置执行权限 + os.chmod('cross_compile.sh', 0o755) + print("✅ 已创建交叉编译脚本: cross_compile.sh") + +def main(): + """主函数""" + print("座位分配系统 - 跨平台构建工具") + print("=" * 50) + print("在macOS上构建Windows版本") + + # 检查依赖 + if not check_dependencies(): + return + + print("\n选择构建方法:") + print("1. 构建macOS版本(推荐)") + print("2. 使用Wine构建Windows版本(复杂)") + print("3. 创建交叉编译脚本(实验性)") + print("4. 生成Docker构建文件(高级)") + + choice = input("\n请选择 (1-4): ").strip() + + if choice == '1': + print("\n构建macOS版本...") + success, app_path = build_native_macos() + if success: + print(f"\n🎉 构建完成!") + print(f"生成文件: {app_path}") + print("\n注意: 这是macOS版本,不能在Windows上运行") + print("如需Windows版本,请在Windows系统上运行构建脚本") + + elif choice == '2': + print("\n使用Wine构建Windows版本...") + if install_wine() and setup_wine_python(): + print("Wine环境已准备就绪") + print("请手动在Wine中运行: wine python windows_build.py") + else: + print("Wine环境设置失败") + + elif choice == '3': + print("\n创建交叉编译脚本...") + create_cross_compile_script() + print("请运行: ./cross_compile.sh") + + elif choice == '4': + print("\n生成Docker构建文件...") + build_with_docker() + + else: + print("无效选择") + +if __name__ == "__main__": + main() diff --git a/seat_allocation_system.py b/seat_allocation_system.py new file mode 100644 index 0000000..fad598b --- /dev/null +++ b/seat_allocation_system.py @@ -0,0 +1,898 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +完整的座位分配系统 +包含文件校验、智能分配算法和日志输出功能 +支持1-10人连坐需求,能够处理不连续座位 +""" + +import pandas as pd +import numpy as np +from pathlib import Path +import datetime +import sys + +class Logger: + """日志记录器""" + def __init__(self, log_file='seat_allocation_log.txt'): + self.log_file = log_file + self.logs = [] + + def log(self, message, print_to_console=True): + """记录日志""" + timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + log_entry = f"[{timestamp}] {message}" + self.logs.append(log_entry) + + if print_to_console: + print(message) + + def save_logs(self): + """保存日志到文件""" + try: + with open(self.log_file, 'w', encoding='utf-8') as f: + f.write('\n'.join(self.logs)) + self.log(f"日志已保存到: {self.log_file}") + return True + except Exception as e: + print(f"保存日志失败: {e}") + return False + +class SeatAllocationSystem: + """座位分配系统""" + + def __init__(self): + self.logger = Logger() + self.personnel_df = None + self.seat_df = None + self.seating_groups = [] + self.row_analysis = {} + + def load_data(self): + """加载人员信息和座位信息数据""" + self.logger.log("=== 开始加载数据 ===") + + # 检查文件是否存在 + personnel_file = '人员信息.xlsx' + seat_file = '座位信息.xlsx' + + if not Path(personnel_file).exists(): + self.logger.log(f"❌ 错误: {personnel_file} 文件不存在") + return False + + if not Path(seat_file).exists(): + self.logger.log(f"❌ 错误: {seat_file} 文件不存在") + return False + + try: + # 读取人员信息,指定数据类型 + self.personnel_df = pd.read_excel(personnel_file, dtype={ + '姓名': 'str', + '证件类型': 'str', + '证件号': 'str', # 证件号作为字符串读取,避免X被转换 + '手机号': 'str' + }) + + # 读取座位信息,指定数据类型 + self.seat_df = pd.read_excel(seat_file, dtype={ + '区域': 'str', + '楼层': 'str', + '排号': 'str', + '座位号': 'str', + '姓名': 'str', + '证件类型': 'str', + '证件号': 'str', + '手机号': 'str', + '签发地/国籍': 'str' + }) + + # 清理文字信息中的空格 + self.clean_text_data() + + self.logger.log(f"✅ 文件加载成功") + self.logger.log(f" 人员信息: {self.personnel_df.shape[0]} 行 × {self.personnel_df.shape[1]} 列") + self.logger.log(f" 座位信息: {self.seat_df.shape[0]} 行 × {self.seat_df.shape[1]} 列") + return True + except Exception as e: + self.logger.log(f"❌ 文件加载失败: {e}") + return False + + def clean_text_data(self): + """清理文字数据中的空格""" + self.logger.log("清理文字数据中的空格...") + + # 清理人员信息中的空格 + text_columns_personnel = ['姓名', '证件类型', '证件号'] + for col in text_columns_personnel: + if col in self.personnel_df.columns: + self.personnel_df[col] = self.personnel_df[col].astype(str).str.strip() + + # 清理座位信息中的空格 + text_columns_seat = ['区域', '楼层', '排号', '座位号', '姓名', '证件类型', '证件号', '签发地/国籍'] + for col in text_columns_seat: + if col in self.seat_df.columns: + # 只对非空值进行清理 + mask = self.seat_df[col].notna() + self.seat_df.loc[mask, col] = self.seat_df.loc[mask, col].astype(str).str.strip() + + self.logger.log("✅ 文字数据清理完成") + + def validate_personnel_structure(self): + """校验人员信息文件结构""" + self.logger.log("\n=== 人员信息结构校验 ===") + + required_columns = ['姓名', '证件类型', '证件号', '手机号', '备注'] + validation_results = [] + + # 检查必需列 + missing_columns = [] + for col in required_columns: + if col not in self.personnel_df.columns: + missing_columns.append(col) + + if missing_columns: + self.logger.log(f"❌ 缺少必需列: {missing_columns}") + validation_results.append(False) + else: + self.logger.log("✅ 所有必需列都存在") + validation_results.append(True) + + # 检查数据完整性 + self.logger.log("\n数据完整性检查:") + for col in ['姓名', '证件类型', '证件号', '手机号']: + if col in self.personnel_df.columns: + null_count = self.personnel_df[col].isnull().sum() + if null_count > 0: + self.logger.log(f"⚠️ {col} 列有 {null_count} 个空值") + validation_results.append(False) + else: + self.logger.log(f"✅ {col} 列数据完整") + validation_results.append(True) + + # 检查重复姓名 + duplicate_names = self.personnel_df[self.personnel_df['姓名'].duplicated()] + if not duplicate_names.empty: + self.logger.log(f"⚠️ 发现重复姓名: {duplicate_names['姓名'].tolist()}") + validation_results.append(False) + else: + self.logger.log("✅ 姓名无重复") + validation_results.append(True) + + return all(validation_results) + + def validate_seating_groups(self): + """校验连坐组的完整性和正确性""" + self.logger.log("\n=== 连坐组校验 ===") + + validation_results = [] + issues = [] + + i = 0 + group_num = 1 + + while i < len(self.personnel_df): + person = self.personnel_df.iloc[i] + remark = person['备注'] + + if pd.isna(remark): + # 无备注,单独坐 + self.logger.log(f"✅ 第 {group_num} 组: {person['姓名']} (单独)") + i += 1 + else: + # 有备注,检查连坐组 + try: + group_size = int(remark) + + if group_size < 1 or group_size > 10: + issue = f"❌ 第 {group_num} 组: {person['姓名']} 的备注 {group_size} 超出范围 (1-10)" + self.logger.log(issue) + issues.append(issue) + validation_results.append(False) + i += 1 + continue + + # 检查后续人员是否足够 + if i + group_size > len(self.personnel_df): + issue = f"❌ 第 {group_num} 组: {person['姓名']} 需要 {group_size} 人连坐,但后续人员不足" + self.logger.log(issue) + issues.append(issue) + validation_results.append(False) + i += 1 + continue + + # 检查后续人员的备注是否为空 + group_members = [] + valid_group = True + + for j in range(group_size): + member = self.personnel_df.iloc[i + j] + group_members.append(member['姓名']) + + if j == 0: + # 第一个人应该有备注 + if pd.isna(member['备注']) or int(member['备注']) != group_size: + issue = f"❌ 第 {group_num} 组: 组长 {member['姓名']} 的备注应该是 {group_size}" + self.logger.log(issue) + issues.append(issue) + valid_group = False + else: + # 后续人员应该没有备注 + if not pd.isna(member['备注']): + issue = f"❌ 第 {group_num} 组: {member['姓名']} 应该没有备注(当前备注: {member['备注']})" + self.logger.log(issue) + issues.append(issue) + valid_group = False + + if valid_group: + self.logger.log(f"✅ 第 {group_num} 组: {', '.join(group_members)} ({group_size}人连坐)") + validation_results.append(True) + else: + validation_results.append(False) + + i += group_size + + except ValueError: + issue = f"❌ 第 {group_num} 组: {person['姓名']} 的备注 '{remark}' 不是有效数字" + self.logger.log(issue) + issues.append(issue) + validation_results.append(False) + i += 1 + + group_num += 1 + + return all(validation_results), issues + + def validate_seat_structure(self): + """校验座位信息结构和连续性""" + self.logger.log("\n=== 座位信息结构校验 ===") + + required_columns = ['区域', '楼层', '排号', '座位号'] + validation_results = [] + + # 检查必需列 + missing_columns = [] + for col in required_columns: + if col not in self.seat_df.columns: + missing_columns.append(col) + + if missing_columns: + self.logger.log(f"❌ 缺少必需列: {missing_columns}") + return False, {} + + self.logger.log("✅ 所有必需列都存在") + + # 检查数据完整性 + for col in required_columns: + null_count = self.seat_df[col].isnull().sum() + if null_count > 0: + self.logger.log(f"❌ {col} 列有 {null_count} 个空值") + validation_results.append(False) + else: + self.logger.log(f"✅ {col} 列数据完整") + validation_results.append(True) + + # 分析座位结构 + self.logger.log("\n座位结构分析:") + seat_groups = {} + for _, row in self.seat_df.iterrows(): + key = (row['区域'], row['楼层'], row['排号']) + if key not in seat_groups: + seat_groups[key] = [] + seat_groups[key].append(row['座位号']) + + # 检查每排的座位结构和可用连续段 + self.logger.log("\n座位结构分析:") + for (area, floor, row_num), seats in seat_groups.items(): + # 提取座位号数字 + seat_numbers = [] + for seat in seats: + try: + seat_numbers.append(int(str(seat).replace('号', ''))) + except: + self.logger.log(f"❌ {area}-{floor}-{row_num}: 座位号格式异常 '{seat}'") + validation_results.append(False) + continue + + seat_numbers.sort() + + # 分析连续段 + consecutive_segments = [] + if seat_numbers: + current_segment = [seat_numbers[0]] + + for i in range(1, len(seat_numbers)): + if seat_numbers[i] - seat_numbers[i-1] == 1: + # 连续 + current_segment.append(seat_numbers[i]) + else: + # 不连续,保存当前段,开始新段 + consecutive_segments.append(current_segment) + current_segment = [seat_numbers[i]] + + # 添加最后一段 + consecutive_segments.append(current_segment) + + # 显示分析结果 + if len(consecutive_segments) == 1 and len(consecutive_segments[0]) == len(seat_numbers): + self.logger.log(f"✅ {area}-{floor}-{row_num}: {len(seats)} 个座位完全连续 ({min(seat_numbers)}-{max(seat_numbers)})") + else: + segments_info = [] + max_segment_size = 0 + for segment in consecutive_segments: + if len(segment) == 1: + segments_info.append(f"{segment[0]}") + else: + segments_info.append(f"{segment[0]}-{segment[-1]}") + max_segment_size = max(max_segment_size, len(segment)) + + self.logger.log(f"📊 {area}-{floor}-{row_num}: {len(seats)} 个座位,{len(consecutive_segments)} 个连续段: {', '.join(segments_info)}") + self.logger.log(f" 最大连续段: {max_segment_size} 个座位") + + validation_results.append(True) # 座位不连续不算错误,只是需要特殊处理 + + return all(validation_results), seat_groups + + def validate_capacity_feasibility(self, seat_groups): + """校验座位容量和分配可行性""" + self.logger.log("\n=== 容量和可行性校验 ===") + + # 统计人员总数 + total_people = len(self.personnel_df) + total_seats = sum(len(seats) for seats in seat_groups.values()) + + self.logger.log(f"总人数: {total_people}") + self.logger.log(f"总座位数: {total_seats}") + + if total_people > total_seats: + self.logger.log(f"❌ 座位不足: 需要 {total_people} 个座位,只有 {total_seats} 个") + return False + else: + self.logger.log(f"✅ 座位充足: 剩余 {total_seats - total_people} 个座位") + + # 分析连坐组需求 + self.logger.log("\n连坐组需求分析:") + group_sizes = [] + i = 0 + + while i < len(self.personnel_df): + person = self.personnel_df.iloc[i] + remark = person['备注'] + + if pd.isna(remark): + group_sizes.append(1) + i += 1 + else: + try: + group_size = int(remark) + group_sizes.append(group_size) + i += group_size + except: + group_sizes.append(1) + i += 1 + + max_group_size = max(group_sizes) + self.logger.log(f"最大连坐组: {max_group_size} 人") + + # 检查是否有足够的连续座位来容纳最大连坐组 + self.logger.log(f"\n连续座位可行性分析:") + max_consecutive_available = 0 + feasible_rows = [] + + for (area, floor, row_num), seats in seat_groups.items(): + # 分析每排的连续座位段 + seat_numbers = [] + for seat in seats: + try: + seat_numbers.append(int(str(seat).replace('号', ''))) + except: + continue + + seat_numbers.sort() + + # 找出最大连续段 + max_consecutive_in_row = 0 + if seat_numbers: + current_consecutive = 1 + for i in range(1, len(seat_numbers)): + if seat_numbers[i] - seat_numbers[i-1] == 1: + current_consecutive += 1 + else: + max_consecutive_in_row = max(max_consecutive_in_row, current_consecutive) + current_consecutive = 1 + max_consecutive_in_row = max(max_consecutive_in_row, current_consecutive) + + if max_consecutive_in_row >= max_group_size: + feasible_rows.append((area, floor, row_num, max_consecutive_in_row)) + + max_consecutive_available = max(max_consecutive_available, max_consecutive_in_row) + self.logger.log(f" {area}-{floor}-{row_num}: 最大连续 {max_consecutive_in_row} 个座位") + + self.logger.log(f"\n全场最大连续座位: {max_consecutive_available} 个") + + if max_group_size > max_consecutive_available: + self.logger.log(f"❌ 无法容纳最大连坐组: 需要 {max_group_size} 个连续座位,最大连续段只有 {max_consecutive_available} 个") + return False + else: + self.logger.log(f"✅ 可以容纳最大连坐组") + self.logger.log(f"可容纳最大连坐组的排数: {len(feasible_rows)} 个") + + # 统计各种大小的连坐组 + size_counts = {} + for size in group_sizes: + size_counts[size] = size_counts.get(size, 0) + 1 + + self.logger.log("\n连坐组分布:") + for size in sorted(size_counts.keys()): + count = size_counts[size] + if size == 1: + self.logger.log(f" 单人组: {count} 个") + else: + self.logger.log(f" {size}人连坐组: {count} 个") + + return True + + def analyze_seating_requirements(self): + """分析人员连坐需求""" + self.logger.log("\n=== 人员连坐需求分析 ===") + + self.seating_groups = [] + i = 0 + + while i < len(self.personnel_df): + person = self.personnel_df.iloc[i] + remark = person['备注'] + + if pd.isna(remark): + # 无备注,单独坐 + self.seating_groups.append({ + 'type': 'single', + 'size': 1, + 'members': [person], + 'leader': person['姓名'] + }) + i += 1 + else: + # 有备注,按备注数量连坐 + group_size = int(remark) + group_members = [] + + # 收集连坐组成员 + for j in range(group_size): + if i + j < len(self.personnel_df): + group_members.append(self.personnel_df.iloc[i + j]) + + self.seating_groups.append({ + 'type': 'group' if group_size > 1 else 'single', + 'size': len(group_members), + 'members': group_members, + 'leader': person['姓名'] + }) + + i += group_size + + # 统计 + size_stats = {} + for group in self.seating_groups: + size = group['size'] + size_stats[size] = size_stats.get(size, 0) + 1 + + self.logger.log(f"总共识别出 {len(self.seating_groups)} 个座位组:") + for size in sorted(size_stats.keys()): + count = size_stats[size] + if size == 1: + self.logger.log(f" 单人组: {count} 个") + else: + self.logger.log(f" {size}人连坐组: {count} 个") + + return True + + def analyze_seat_structure_advanced(self): + """高级座位结构分析,识别连续段""" + self.logger.log("\n=== 高级座位结构分析 ===") + + seat_groups = {} + for _, row in self.seat_df.iterrows(): + key = (row['区域'], row['楼层'], row['排号']) + if key not in seat_groups: + seat_groups[key] = [] + seat_groups[key].append({ + 'index': row.name, + '座位号': row['座位号'], + 'row_data': row + }) + + # 分析每排的连续段 + self.row_analysis = {} + for key, seats in seat_groups.items(): + area, floor, row_num = key + + # 按座位号排序 + def get_seat_number(seat_info): + try: + return int(seat_info['座位号'].replace('号', '')) + except: + return 0 + + seats.sort(key=get_seat_number) + seat_numbers = [get_seat_number(seat) for seat in seats] + + # 找出所有连续段 + consecutive_segments = [] + if seat_numbers: + current_segment_start = 0 + + for i in range(1, len(seat_numbers)): + if seat_numbers[i] - seat_numbers[i-1] != 1: + # 发现间隙,结束当前段 + consecutive_segments.append({ + 'start_idx': current_segment_start, + 'end_idx': i - 1, + 'size': i - current_segment_start, + 'seat_numbers': seat_numbers[current_segment_start:i], + 'seats': seats[current_segment_start:i] + }) + current_segment_start = i + + # 添加最后一段 + consecutive_segments.append({ + 'start_idx': current_segment_start, + 'end_idx': len(seat_numbers) - 1, + 'size': len(seat_numbers) - current_segment_start, + 'seat_numbers': seat_numbers[current_segment_start:], + 'seats': seats[current_segment_start:] + }) + + self.row_analysis[key] = { + 'total_seats': len(seats), + 'all_seats': seats, + 'consecutive_segments': consecutive_segments, + 'max_consecutive': max(seg['size'] for seg in consecutive_segments) if consecutive_segments else 0 + } + + # 显示分析结果 + if len(consecutive_segments) == 1: + self.logger.log(f"✅ {area}-{floor}-{row_num}: {len(seats)} 个座位完全连续") + else: + segments_info = [] + for seg in consecutive_segments: + if seg['size'] == 1: + segments_info.append(f"{seg['seat_numbers'][0]}") + else: + segments_info.append(f"{seg['seat_numbers'][0]}-{seg['seat_numbers'][-1]}") + self.logger.log(f"📊 {area}-{floor}-{row_num}: {len(seats)} 个座位,{len(consecutive_segments)} 段: {', '.join(segments_info)}") + self.logger.log(f" 最大连续段: {self.row_analysis[key]['max_consecutive']} 个座位") + + return True + + def smart_seat_assignment(self): + """智能座位分配算法""" + self.logger.log("\n=== 开始智能座位分配 ===") + + # 按组大小排序,大组优先分配 + sorted_groups = sorted(self.seating_groups, key=lambda x: x['size'], reverse=True) + + # 创建座位分配结果 + seat_df_copy = self.seat_df.copy() + + # 记录已使用的座位 + used_seats = set() + + assignment_log = [] + unassigned_groups = [] + + self.logger.log(f"需要分配 {len(sorted_groups)} 个组") + + for group_idx, seating_group in enumerate(sorted_groups): + group_size = seating_group['size'] + group_type = seating_group['type'] + leader = seating_group['leader'] + + self.logger.log(f"\n处理第 {group_idx + 1} 组: {leader} ({group_type}, {group_size} 人)") + + if group_size == 1: + # 单人组,找任意可用座位 + assigned = False + for (area, floor, row_num), analysis in self.row_analysis.items(): + for seat_info in analysis['all_seats']: + if seat_info['index'] not in used_seats: + # 分配座位 + person_info = seating_group['members'][0] + seat_index = seat_info['index'] + + # 更新座位信息,确保数据类型正确 + seat_df_copy.loc[seat_index, '姓名'] = str(person_info['姓名']).strip() + seat_df_copy.loc[seat_index, '证件类型'] = str(person_info['证件类型']).strip() + seat_df_copy.loc[seat_index, '证件号'] = str(person_info['证件号']).strip() + seat_df_copy.loc[seat_index, '手机国家号'] = person_info.get('Unnamed: 3', 86) + seat_df_copy.loc[seat_index, '手机号'] = str(person_info['手机号']).strip() + seat_df_copy.loc[seat_index, '签发地/国籍'] = str(person_info.get('备注', '')).strip() + + assignment_log.append({ + '组号': group_idx + 1, + '组类型': group_type, + '组大小': group_size, + '组长': leader, + '姓名': person_info['姓名'], + '区域': area, + '楼层': floor, + '排号': row_num, + '座位号': seat_info['座位号'] + }) + + used_seats.add(seat_index) + self.logger.log(f" 分配到 {area}-{floor}-{row_num}: {person_info['姓名']} -> {seat_info['座位号']}") + assigned = True + break + if assigned: + break + else: + # 多人组,需要连续座位 + # 更新可用座位分析(排除已使用的座位) + current_row_analysis = {} + for key, analysis in self.row_analysis.items(): + available_seats = [seat for seat in analysis['all_seats'] if seat['index'] not in used_seats] + if available_seats: + # 重新分析连续段 + available_seats.sort(key=lambda x: int(x['座位号'].replace('号', ''))) + seat_numbers = [int(seat['座位号'].replace('号', '')) for seat in available_seats] + + # 找出连续段 + consecutive_segments = [] + if seat_numbers: + current_segment_start = 0 + + for i in range(1, len(seat_numbers)): + if seat_numbers[i] - seat_numbers[i-1] != 1: + consecutive_segments.append({ + 'start_idx': current_segment_start, + 'end_idx': i - 1, + 'size': i - current_segment_start, + 'seats': available_seats[current_segment_start:i] + }) + current_segment_start = i + + consecutive_segments.append({ + 'start_idx': current_segment_start, + 'end_idx': len(seat_numbers) - 1, + 'size': len(seat_numbers) - current_segment_start, + 'seats': available_seats[current_segment_start:] + }) + + current_row_analysis[key] = { + 'consecutive_segments': consecutive_segments + } + + # 寻找合适的连续座位 + assigned = False + for (area, floor, row_num), analysis in current_row_analysis.items(): + for segment in analysis['consecutive_segments']: + if segment['size'] >= group_size: + # 找到合适的连续座位 + seats_to_use = segment['seats'][:group_size] + seat_numbers = [int(seat['座位号'].replace('号', '')) for seat in seats_to_use] + + self.logger.log(f" 分配到 {area}-{floor}-{row_num} (连续座位: {min(seat_numbers)}-{max(seat_numbers)})") + + # 分配座位 + for i, seat_info in enumerate(seats_to_use): + person_info = seating_group['members'][i] + seat_index = seat_info['index'] + + seat_df_copy.loc[seat_index, '姓名'] = str(person_info['姓名']).strip() + seat_df_copy.loc[seat_index, '证件类型'] = str(person_info['证件类型']).strip() + seat_df_copy.loc[seat_index, '证件号'] = str(person_info['证件号']).strip() + seat_df_copy.loc[seat_index, '手机国家号'] = person_info.get('Unnamed: 3', 86) + seat_df_copy.loc[seat_index, '手机号'] = str(person_info['手机号']).strip() + seat_df_copy.loc[seat_index, '签发地/国籍'] = str(person_info.get('备注', '')).strip() + + assignment_log.append({ + '组号': group_idx + 1, + '组类型': group_type, + '组大小': group_size, + '组长': leader, + '姓名': person_info['姓名'], + '区域': area, + '楼层': floor, + '排号': row_num, + '座位号': seat_info['座位号'] + }) + + used_seats.add(seat_index) + self.logger.log(f" {person_info['姓名']} -> {seat_info['座位号']}") + + assigned = True + break + if assigned: + break + + if not assigned: + self.logger.log(f" ❌ 无法为第 {group_idx + 1} 组分配座位") + unassigned_groups.append(seating_group) + + # 显示未分配的组 + if unassigned_groups: + self.logger.log(f"\n⚠️ 有 {len(unassigned_groups)} 个组未能分配座位") + for group in unassigned_groups: + if group['type'] == 'single': + self.logger.log(f" 未分配: {group['leader']}") + else: + member_names = [member['姓名'] for member in group['members']] + self.logger.log(f" 未分配: {', '.join(member_names)} (连坐 {group['size']} 人)") + + return seat_df_copy, assignment_log + + def save_results(self, seat_df_result, assignment_log): + """保存分配结果""" + try: + # 保存更新后的座位信息 + output_file = '座位信息_最终分配.xlsx' + seat_df_result.to_excel(output_file, index=False) + self.logger.log(f"\n座位分配结果已保存到: {output_file}") + + # 保存分配日志 + if assignment_log: + log_df = pd.DataFrame(assignment_log) + log_file = '最终座位分配日志.xlsx' + log_df.to_excel(log_file, index=False) + self.logger.log(f"分配日志已保存到: {log_file}") + + # 显示分配统计 + self.logger.log(f"\n=== 分配统计 ===") + self.logger.log(f"总共分配了 {len(assignment_log)} 个座位") + + # 按组大小统计 + size_stats = log_df.groupby('组大小').agg({ + '姓名': 'count', + '组号': 'nunique' + }).rename(columns={'姓名': '总人数', '组号': '组数'}) + + self.logger.log("\n按组大小统计:") + for size, row in size_stats.iterrows(): + if size == 1: + self.logger.log(f" 单人组: {row['组数']} 个组, {row['总人数']} 人") + else: + self.logger.log(f" {size}人连坐组: {row['组数']} 个组, {row['总人数']} 人") + + # 验证连续性 + self.logger.log("\n=== 连续性验证 ===") + consecutive_check = [] + + for group_num in sorted(log_df['组号'].unique()): + group_data = log_df[log_df['组号'] == group_num] + group_size = group_data.iloc[0]['组大小'] + group_leader = group_data.iloc[0]['组长'] + + if group_size > 1: # 多人组 + # 检查是否在同一排 + areas = group_data['区域'].unique() + floors = group_data['楼层'].unique() + rows = group_data['排号'].unique() + + if len(areas) == 1 and len(floors) == 1 and len(rows) == 1: + # 在同一排,检查座位号是否连续 + seats = group_data['座位号'].tolist() + seat_numbers = [] + for seat in seats: + try: + seat_numbers.append(int(seat.replace('号', ''))) + except: + seat_numbers.append(0) + + seat_numbers.sort() + is_consecutive = all(seat_numbers[i] + 1 == seat_numbers[i+1] for i in range(len(seat_numbers)-1)) + + if is_consecutive: + consecutive_check.append(True) + self.logger.log(f"✅ 组 {group_num} ({group_leader}): {group_size}人连坐,座位连续 {seat_numbers}") + else: + consecutive_check.append(False) + self.logger.log(f"❌ 组 {group_num} ({group_leader}): {group_size}人连坐,座位不连续 {seat_numbers}") + else: + consecutive_check.append(False) + self.logger.log(f"❌ 组 {group_num} ({group_leader}): 不在同一排") + + success_rate = sum(consecutive_check) / len(consecutive_check) * 100 if consecutive_check else 100 + self.logger.log(f"\n连续性检查结果: {sum(consecutive_check)}/{len(consecutive_check)} 个多人组座位连续 ({success_rate:.1f}%)") + + return True + except Exception as e: + self.logger.log(f"保存结果时出错: {e}") + return False + + def run_validation(self): + """运行完整的文件校验""" + self.logger.log("=" * 60) + self.logger.log("座位分配系统 - 文件校验") + self.logger.log("=" * 60) + + # 加载数据 + if not self.load_data(): + return False + + # 人员信息结构校验 + personnel_structure_valid = self.validate_personnel_structure() + + # 连坐组校验 + seating_groups_valid, group_issues = self.validate_seating_groups() + + # 座位信息校验 + seat_structure_valid, seat_groups = self.validate_seat_structure() + + # 容量可行性校验 + capacity_valid = self.validate_capacity_feasibility(seat_groups) + + # 总结 + self.logger.log("\n" + "=" * 60) + self.logger.log("校验结果总结") + self.logger.log("=" * 60) + + all_valid = all([ + personnel_structure_valid, + seating_groups_valid, + seat_structure_valid, + capacity_valid + ]) + + self.logger.log(f"人员信息结构: {'✅ 通过' if personnel_structure_valid else '❌ 失败'}") + self.logger.log(f"连坐组完整性: {'✅ 通过' if seating_groups_valid else '❌ 失败'}") + self.logger.log(f"座位信息结构: {'✅ 通过' if seat_structure_valid else '❌ 失败'}") + self.logger.log(f"容量可行性: {'✅ 通过' if capacity_valid else '❌ 失败'}") + + self.logger.log(f"\n总体校验结果: {'✅ 全部通过' if all_valid else '❌ 存在问题'}") + + if group_issues: + self.logger.log(f"\n发现的问题:") + for issue in group_issues: + self.logger.log(f" {issue}") + + if all_valid: + self.logger.log("\n🎉 文件校验通过,可以进行座位分配!") + else: + self.logger.log("\n⚠️ 请修复上述问题后再进行座位分配。") + + return all_valid + + def run_allocation(self): + """运行完整的座位分配""" + self.logger.log("\n" + "=" * 60) + self.logger.log("开始座位分配") + self.logger.log("=" * 60) + + # 分析人员连坐需求 + if not self.analyze_seating_requirements(): + return False + + # 高级座位结构分析 + if not self.analyze_seat_structure_advanced(): + return False + + # 执行智能座位分配 + seat_df_result, assignment_log = self.smart_seat_assignment() + + # 保存结果 + if self.save_results(seat_df_result, assignment_log): + self.logger.log("\n🎉 座位分配完成!") + return True + else: + self.logger.log("\n❌ 座位分配失败!") + return False + +def main(): + """主函数""" + system = SeatAllocationSystem() + + try: + # 运行校验 + if system.run_validation(): + # 校验通过,运行分配 + system.run_allocation() + + # 保存日志 + system.logger.save_logs() + + except Exception as e: + system.logger.log(f"系统运行出错: {e}") + system.logger.save_logs() + +if __name__ == "__main__": + main() diff --git a/simple_build.py b/simple_build.py new file mode 100644 index 0000000..91497e1 --- /dev/null +++ b/simple_build.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +简化构建脚本 +在当前平台构建可执行文件 +""" + +import os +import sys +import subprocess +import platform +import shutil +from pathlib import Path + +def get_platform_info(): + """获取平台信息""" + system = platform.system() + machine = platform.machine().lower() + + if system == 'Windows': + if machine in ['amd64', 'x86_64']: + return 'windows_x64' + elif machine in ['i386', 'i686', 'x86']: + return 'windows_x86' + else: + return f'windows_{machine}' + elif system == 'Darwin': + if machine in ['arm64', 'aarch64']: + return 'macos_arm64' + else: + return 'macos_x64' + elif system == 'Linux': + return f'linux_{machine}' + else: + return f'{system.lower()}_{machine}' + +def install_dependencies(): + """安装依赖""" + print("安装依赖包...") + packages = ['pandas', 'openpyxl', 'pyinstaller'] + + for package in packages: + try: + __import__(package) + print(f"✅ {package} 已安装") + except ImportError: + print(f"📦 安装 {package}...") + try: + subprocess.check_call([sys.executable, '-m', 'pip', 'install', package]) + print(f"✅ {package} 安装成功") + except subprocess.CalledProcessError: + print(f"❌ {package} 安装失败") + return False + return True + +def build_executable(): + """构建可执行文件""" + print("\n开始构建可执行文件...") + + # 获取平台信息 + platform_name = get_platform_info() + exe_name = f"座位分配系统_{platform_name}" + + print(f"目标平台: {platform_name}") + print(f"输出文件: {exe_name}") + + # 清理之前的构建 + if os.path.exists('dist'): + shutil.rmtree('dist') + if os.path.exists('build'): + shutil.rmtree('build') + + # 构建命令 + cmd = [ + sys.executable, '-m', 'PyInstaller', + '--onefile', + '--console', + '--clean', + '--name', exe_name, + 'seat_allocation_system.py' + ] + + try: + print(f"执行命令: {' '.join(cmd)}") + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode == 0: + print("✅ 构建成功!") + + # 检查生成的文件 + if platform.system() == 'Windows': + exe_path = Path('dist') / f'{exe_name}.exe' + else: + exe_path = Path('dist') / exe_name + + if exe_path.exists(): + file_size = exe_path.stat().st_size / (1024 * 1024) # MB + print(f"生成文件: {exe_path}") + print(f"文件大小: {file_size:.1f} MB") + return True, exe_path + else: + print("❌ 未找到生成的文件") + return False, None + else: + print("❌ 构建失败!") + print("错误输出:") + print(result.stderr) + return False, None + + except Exception as e: + print(f"❌ 构建过程中出现错误: {e}") + return False, None + +def create_distribution(): + """创建分发包""" + print("\n创建分发包...") + + platform_name = get_platform_info() + package_name = f"座位分配系统_{platform_name}_分发包" + package_dir = Path(package_name) + + # 创建分发目录 + if package_dir.exists(): + shutil.rmtree(package_dir) + package_dir.mkdir() + + # 复制可执行文件 + dist_dir = Path('dist') + if dist_dir.exists(): + for file in dist_dir.iterdir(): + if file.is_file(): + shutil.copy2(file, package_dir) + print(f"复制文件: {file.name}") + + # 复制示例文件 + if Path('人员信息.xlsx').exists(): + shutil.copy2('人员信息.xlsx', package_dir / '人员信息_示例.xlsx') + print("复制示例文件: 人员信息_示例.xlsx") + + if Path('座位信息.xlsx').exists(): + shutil.copy2('座位信息.xlsx', package_dir / '座位信息_示例.xlsx') + print("复制示例文件: 座位信息_示例.xlsx") + + # 创建使用说明 + readme_content = f"""座位分配系统 使用说明 + +平台: {platform_name} +构建时间: {platform.platform()} + +使用方法: +1. 准备Excel文件 + - 人员信息.xlsx: 包含姓名、证件信息、备注等 + - 座位信息.xlsx: 包含区域、楼层、排号、座位号等 + +2. 运行程序 + - 将可执行文件放在Excel文件同一目录 + - 双击运行可执行文件 + - 等待处理完成 + +3. 查看结果 + - 座位信息_最终分配.xlsx: 分配结果 + - 最终座位分配日志.xlsx: 详细记录 + - seat_allocation_log.txt: 运行日志 + +功能特点: +- 支持1-10人连坐需求 +- 自动处理不连续座位 +- 完整的数据校验 +- 详细的分配日志 + +注意事项: +- 确保Excel文件格式正确 +- 备注数字表示连坐人数 +- 运行时会在同目录生成结果文件 +""" + + with open(package_dir / '使用说明.txt', 'w', encoding='utf-8') as f: + f.write(readme_content) + + print(f"✅ 分发包已创建: {package_dir}") + return package_dir + +def main(): + """主函数""" + print("座位分配系统 - 简化构建工具") + print("=" * 50) + + # 显示系统信息 + print(f"系统: {platform.system()} {platform.release()}") + print(f"架构: {platform.machine()}") + print(f"Python: {sys.version}") + + # 检查主程序文件 + if not os.path.exists('seat_allocation_system.py'): + print("❌ 未找到 seat_allocation_system.py 文件") + return + + # 安装依赖 + if not install_dependencies(): + print("❌ 依赖安装失败") + return + + # 构建可执行文件 + success, exe_path = build_executable() + if not success: + print("❌ 构建失败") + return + + # 创建分发包 + package_dir = create_distribution() + + print("\n🎉 构建完成!") + print(f"✅ 可执行文件: {exe_path}") + print(f"✅ 分发包: {package_dir}") + + # 平台特定说明 + if platform.system() == 'Windows': + print("\n📝 Windows使用说明:") + print("- 可以直接在Windows系统上运行") + print("- 确保安装了Visual C++ Redistributable") + elif platform.system() == 'Darwin': + print("\n📝 macOS使用说明:") + print("- 只能在macOS系统上运行") + print("- 如需Windows版本,请在Windows系统上构建") + print("- 或使用GitHub Actions自动构建") + else: + print(f"\n📝 {platform.system()}使用说明:") + print("- 只能在相同系统上运行") + print("- 如需其他平台版本,请在对应系统上构建") + +if __name__ == "__main__": + main() diff --git a/人员信息.xlsx b/人员信息.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..366820a3bd5b2af0b472eb972bb9e9951e0f9701 GIT binary patch literal 16191 zcmeHu1$P`vl5UHcnb`u1nJi{zmcZVK5bPf?+ zHiX6|{wS}9RH_3-4|sdkwe4{5MRCEC5f$9|T-aaft7TwFfW0ygY zmsTnSiB@`2HTdsehheVl+Tb!mSy$_fh%Z!n*~vrbmy_dWU7Aj(ceDdK z6L~U7mYyu0F!y8}|<=P$YiYML+p~!(KTDX zT3!)(Pp$XpVd&^HsSy|6(sFQ2;Pe3dDcgh^0&g@u%0RRTT$cF?x!_|tx#n!>n(AL)S zcbF?w)3x2=MEBA&e;1xFXO%D8jx8jyz!oI=C3RYAk>-34lh)6=_Q83c9fCFey>r2D znz>TV3d%3*IA+`N{L{kb)%CaPO!Z>c6jITh6=6iQHriHKxgQJWdxOJ2g2VmQlxoqf zQ^j(iy51bx;*U(|cO$?6LM5fF#gdHkZX*`uQzm`j_{;B=Y49Kk+k zV{N88_%%a^aYG2xWAd6Br7>837{?T;3&+i^kzPAMteTYft0;5VWVfL|h9utW*!nW- zPg*jWaBNC~k)^(ImmWB&)x+UWbeVhEo}hnpP@29kWzojC4~S|hjCr9X{IEj|e!PvH z8H}z!@VRYbGdn4qZC+LY@oP%sDnN_b^|TxW_Pkh0U# z_sUAGlL{sO&@7L;`}mKLO<<^?F1bZ+LidlO(6!c9v1U{o_jn@OQsX zAsXV%foi21>q-xnxQf8>PTl^`iE*9hVkE5g*seC~#{<>tM34rys8L>9K2WAAi97N1 zvTbAaL2`-!>QCw4ijHl9T}R@vY*{zd=ZmA?A53nWt^}U1+8U8_ z)1~lNJKAv5cQUQ-37-KRjo($QBRz=E`U!KZ;b!(~mz8TFuqtasL7~qp2_~&{!=5~o z?LWRSu{p`HH8ds69_0lPAMd1@-ayNIJ#1;%#F3n=XW5FAenG@yb#(mz*mW7L z)BPBd>gv7jU~K7FAiErJ!X;vgK1fyr^$VIgz76EivlPqud)w!hOo zzDZQa^Fl1lzCA;+AyBDNb@5l2b;Ig`w_P2MB;7AmB?*Hr!=-K7^7?FeSC02T_vNERO{ulPug^79xJnO%gTn+hH|qn_sP=>)e^e)c z5TmDGF0uTjj14DqW<)*6);1VhIWn#7(~Il-Avy;$+CypJKZ^SofqpfKuz<=v^_ z#V~J5qM0ZDmIMr~JO4YclQsQ)g;;?aVK~`i?2DVw-l;8i9kkCK%ztwC7{7+Mcu)a= zD?0!H8;F;Gx_d`66B8##hQEH8e*68*1YO$XVKDaEh%1i~aqq^gQ2i-UL19s3)7TW6)JjZnvh=CHk6fg;n;%A;SDV{`b` z60}jdrZVC`&I$<1Z49qoU6%~>ELK&3cm@UzViewWPEdc%hn�(lWCvVpemEsc{z8 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