初步开发完成

This commit is contained in:
xsl
2026-01-26 23:45:58 +08:00
parent 1fe6708e11
commit 989a0227c7
58 changed files with 150371 additions and 217 deletions
Binary file not shown.
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""
创建测试用户脚本
在数据库中创建测试用户:admin, market, other
"""
import asyncio
import sys
from pathlib import Path
# 添加项目路径
sys.path.insert(0, str(Path(__file__).parent))
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy import text
# 数据库配置
DATABASE_URL = 'mysql+aiomysql://root:rootpassword@localhost:3306/project_manager?charset=utf8mb4'
# 创建引擎
engine = create_async_engine(DATABASE_URL, echo=False)
AsyncSessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
def hash_password(password: str) -> str:
"""使用bcrypt哈希密码"""
import bcrypt
salt = bcrypt.gensalt(rounds=12)
hashed = bcrypt.hashpw(password.encode("utf-8"), salt)
return hashed.decode("utf-8")
async def create_test_users():
"""创建测试用户"""
async with AsyncSessionLocal() as session:
# 检查是否已有测试用户
result = await session.execute(text("SELECT username FROM users WHERE username IN ('market', 'other')"))
existing = result.fetchall()
if len(existing) >= 2:
print("测试用户已存在,跳过创建")
return
print("开始创建测试用户...")
# 检查market用户是否存在
result = await session.execute(text("SELECT id FROM users WHERE username = 'market'"))
market_exists = result.fetchone()
if not market_exists:
# 插入市场部用户
await session.execute(text("""
INSERT INTO users (username, password_hash, real_name, department, role, phone, is_active)
VALUES (:username, :password_hash, :real_name, :department, :role, :phone, :is_active)
"""), {
'username': 'market',
'password_hash': hash_password('market123'),
'real_name': '市场部用户',
'department': '市场部',
'role': 'market',
'phone': '13800000002',
'is_active': True
})
print(" - 市场部用户创建成功")
else:
print(" - 市场部用户已存在")
# 检查other用户是否存在
result = await session.execute(text("SELECT id FROM users WHERE username = 'other'"))
other_exists = result.fetchone()
if not other_exists:
# 插入其他部门用户
await session.execute(text("""
INSERT INTO users (username, password_hash, real_name, department, role, phone, is_active)
VALUES (:username, :password_hash, :real_name, :department, :role, :phone, :is_active)
"""), {
'username': 'other',
'password_hash': hash_password('other123'),
'real_name': '其他部门用户',
'department': '其他部门',
'role': 'other',
'phone': '13800000003',
'is_active': True
})
print(" - 其他部门用户创建成功")
else:
print(" - 其他部门用户已存在")
await session.commit()
print("\n✅ 测试用户创建完成!")
print("\n用户账号信息:")
print(" - admin / admin123 (系统管理员)")
print(" - market / market123 (市场部)")
print(" - other / other123 (其他部门)")
async def main():
"""主函数"""
print("="*60)
print("创建测试用户")
print("="*60)
try:
await create_test_users()
except Exception as e:
print(f"❌ 错误: {e}")
import traceback
traceback.print_exc()
finally:
await engine.dispose()
if __name__ == '__main__':
asyncio.run(main())
+5 -40
View File
@@ -1,26 +1,12 @@
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy.orm import declarative_base
from .settings import get_settings
import os
from config.settings import get_settings
# 从配置文件获取数据库URL
settings = get_settings()
DATABASE_URL = f'mysql+aiomysql://{settings.DB_USER}:{settings.DB_PASSWORD}@{settings.DB_HOST}:{settings.DB_PORT}/{settings.DB_NAME}?charset={settings.DB_CHARSET}'
# 检测是否使用测试环境(SQLite)
IS_TEST = settings.DEBUG or os.getenv("USE_SQLITE", "false").lower() == "true"
if IS_TEST:
# 测试环境使用SQLite
DATABASE_URL = "sqlite+aiosqlite:///:memory:"
else:
# 生产环境使用MySQL
DATABASE_URL = (
f"mysql+aiomysql://{settings.DB_USER}:{settings.DB_PASSWORD}"
f"@{settings.DB_HOST}:{settings.DB_PORT}/{settings.DB_NAME}"
f"?charset={settings.DB_CHARSET}"
)
engine = create_async_engine(DATABASE_URL, echo=settings.DEBUG, future=True)
engine = create_async_engine(DATABASE_URL, echo=False, future=True)
AsyncSessionLocal = async_sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
@@ -29,30 +15,9 @@ Base = declarative_base()
async def get_db():
"""获取数据库会话的依赖项函数
用于FastAPI依赖注入,自动管理数据库会话的生命周期
成功时自动提交,异常时自动回滚,最后确保关闭会话
第一次调用时自动创建表结构
"""
"""获取数据库会话"""
async with AsyncSessionLocal() as session:
try:
# 检查表是否存在,如果不存在则创建
from sqlalchemy import inspect, text
from src.models.user import User
from src.models.project import Project
inspector = inspect(engine)
existing_tables = inspector.get_table_names()
if "users" not in existing_tables:
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all, tables=[User.__tablename__])
elif "projects" not in existing_tables:
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all, tables=[Project.__tablename__])
yield session
await session.commit()
except Exception:
+309
View File
@@ -0,0 +1,309 @@
#!/usr/bin/env python3
"""
Excel数据导入脚本
从docs/example.xls导入工程项目数据到数据库
使用SQLAlchemy
"""
import sys
import xlrd
from datetime import datetime, date
import os
from decimal import Decimal
import asyncio
# 添加项目路径
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import declarative_base
from sqlalchemy import text
# 配置
EXCEL_FILE = '../docs/example.xls'
DATABASE_URL = "mysql+aiomysql://root:rootpassword@localhost:3306/project_manager?charset=utf8mb4"
# 创建引擎
engine = create_async_engine(DATABASE_URL, echo=False)
AsyncSessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
def parse_date_value(cell_value, workbook):
"""解析Excel日期值"""
if cell_value == '':
return None
# 检查是否是日期类型
try:
date_tuple = xlrd.xldate_as_tuple(cell_value, workbook.datemode)
if date_tuple[0] < 1900:
return None
return f'{date_tuple[0]}-{date_tuple[1]:02d}-{date_tuple[2]:02d}'
except:
pass
# 检查特殊值
if cell_value in ['未到期', '可退质保金', '未开工']:
return None
# 尝试解析字符串日期
try:
date_obj = datetime.strptime(str(cell_value), '%Y-%m-%d')
return date_obj.strftime('%Y-%m-%d')
except:
pass
return None
def parse_number_value(cell_value):
"""解析数字值"""
if cell_value == '':
return None
try:
return float(cell_value)
except:
return None
def parse_int_value(cell_value):
"""解析整数值"""
num = parse_number_value(cell_value)
if num is not None:
return int(num)
return None
def parse_enum_value(cell_value, allowed_values):
"""解析枚举值"""
if cell_value == '':
return None
if cell_value in allowed_values:
return cell_value
return None
def clean_text_value(cell_value):
"""清理文本值"""
if cell_value == '':
return None
return str(cell_value).strip()
def parse_decimal_value(cell_value):
"""解析Decimal值"""
num = parse_number_value(cell_value)
if num is not None:
return Decimal(str(num))
return None
async def import_projects_from_sheet(sheet, sheet_name, admin_id):
"""从工作表导入项目数据"""
print(f'\n正在导入工作表: {sheet_name}')
print(f'总行数: {sheet.nrows}')
async with AsyncSessionLocal() as session:
success_count = 0
skip_count = 0
error_count = 0
# 从第6行开始(前5行是表头)
for row_idx in range(5, sheet.nrows):
try:
# 读取数据
row_data = [sheet.cell_value(row_idx, col_idx) for col_idx in range(sheet.ncols)]
# 检查是否是合计行或空行(检查第1列,索引0)
if len(row_data) == 0 or row_data[0] == '' or str(row_data[0]).strip() == '':
skip_count += 1
continue
# 准备SQL插入语句
sql = """
INSERT INTO projects (
project_no, power_contract_no, name, subitem_count, subitem_code,
total_investment, contract_amount, warranty_ratio, settlement_amount,
total_cost_estimated, voltage_level, engineering_type, owner_unit,
owner_contact, bidding_type, signing_date, start_date, planned_end_date,
actual_end_date, warranty_amount, warranty_expiry_date, actual_warranty_refund_date,
project_department, project_leader, payment_method, total_cost_control,
is_adjusted, labor_cost_control, labor_cost_planned, labor_cost_paid,
material_cost_control, material_cost_payable, material_cost_actual,
material_cost_paid, other_cost_control, other_cost_payable, other_cost_actual,
tax_amount, profit, actual_profit, cost_settlement_amount, cumulative_progress,
receivable_amount, invoice_amount, actual_receipt_amount, receipt_completion_rate,
payable_amount, actual_payment_amount, unpaid_amount, payment_completion_rate,
labor_debt_amount, settlement_cost_amount, settlement_labor_cost,
settlement_material_cost, settlement_other_cost, due_settlement_count,
unsettlement_count, problems, suggestions, remarks, status, created_by
) VALUES (
:project_no, :power_contract_no, :name, :subitem_count, :subitem_code,
:total_investment, :contract_amount, :warranty_ratio, :settlement_amount,
:total_cost_estimated, :voltage_level, :engineering_type, :owner_unit,
:owner_contact, :bidding_type, :signing_date, :start_date, :planned_end_date,
:actual_end_date, :warranty_amount, :warranty_expiry_date, :actual_warranty_refund_date,
:project_department, :project_leader, :payment_method, :total_cost_control,
:is_adjusted, :labor_cost_control, :labor_cost_planned, :labor_cost_paid,
:material_cost_control, :material_cost_payable, :material_cost_actual,
:material_cost_paid, :other_cost_control, :other_cost_payable, :other_cost_actual,
:tax_amount, :profit, :actual_profit, :cost_settlement_amount, :cumulative_progress,
:receivable_amount, :invoice_amount, :actual_receipt_amount, :receipt_completion_rate,
:payable_amount, :actual_payment_amount, :unpaid_amount, :payment_completion_rate,
:labor_debt_amount, :settlement_cost_amount, :settlement_labor_cost,
:settlement_material_cost, :settlement_other_cost, :due_settlement_count,
:unsettlement_count, :problems, :suggestions, :remarks, :status, :created_by
)
"""
# 准备参数(使用安全的索引访问)
def safe_get(idx, default=None):
return row_data[idx] if idx < len(row_data) else default
params = {
'project_no': clean_text_value(safe_get(1)),
'power_contract_no': clean_text_value(safe_get(2)),
'name': clean_text_value(safe_get(3)),
'subitem_count': parse_int_value(safe_get(4)),
'subitem_code': clean_text_value(safe_get(5)),
'total_investment': parse_decimal_value(safe_get(6)),
'contract_amount': parse_decimal_value(safe_get(7)),
'warranty_ratio': parse_decimal_value(safe_get(8)),
'settlement_amount': parse_decimal_value(safe_get(9)),
'total_cost_estimated': parse_decimal_value(safe_get(10)),
'voltage_level': clean_text_value(safe_get(11)),
'engineering_type': clean_text_value(safe_get(12)),
'owner_unit': clean_text_value(safe_get(13)),
'owner_contact': clean_text_value(safe_get(14)),
'bidding_type': clean_text_value(safe_get(15)),
'signing_date': parse_date_value(safe_get(16), sheet.book),
'start_date': parse_date_value(safe_get(17), sheet.book),
'planned_end_date': parse_date_value(safe_get(18), sheet.book),
'actual_end_date': parse_date_value(safe_get(19), sheet.book),
'warranty_amount': parse_decimal_value(safe_get(20)),
'warranty_expiry_date': parse_date_value(safe_get(21), sheet.book),
'actual_warranty_refund_date': parse_date_value(safe_get(22), sheet.book),
'project_department': clean_text_value(safe_get(23)),
'project_leader': clean_text_value(safe_get(24)),
'payment_method': clean_text_value(safe_get(25)),
'total_cost_control': parse_decimal_value(safe_get(26)),
'is_adjusted': parse_enum_value(safe_get(27), ['', '']),
'labor_cost_control': parse_decimal_value(safe_get(28)),
'labor_cost_planned': parse_decimal_value(safe_get(29)),
'labor_cost_paid': parse_decimal_value(safe_get(30)),
'material_cost_control': parse_decimal_value(safe_get(31)),
'material_cost_payable': parse_decimal_value(safe_get(32)),
'material_cost_actual': parse_decimal_value(safe_get(33)),
'material_cost_paid': parse_decimal_value(safe_get(34)),
'other_cost_control': parse_decimal_value(safe_get(35)),
'other_cost_payable': parse_decimal_value(safe_get(36)),
'other_cost_actual': parse_decimal_value(safe_get(37)),
'tax_amount': parse_decimal_value(safe_get(38)),
'profit': parse_decimal_value(safe_get(39)),
'actual_profit': parse_decimal_value(safe_get(40)),
'cost_settlement_amount': parse_decimal_value(safe_get(41)),
'cumulative_progress': parse_decimal_value(safe_get(42)),
'receivable_amount': parse_decimal_value(safe_get(43)),
'invoice_amount': parse_decimal_value(safe_get(44)),
'actual_receipt_amount': parse_decimal_value(safe_get(45)),
'receipt_completion_rate': parse_decimal_value(safe_get(46)),
'payable_amount': parse_decimal_value(safe_get(47)),
'actual_payment_amount': parse_decimal_value(safe_get(48)),
'unpaid_amount': parse_decimal_value(safe_get(49)),
'payment_completion_rate': parse_decimal_value(safe_get(50)),
'labor_debt_amount': parse_decimal_value(safe_get(51)),
'settlement_cost_amount': parse_decimal_value(safe_get(52)),
'settlement_labor_cost': parse_decimal_value(safe_get(53)),
'settlement_material_cost': parse_decimal_value(safe_get(54)),
'settlement_other_cost': parse_decimal_value(safe_get(55)),
'due_settlement_count': parse_int_value(safe_get(56)),
'unsettlement_count': parse_int_value(safe_get(57)),
'problems': clean_text_value(safe_get(58)),
'suggestions': clean_text_value(safe_get(59)),
'remarks': clean_text_value(safe_get(60)),
'status': '新建',
'created_by': admin_id
}
# 跳过没有项目名称的行
if not params['name']:
skip_count += 1
continue
await session.execute(text(sql), params)
success_count += 1
except Exception as e:
print(f'{row_idx+1}行导入错误: {e}', file=sys.stderr)
error_count += 1
continue
await session.commit()
print(f'导入完成: 成功 {success_count} 条, 跳过 {skip_count} 条, 错误 {error_count}')
return True
async def main():
"""主函数"""
print('='*60)
print('工程项目数据导入脚本')
print('='*60)
# 检查Excel文件是否存在
if not os.path.exists(EXCEL_FILE):
print(f'错误: Excel文件不存在: {EXCEL_FILE}', file=sys.stderr)
sys.exit(1)
# 打开Excel文件
try:
workbook = xlrd.open_workbook(EXCEL_FILE)
print(f'已打开Excel文件: {EXCEL_FILE}')
print(f'工作表数量: {len(workbook.sheet_names())}')
except Exception as e:
print(f'错误: 无法打开Excel文件: {e}', file=sys.stderr)
sys.exit(1)
# 获取管理员用户ID
async with AsyncSessionLocal() as session:
result = await session.execute(text("SELECT id FROM users WHERE username='admin' LIMIT 1"))
row = result.fetchone()
if not row:
print('错误: 未找到管理员用户', file=sys.stderr)
sys.exit(1)
admin_id = row[0]
print(f'管理员用户ID: {admin_id}')
try:
# 导入主要工作表
main_sheets = [
'1-1基建',
'1-2业扩',
'1-3客户',
'1-4营销',
'2检修、技改、应急抢修项目'
]
for sheet_name in main_sheets:
if sheet_name in workbook.sheet_names():
sheet = workbook.sheet_by_name(sheet_name)
await import_projects_from_sheet(sheet, sheet_name, admin_id)
else:
print(f'警告: 工作表不存在: {sheet_name}')
# 显示统计信息
async with AsyncSessionLocal() as session:
result = await session.execute(text("SELECT COUNT(*) FROM projects"))
total_count = result.scalar()
print(f'\n数据库中共有 {total_count} 个项目')
result = await session.execute(text("SELECT engineering_type, COUNT(*) as count FROM projects GROUP BY engineering_type"))
print('\n按工程类别统计:')
for row in result.fetchall():
print(f' {row[0]}: {row[1]}')
print('\n' + '='*60)
print('数据导入完成!')
print('='*60)
except Exception as e:
print(f'错误: {e}', file=sys.stderr)
import traceback
traceback.print_exc()
sys.exit(1)
finally:
await engine.dispose()
if __name__ == '__main__':
asyncio.run(main())