初步开发完成

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
BIN
View File
Binary file not shown.
+4 -11
View File
@@ -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.
+43
View File
@@ -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.
+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())
+1 -2
View File
@@ -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.
-3
View File
@@ -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.
+30 -40
View File
@@ -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
View File
@@ -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()
View File
+6 -6
View File
@@ -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
+1 -1
View File
@@ -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"] == "用户创建成功"