项目基本完成
This commit is contained in:
commit
672760bb25
8
.idea/TableSynthesis.iml
Normal file
8
.idea/TableSynthesis.iml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="jdk" jdkName="TableSynthesis" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
6
.idea/inspectionProfiles/Project_Default.xml
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="PyInterpreterInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
</profile>
|
||||
</component>
|
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
6
.idea/misc.xml
Normal file
6
.idea/misc.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="TableSynthesis" />
|
||||
</component>
|
||||
</project>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/TableSynthesis.iml" filepath="$PROJECT_DIR$/.idea/TableSynthesis.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
146
README.md
Normal file
146
README.md
Normal file
@ -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文件。
|
89
build_workflow.yml
Normal file
89
build_workflow.yml
Normal file
@ -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 }}
|
326
cross_platform_build.py
Normal file
326
cross_platform_build.py
Normal file
@ -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()
|
898
seat_allocation_system.py
Normal file
898
seat_allocation_system.py
Normal file
@ -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()
|
232
simple_build.py
Normal file
232
simple_build.py
Normal file
@ -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()
|
Loading…
Reference in New Issue
Block a user