feat(time-tracking): 添加个人工时记录系统设计文档

- 完成系统架构和数据模型设计,包括项目、工时记录、休息日和周期表模型
- 设计项目管理模块,支持传统项目与PSI项目管理及批量导入功能
- 规划工时记录模块,含日期、事件描述、项目选择及工时计算规则
- 定义休息日分类,支持周末、国定节假日、个人假期及调休工时管理
- 制定统计分析模块设计,支持按Cut-Off周期的周统计与项目工时分布
- 设计周期管理模块,提供周期设置及预设模板功能
- 制定用户界面布局及各页面表单、样式设计方案
- 规划RESTful API端点,涵盖项目、工时记录、休息日、周期及统计数据操作
- 设计数据流示意,阐明操作流程及前后端交互逻辑
- 制定数据存储方案,包括SQLite数据库配置及备份导出机制
This commit is contained in:
2025-09-04 15:19:35 +08:00
parent cda1360ce4
commit ef9432f6da
11 changed files with 1163 additions and 0 deletions

245
backend/api/projects.py Normal file
View File

@@ -0,0 +1,245 @@
from flask import Blueprint, request, jsonify
from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
from backend.models.models import Project, ProjectType
from backend.models.utils import *
import csv
import io
projects_bp = Blueprint('projects', __name__)
def get_db_session():
"""获取数据库会话"""
engine = create_engine('sqlite:///data/timetrack.db')
Session = sessionmaker(bind=engine)
return Session()
@projects_bp.route('/api/projects', methods=['GET'])
def get_projects():
"""获取所有项目列表"""
try:
session = get_db_session()
projects = session.query(Project).filter_by(is_active=True).all()
result = []
for project in projects:
project_dict = project.to_dict()
# 为前端显示格式化项目信息
if project.project_type == ProjectType.TRADITIONAL:
project_dict['display_name'] = f"{project.project_name} ({project.customer_name}-{project.project_code})"
else: # PSI项目
project_dict['display_name'] = f"{project.project_name} ({project.customer_name}-{project.contract_number})"
result.append(project_dict)
session.close()
return jsonify({'success': True, 'data': result})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@projects_bp.route('/api/projects', methods=['POST'])
def create_project():
"""创建新项目"""
try:
data = request.json
session = get_db_session()
# 验证必填字段
if not data.get('project_name') or not data.get('customer_name') or not data.get('project_type'):
return jsonify({'success': False, 'error': '项目名称、客户名和项目类型为必填项'}), 400
# 根据项目类型设置字段
if data['project_type'] == 'traditional':
if not data.get('project_code'):
return jsonify({'success': False, 'error': '传统项目需要填写项目代码'}), 400
project_code = data['project_code']
contract_number = None
else: # PSI项目
if not data.get('contract_number'):
return jsonify({'success': False, 'error': 'PSI项目需要填写合同号'}), 400
project_code = 'PSI-PROJ' # PSI项目统一代码
contract_number = data['contract_number']
# 检查唯一性约束
existing_project = None
if data['project_type'] == 'traditional':
# 传统项目:客户名+项目代码唯一
existing_project = session.query(Project).filter_by(
customer_name=data['customer_name'],
project_code=project_code,
project_type=ProjectType.TRADITIONAL
).first()
else:
# PSI项目客户名+合同号唯一
existing_project = session.query(Project).filter_by(
customer_name=data['customer_name'],
contract_number=contract_number,
project_type=ProjectType.PSI
).first()
if existing_project:
session.close()
return jsonify({'success': False, 'error': '项目已存在'}), 400
# 创建新项目
project = Project(
project_name=data['project_name'],
project_type=ProjectType(data['project_type']),
project_code=project_code,
customer_name=data['customer_name'],
contract_number=contract_number,
description=data.get('description', '')
)
session.add(project)
session.commit()
result = project.to_dict()
session.close()
return jsonify({'success': True, 'data': result})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@projects_bp.route('/api/projects/<int:project_id>', methods=['PUT'])
def update_project(project_id):
"""更新项目信息"""
try:
data = request.json
session = get_db_session()
project = session.query(Project).get(project_id)
if not project:
session.close()
return jsonify({'success': False, 'error': '项目不存在'}), 404
# 更新字段
if 'project_name' in data:
project.project_name = data['project_name']
if 'description' in data:
project.description = data['description']
session.commit()
result = project.to_dict()
session.close()
return jsonify({'success': True, 'data': result})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@projects_bp.route('/api/projects/<int:project_id>', methods=['DELETE'])
def delete_project(project_id):
"""删除项目(软删除)"""
try:
session = get_db_session()
project = session.query(Project).get(project_id)
if not project:
session.close()
return jsonify({'success': False, 'error': '项目不存在'}), 404
project.is_active = False
session.commit()
session.close()
return jsonify({'success': True, 'message': '项目已删除'})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@projects_bp.route('/api/projects/import', methods=['POST'])
def import_projects():
"""批量导入项目"""
try:
if 'file' not in request.files:
return jsonify({'success': False, 'error': '请选择CSV文件'}), 400
file = request.files['file']
if file.filename == '' or not file.filename.endswith('.csv'):
return jsonify({'success': False, 'error': '请选择有效的CSV文件'}), 400
session = get_db_session()
# 读取CSV文件
stream = io.StringIO(file.stream.read().decode("utf-8"))
csv_reader = csv.DictReader(stream)
created_count = 0
errors = []
for row_num, row in enumerate(csv_reader, start=2):
try:
# 验证必填字段
if not row.get('项目名称') or not row.get('客户名') or not row.get('项目类型'):
errors.append(f"{row_num}行:项目名称、客户名和项目类型为必填项")
continue
project_type = row['项目类型'].lower()
if project_type not in ['traditional', 'psi']:
errors.append(f"{row_num}行:项目类型只能是 traditional 或 psi")
continue
# 根据项目类型设置字段
if project_type == 'traditional':
if not row.get('项目代码'):
errors.append(f"{row_num}行:传统项目需要填写项目代码")
continue
project_code = row['项目代码']
contract_number = None
else: # PSI项目
if not row.get('合同号'):
errors.append(f"{row_num}PSI项目需要填写合同号")
continue
project_code = 'PSI-PROJ'
contract_number = row['合同号']
# 检查重复
existing_project = None
if project_type == 'traditional':
existing_project = session.query(Project).filter_by(
customer_name=row['客户名'],
project_code=project_code,
project_type=ProjectType.TRADITIONAL
).first()
else:
existing_project = session.query(Project).filter_by(
customer_name=row['客户名'],
contract_number=contract_number,
project_type=ProjectType.PSI
).first()
if existing_project:
errors.append(f"{row_num}行:项目已存在")
continue
# 创建项目
project = Project(
project_name=row['项目名称'],
project_type=ProjectType(project_type),
project_code=project_code,
customer_name=row['客户名'],
contract_number=contract_number,
description=row.get('描述', '')
)
session.add(project)
created_count += 1
except Exception as e:
errors.append(f"{row_num}行:{str(e)}")
session.commit()
session.close()
return jsonify({
'success': True,
'message': f'成功导入{created_count}个项目',
'created_count': created_count,
'errors': errors
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500