初步开发完成
This commit is contained in:
Binary file not shown.
+4
-11
@@ -1,9 +1,3 @@
|
||||
# 应用配置
|
||||
APP_NAME=海洋项目管理系统
|
||||
APP_VERSION=1.0.0
|
||||
DEBUG=True
|
||||
USE_SQLITE=true
|
||||
|
||||
# 数据库配置
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
@@ -12,10 +6,9 @@ DB_PASSWORD=rootpassword
|
||||
DB_NAME=project_manager
|
||||
DB_CHARSET=utf8mb4
|
||||
|
||||
# 安全配置
|
||||
# 应用配置
|
||||
SECRET_KEY=your-secret-key-change-this-in-production
|
||||
ALGORITHM=HS256
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=1440
|
||||
DEBUG=True
|
||||
|
||||
# CORS配置
|
||||
CORS_ORIGINS=["http://localhost:3000","http://127.0.0.1:3000"]
|
||||
# 其他配置
|
||||
CORS_ORIGINS=["http://localhost:5173","http://localhost:3000","http://127.0.0.1:5173","http://127.0.0.1:3000"]
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,43 @@
|
||||
/home/xsl/code/ocean_project_manager/backend/main.py:78: DeprecationWarning:
|
||||
on_event is deprecated, use lifespan event handlers instead.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/).
|
||||
|
||||
@app.on_event("startup")
|
||||
INFO: Will watch for changes in these directories: ['/home/xsl/code/ocean_project_manager/backend']
|
||||
INFO: Uvicorn running on http://0.0.0.0:8188 (Press CTRL+C to quit)
|
||||
INFO: Started reloader process [6520] using WatchFiles
|
||||
INFO: Started server process [6522]
|
||||
INFO: Waiting for application startup.
|
||||
INFO: Application startup complete.
|
||||
INFO: 127.0.0.1:38698 - "POST /api/v1/auth/login HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:57886 - "POST /api/v1/auth/login HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:57900 - "POST /api/v1/auth/login HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:43792 - "GET /health HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:47906 - "GET / HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:47906 - "GET /favicon.ico HTTP/1.1" 404 Not Found
|
||||
INFO: 127.0.0.1:57272 - "GET / HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:57288 - "GET /favicon.ico HTTP/1.1" 404 Not Found
|
||||
INFO: 127.0.0.1:60220 - "GET / HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:60230 - "GET /favicon.ico HTTP/1.1" 404 Not Found
|
||||
INFO: 127.0.0.1:49220 - "GET / HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:49232 - "GET /favicon.ico HTTP/1.1" 404 Not Found
|
||||
INFO: 127.0.0.1:43984 - "POST /api/v1/auth/login HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:43998 - "POST /api/v1/auth/login HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:35788 - "GET / HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:35788 - "GET /favicon.ico HTTP/1.1" 404 Not Found
|
||||
INFO: 127.0.0.1:56510 - "POST /api/v1/auth/login HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:59614 - "POST /api/v1/auth/login HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:39442 - "GET /api/v1/projects?page=1&page_size=10 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:39456 - "GET /api/v1/projects?page=1&page_size=10 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:39462 - "GET /api/v1/projects?page=1&page_size=10 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:39476 - "GET /api/v1/projects?page=1&page_size=10 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:39482 - "GET /api/v1/projects/statistics?group_by=engineering_type&time_field=signing_date&group_by_time=month HTTP/1.1" 422 Unprocessable Entity
|
||||
INFO: 127.0.0.1:39486 - "GET /api/v1/projects/statistics/group?group_by=engineering_type&time_field=signing_date&group_by_time=month HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:39502 - "GET /api/v1/projects/statistics/timeline?group_by=engineering_type&time_field=signing_date&group_by_time=month HTTP/1.1" 422 Unprocessable Entity
|
||||
INFO: 127.0.0.1:39506 - "GET /api/v1/projects/statistics/group?group_by=engineering_type&time_field=signing_date&group_by_time=month HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:39522 - "GET /api/v1/projects/statistics?group_by=engineering_type&time_field=signing_date&group_by_time=month HTTP/1.1" 422 Unprocessable Entity
|
||||
INFO: 127.0.0.1:39530 - "GET /api/v1/projects/statistics/timeline?group_by=engineering_type&time_field=signing_date&group_by_time=month HTTP/1.1" 422 Unprocessable Entity
|
||||
INFO: 127.0.0.1:33492 - "GET /api/v1/projects?page=1&page_size=10 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:33496 - "GET /api/v1/projects?page=1&page_size=10 HTTP/1.1" 200 OK
|
||||
Binary file not shown.
@@ -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())
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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())
|
||||
+1
-2
@@ -77,8 +77,7 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
# await init_db() # 注释掉,MySQL需要配置后再启用
|
||||
pass
|
||||
await init_db() # 启用数据库初始化
|
||||
|
||||
|
||||
@app.get("/")
|
||||
|
||||
Binary file not shown.
@@ -17,7 +17,6 @@ class User(Base):
|
||||
email = Column(String(100), unique=True, index=True, comment="邮箱")
|
||||
phone = Column(String(20), comment="电话")
|
||||
is_active = Column(Boolean, default=True, index=True, comment="是否激活")
|
||||
created_by = Column(Integer, ForeignKey("users.id", ondelete="RESTRICT"), comment="创建人ID")
|
||||
created_at = Column(
|
||||
DateTime,
|
||||
server_default=func.now(),
|
||||
@@ -30,6 +29,4 @@ class User(Base):
|
||||
comment="更新时间",
|
||||
)
|
||||
|
||||
created_users = relationship("User", back_populates="creator")
|
||||
creator = relationship("User", remote_side="User.id", back_populates="created_users")
|
||||
created_projects = relationship("Project", back_populates="creator")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -141,13 +141,13 @@ async def get_statistics(
|
||||
|
||||
total_count = len(projects)
|
||||
total_contract_amount = sum(
|
||||
p.contract_amount or Decimal(0) for p in projects
|
||||
Decimal(str(p.contract_amount)) if p.contract_amount else Decimal(0) for p in projects
|
||||
)
|
||||
total_receipt_amount = sum(
|
||||
p.actual_receipt_amount or Decimal(0) for p in projects
|
||||
Decimal(str(p.actual_receipt_amount)) if p.actual_receipt_amount else Decimal(0) for p in projects
|
||||
)
|
||||
total_payment_amount = sum(
|
||||
p.actual_payment_amount or Decimal(0) for p in projects
|
||||
Decimal(str(p.actual_payment_amount)) if p.actual_payment_amount else Decimal(0) for p in projects
|
||||
)
|
||||
|
||||
avg_receipt_rate = 0
|
||||
@@ -157,7 +157,7 @@ async def get_statistics(
|
||||
)
|
||||
|
||||
avg_payment_rate = 0
|
||||
total_payable = sum(p.payable_amount or Decimal(0) for p in projects)
|
||||
total_payable = sum(Decimal(str(p.payable_amount)) if p.payable_amount else Decimal(0) for p in projects)
|
||||
if total_payable > 0:
|
||||
avg_payment_rate = float(
|
||||
(total_payment_amount / total_payable) * 100
|
||||
@@ -166,7 +166,7 @@ async def get_statistics(
|
||||
avg_progress = 0
|
||||
if total_count > 0:
|
||||
avg_progress = sum(
|
||||
p.cumulative_progress or Decimal(0) for p in projects
|
||||
Decimal(str(p.cumulative_progress)) if p.cumulative_progress else Decimal(0) for p in projects
|
||||
) / total_count
|
||||
|
||||
return {
|
||||
@@ -207,7 +207,7 @@ async def get_project(
|
||||
}
|
||||
|
||||
|
||||
@router.post("", response_model=dict)
|
||||
@router.post("", response_model=dict, status_code=status.HTTP_201_CREATED)
|
||||
async def create_project(
|
||||
project_data: ProjectCreate,
|
||||
current_user: User = Depends(require_market_or_admin),
|
||||
@@ -217,12 +217,10 @@ async def create_project(
|
||||
select(Project).where(Project.project_no == project_data.project_no)
|
||||
)
|
||||
if result.scalar_one_or_none():
|
||||
return {
|
||||
"success": False,
|
||||
"message": "项目编号已存在",
|
||||
"data": None,
|
||||
"error_code": "2002",
|
||||
}
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="项目编号已存在"
|
||||
)
|
||||
|
||||
project = Project(
|
||||
**project_data.model_dump(),
|
||||
@@ -251,20 +249,16 @@ async def update_project(
|
||||
project = result.scalar_one_or_none()
|
||||
|
||||
if not project:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "项目不存在",
|
||||
"data": None,
|
||||
"error_code": "2001",
|
||||
}
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="项目不存在"
|
||||
)
|
||||
|
||||
if current_user.role == "market" and project.created_by != current_user.id:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "无权修改此项目",
|
||||
"data": None,
|
||||
"error_code": "3001",
|
||||
}
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="无权修改此项目"
|
||||
)
|
||||
|
||||
update_data = project_data.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
@@ -291,20 +285,16 @@ async def delete_project(
|
||||
project = result.scalar_one_or_none()
|
||||
|
||||
if not project:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "项目不存在",
|
||||
"data": None,
|
||||
"error_code": "2001",
|
||||
}
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="项目不存在"
|
||||
)
|
||||
|
||||
if current_user.role == "market" and project.created_by != current_user.id:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "无权删除此项目",
|
||||
"data": None,
|
||||
"error_code": "3001",
|
||||
}
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="无权删除此项目"
|
||||
)
|
||||
|
||||
await db.delete(project)
|
||||
await db.commit()
|
||||
@@ -424,9 +414,9 @@ async def get_group_statistics(
|
||||
"total_payment_amount": Decimal(0),
|
||||
}
|
||||
groups[group_value]["count"] += 1
|
||||
groups[group_value]["total_contract_amount"] += p.contract_amount or Decimal(0)
|
||||
groups[group_value]["total_receipt_amount"] += p.actual_receipt_amount or Decimal(0)
|
||||
groups[group_value]["total_payment_amount"] += p.actual_payment_amount or Decimal(0)
|
||||
groups[group_value]["total_contract_amount"] += Decimal(str(p.contract_amount)) if p.contract_amount else Decimal(0)
|
||||
groups[group_value]["total_receipt_amount"] += Decimal(str(p.actual_receipt_amount)) if p.actual_receipt_amount else Decimal(0)
|
||||
groups[group_value]["total_payment_amount"] += Decimal(str(p.actual_payment_amount)) if p.actual_payment_amount else Decimal(0)
|
||||
|
||||
result_list = []
|
||||
for key, value in groups.items():
|
||||
@@ -490,7 +480,7 @@ async def get_timeline_statistics(
|
||||
if key not in groups:
|
||||
groups[key] = {"count": 0, "total_contract_amount": Decimal(0)}
|
||||
groups[key]["count"] += 1
|
||||
groups[key]["total_contract_amount"] += p.contract_amount or Decimal(0)
|
||||
groups[key]["total_contract_amount"] += Decimal(str(p.contract_amount)) if p.contract_amount else Decimal(0)
|
||||
|
||||
result_list = []
|
||||
for key in sorted(groups.keys()):
|
||||
|
||||
+31
-43
@@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, or_
|
||||
from config.database import get_db
|
||||
@@ -63,7 +63,7 @@ async def get_users(
|
||||
}
|
||||
|
||||
|
||||
@router.post("", response_model=dict)
|
||||
@router.post("", response_model=dict, status_code=status.HTTP_201_CREATED)
|
||||
async def create_user(
|
||||
user_data: UserCreate,
|
||||
current_user: User = Depends(require_admin),
|
||||
@@ -73,24 +73,20 @@ async def create_user(
|
||||
select(User).where(User.username == user_data.username)
|
||||
)
|
||||
if result.scalar_one_or_none():
|
||||
return {
|
||||
"success": False,
|
||||
"message": "用户名已存在",
|
||||
"data": None,
|
||||
"error_code": "2002",
|
||||
}
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="用户名已存在"
|
||||
)
|
||||
|
||||
if user_data.email:
|
||||
result = await db.execute(
|
||||
select(User).where(User.email == user_data.email)
|
||||
)
|
||||
if result.scalar_one_or_none():
|
||||
return {
|
||||
"success": False,
|
||||
"message": "邮箱已存在",
|
||||
"data": None,
|
||||
"error_code": "2002",
|
||||
}
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="邮箱已存在"
|
||||
)
|
||||
|
||||
user = User(
|
||||
username=user_data.username,
|
||||
@@ -121,14 +117,12 @@ async def get_user(
|
||||
):
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
|
||||
if not user:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "用户不存在",
|
||||
"data": None,
|
||||
"error_code": "2001",
|
||||
}
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="用户不存在"
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
@@ -147,19 +141,17 @@ async def update_user(
|
||||
):
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
|
||||
if not user:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "用户不存在",
|
||||
"data": None,
|
||||
"error_code": "2001",
|
||||
}
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="用户不存在"
|
||||
)
|
||||
|
||||
update_data = user_data.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(user, field, value)
|
||||
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
|
||||
@@ -179,14 +171,12 @@ async def delete_user(
|
||||
):
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
|
||||
if not user:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "用户不存在",
|
||||
"data": None,
|
||||
"error_code": "2001",
|
||||
}
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="用户不存在"
|
||||
)
|
||||
|
||||
await db.delete(user)
|
||||
await db.commit()
|
||||
@@ -208,14 +198,12 @@ async def reset_password(
|
||||
):
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
|
||||
if not user:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "用户不存在",
|
||||
"data": None,
|
||||
"error_code": "2001",
|
||||
}
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="用户不存在"
|
||||
)
|
||||
|
||||
user.password_hash = hash_password(password_data.new_password)
|
||||
await db.commit()
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -173,7 +173,7 @@ async def test_create_project_admin_success(
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["message"] == "项目创建成功"
|
||||
@@ -198,7 +198,7 @@ async def test_create_project_market_success(
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
|
||||
@@ -260,8 +260,8 @@ async def test_create_project_duplicate_project_no(
|
||||
assert response.status_code == 409
|
||||
data = response.json()
|
||||
assert data["success"] is False
|
||||
assert data["error_code"] == "2002"
|
||||
|
||||
assert data["error_code"] is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
@@ -280,7 +280,7 @@ async def test_create_project_missing_required_fields(
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.status_code == 422
|
||||
data = response.json()
|
||||
assert data["success"] is False
|
||||
assert data["error_code"] == "1001"
|
||||
@@ -398,7 +398,7 @@ async def test_update_project_not_found(
|
||||
assert response.status_code == 404
|
||||
data = response.json()
|
||||
assert data["success"] is False
|
||||
assert data["error_code"] == "2001"
|
||||
assert data["error_code"] is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -509,8 +509,8 @@ async def test_delete_project_not_found(
|
||||
assert response.status_code == 404
|
||||
data = response.json()
|
||||
assert data["success"] is False
|
||||
assert data["error_code"] == "2001"
|
||||
|
||||
assert data["error_code"] is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
|
||||
@@ -22,7 +22,7 @@ async def test_create_user_admin_success(
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["message"] == "用户创建成功"
|
||||
|
||||
Reference in New Issue
Block a user