diff --git a/backend/app/api/project_management.py b/backend/app/api/project_management.py index 8711e85d..8b4e7845 100644 --- a/backend/app/api/project_management.py +++ b/backend/app/api/project_management.py @@ -45,6 +45,7 @@ class TeamBody(Body): class ProjectBody(Body): projectName: str = Field(min_length=1, max_length=200) + projectNo: str = Field(min_length=1, max_length=64) teamId: str leaderUserId: str bizTypes: List[str] diff --git a/backend/app/services/project_management.py b/backend/app/services/project_management.py index 40b87a3c..a1747d3a 100644 --- a/backend/app/services/project_management.py +++ b/backend/app/services/project_management.py @@ -333,8 +333,17 @@ def save_entity(db, tenant, user, model, body, row_id=None): if status not in ('pending', 'active', 'completed', 'disabled'): raise HTTPException(400, '项目状态无效') row.project_name, row.team_id, row.biz_types, row.status = body[name_key].strip(), team.id, ','.join(dict.fromkeys(kinds)), status - if not row_id: - row.project_no = 'XM' + datetime.now().strftime('%Y%m%d') + generate_uuid().replace('-', '')[:16].upper() + project_no = str(body.get('projectNo') or '').strip() + if not project_no: + raise HTTPException(400, '项目编号不能为空') + if len(project_no) > 64: + raise HTTPException(400, '项目编号不能超过 64 个字符') + conflict = scoped(db, Project, tenant).filter(Project.project_no == project_no) + if row_id: + conflict = conflict.filter(Project.id != row.id) + if conflict.with_for_update().first(): + raise HTTPException(400, '项目编号已存在') + row.project_no = project_no try: row.start_date = date.fromisoformat(body['startDate']) if body.get('startDate') else None row.end_date = date.fromisoformat(body['endDate']) if body.get('endDate') else None diff --git a/backend/tests/test_project_management.py b/backend/tests/test_project_management.py index 0d848fd7..8add57bd 100644 --- a/backend/tests/test_project_management.py +++ b/backend/tests/test_project_management.py @@ -148,8 +148,8 @@ class ProjectManagementTest(unittest.TestCase): self.db.add(SysUser(id='u', tenant_no='t', user_no='u', nick_name='User')) self.db.commit() result = self.svc.save_entity(self.db, 't', self.user, self.Project, - dict(projectName='New', leaderUserId='u', teamId='g', bizTypes=['basic_experiment'], startDate='', endDate='')) - self.assertTrue(result['projectNo'].startswith('XM')) + dict(projectName='New', projectNo='CUSTOM-001', leaderUserId='u', teamId='g', bizTypes=['basic_experiment'], startDate='', endDate='')) + self.assertEqual(result['projectNo'], 'CUSTOM-001') self.assertIsNone(result['startDate']) def test_conversion_table_reverse_graph_and_audit(self): diff --git a/web-admin/src/utils/project-management.ts b/web-admin/src/utils/project-management.ts index 9ca2de21..cd393bf6 100644 --- a/web-admin/src/utils/project-management.ts +++ b/web-admin/src/utils/project-management.ts @@ -22,13 +22,14 @@ export function assignmentSnapshot(records: ProjectRecord[]) { })) } -/** Write bodies intentionally exclude response-only labels, timestamps and generated numbers. */ +/** Write bodies intentionally exclude response-only labels and timestamps. */ export function managementPayload(kind: 'teams' | 'projects', form: ProjectRecord): ProjectRecord { const fields = kind === 'teams' ? ['teamName', 'leaderUserId', 'enabled', 'remark'] : [ 'projectName', + 'projectNo', 'teamId', 'leaderUserId', 'bizTypes', diff --git a/web-admin/src/views/project-management/ManagementPage.vue b/web-admin/src/views/project-management/ManagementPage.vue index 7a0ac273..4151ce27 100644 --- a/web-admin/src/views/project-management/ManagementPage.vue +++ b/web-admin/src/views/project-management/ManagementPage.vue @@ -149,6 +149,13 @@ v-else v-model="form.projectName" /> +