package projectworkflow import ( "context" "errors" "fmt" "strings" "time" "github.com/jackc/pgx/v5" ) func (s *pgStore) CreateProject(ctx context.Context, p *Project) (*Project, error) { now := time.Now() p.CreatedAt = now p.UpdatedAt = now if p.Status == "" { p.Status = "active" } if p.Environment == "" { p.Environment = "dev" } if p.CurrentStage == "" { p.CurrentStage = "V0.1_triage" } if p.LifecycleStatus == "" { p.LifecycleStatus = "planning" } if p.WorkflowSelectionVersion <= 0 { p.WorkflowSelectionVersion = 1 } query := ` INSERT INTO projects ( code, name, customer_name, device_type, intended_use, target_market, product_domain, project_type_key, compliance_profile_key, governance_profile, team_mode, execution_mode, data_sensitivity, network_policy, budget_class, problem_statement, target_users, target_outcome, product_shape, current_stage, environment, status, creator_user_id, lifecycle_status, active_workflow_key, workflow_selection_version, workflow_plan_revision, client_key, created_at, updated_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, NULLIF(trim($28), ''), $29, $30 ) RETURNING id ` var clientKey any if strings.TrimSpace(p.ClientKey) != "" { clientKey = strings.TrimSpace(p.ClientKey) } else { clientKey = nil } err := s.pool.QueryRow( ctx, query, p.Code, p.Name, p.CustomerName, p.DeviceType, p.IntendedUse, p.TargetMarket, p.ProductDomain, p.ProjectTypeKey, p.ComplianceProfileKey, p.GovernanceProfile, p.TeamMode, p.ExecutionMode, p.DataSensitivity, p.NetworkPolicy, p.BudgetClass, p.ProblemStatement, p.TargetUsers, p.TargetOutcome, p.ProductShape, p.CurrentStage, p.Environment, p.Status, p.CreatorUserID, p.LifecycleStatus, p.ActiveWorkflowKey, p.WorkflowSelectionVersion, p.WorkflowPlanRevision, clientKey, p.CreatedAt, p.UpdatedAt, ).Scan(&p.ID) if err != nil { return nil, fmt.Errorf("insert project failed: %w", err) } return p, nil } func (s *pgStore) GetProjectByClientKey(ctx context.Context, creatorUserID int64, clientKey string) (*Project, error) { clientKey = strings.TrimSpace(clientKey) if creatorUserID <= 0 || clientKey == "" { return nil, ErrProjectNotFound } query := ` SELECT id, code, name, customer_name, device_type, intended_use, target_market, COALESCE(product_domain, ''), COALESCE(project_type_key, ''), COALESCE(compliance_profile_key, ''), COALESCE(governance_profile, ''), COALESCE(team_mode, ''), COALESCE(execution_mode, ''), COALESCE(data_sensitivity, ''), COALESCE(network_policy, ''), COALESCE(budget_class, ''), COALESCE(problem_statement, ''), COALESCE(target_users, ''), COALESCE(target_outcome, ''), COALESCE(product_shape, ''), current_stage, environment, status, COALESCE(creator_user_id, 1), COALESCE(lifecycle_status, 'planning'), COALESCE(active_workflow_key, ''), COALESCE(workflow_selection_version, 1), COALESCE(workflow_plan_revision, 0), active_workflow_changed_at, active_workflow_changed_by, created_at, updated_at, COALESCE(client_key, '') FROM projects WHERE creator_user_id = $1 AND client_key = $2 LIMIT 1 ` p := &Project{} err := s.pool.QueryRow(ctx, query, creatorUserID, clientKey).Scan( &p.ID, &p.Code, &p.Name, &p.CustomerName, &p.DeviceType, &p.IntendedUse, &p.TargetMarket, &p.ProductDomain, &p.ProjectTypeKey, &p.ComplianceProfileKey, &p.GovernanceProfile, &p.TeamMode, &p.ExecutionMode, &p.DataSensitivity, &p.NetworkPolicy, &p.BudgetClass, &p.ProblemStatement, &p.TargetUsers, &p.TargetOutcome, &p.ProductShape, &p.CurrentStage, &p.Environment, &p.Status, &p.CreatorUserID, &p.LifecycleStatus, &p.ActiveWorkflowKey, &p.WorkflowSelectionVersion, &p.WorkflowPlanRevision, &p.ActiveWorkflowChangedAt, &p.ActiveWorkflowChangedBy, &p.CreatedAt, &p.UpdatedAt, &p.ClientKey, ) if errors.Is(err, pgx.ErrNoRows) { return nil, ErrProjectNotFound } if err != nil { return nil, fmt.Errorf("get project by client_key failed: %w", err) } return p, nil } func (s *pgStore) FindProjectByCreatorAndName(ctx context.Context, creatorUserID int64, name string) (*Project, error) { name = strings.TrimSpace(name) if creatorUserID <= 0 || name == "" { return nil, ErrProjectNotFound } query := ` SELECT id, code, name, customer_name, device_type, intended_use, target_market, COALESCE(product_domain, ''), COALESCE(project_type_key, ''), COALESCE(compliance_profile_key, ''), COALESCE(governance_profile, ''), COALESCE(team_mode, ''), COALESCE(execution_mode, ''), COALESCE(data_sensitivity, ''), COALESCE(network_policy, ''), COALESCE(budget_class, ''), COALESCE(problem_statement, ''), COALESCE(target_users, ''), COALESCE(target_outcome, ''), COALESCE(product_shape, ''), current_stage, environment, status, COALESCE(creator_user_id, 1), COALESCE(lifecycle_status, 'planning'), COALESCE(active_workflow_key, ''), COALESCE(workflow_selection_version, 1), COALESCE(workflow_plan_revision, 0), active_workflow_changed_at, active_workflow_changed_by, created_at, updated_at, COALESCE(client_key, '') FROM projects WHERE creator_user_id = $1 AND lower(trim(name)) = lower(trim($2)) ORDER BY id ASC LIMIT 1 ` p := &Project{} err := s.pool.QueryRow(ctx, query, creatorUserID, name).Scan( &p.ID, &p.Code, &p.Name, &p.CustomerName, &p.DeviceType, &p.IntendedUse, &p.TargetMarket, &p.ProductDomain, &p.ProjectTypeKey, &p.ComplianceProfileKey, &p.GovernanceProfile, &p.TeamMode, &p.ExecutionMode, &p.DataSensitivity, &p.NetworkPolicy, &p.BudgetClass, &p.ProblemStatement, &p.TargetUsers, &p.TargetOutcome, &p.ProductShape, &p.CurrentStage, &p.Environment, &p.Status, &p.CreatorUserID, &p.LifecycleStatus, &p.ActiveWorkflowKey, &p.WorkflowSelectionVersion, &p.WorkflowPlanRevision, &p.ActiveWorkflowChangedAt, &p.ActiveWorkflowChangedBy, &p.CreatedAt, &p.UpdatedAt, &p.ClientKey, ) if errors.Is(err, pgx.ErrNoRows) { return nil, ErrProjectNotFound } if err != nil { return nil, fmt.Errorf("find project by creator+name failed: %w", err) } return p, nil } func (s *pgStore) FindProjectByName(ctx context.Context, name string) (*Project, error) { name = strings.TrimSpace(name) if name == "" { return nil, ErrProjectNotFound } query := ` SELECT id, code, name, customer_name, device_type, intended_use, target_market, COALESCE(product_domain, ''), COALESCE(project_type_key, ''), COALESCE(compliance_profile_key, ''), COALESCE(governance_profile, ''), COALESCE(team_mode, ''), COALESCE(execution_mode, ''), COALESCE(data_sensitivity, ''), COALESCE(network_policy, ''), COALESCE(budget_class, ''), COALESCE(problem_statement, ''), COALESCE(target_users, ''), COALESCE(target_outcome, ''), COALESCE(product_shape, ''), current_stage, environment, status, COALESCE(creator_user_id, 1), COALESCE(lifecycle_status, 'planning'), COALESCE(active_workflow_key, ''), COALESCE(workflow_selection_version, 1), COALESCE(workflow_plan_revision, 0), active_workflow_changed_at, active_workflow_changed_by, created_at, updated_at, COALESCE(client_key, '') FROM projects WHERE lower(trim(name)) = lower(trim($1)) AND COALESCE(status, 'active') <> 'demo_retired' ORDER BY id ASC LIMIT 1 ` p := &Project{} err := s.pool.QueryRow(ctx, query, name).Scan( &p.ID, &p.Code, &p.Name, &p.CustomerName, &p.DeviceType, &p.IntendedUse, &p.TargetMarket, &p.ProductDomain, &p.ProjectTypeKey, &p.ComplianceProfileKey, &p.GovernanceProfile, &p.TeamMode, &p.ExecutionMode, &p.DataSensitivity, &p.NetworkPolicy, &p.BudgetClass, &p.ProblemStatement, &p.TargetUsers, &p.TargetOutcome, &p.ProductShape, &p.CurrentStage, &p.Environment, &p.Status, &p.CreatorUserID, &p.LifecycleStatus, &p.ActiveWorkflowKey, &p.WorkflowSelectionVersion, &p.WorkflowPlanRevision, &p.ActiveWorkflowChangedAt, &p.ActiveWorkflowChangedBy, &p.CreatedAt, &p.UpdatedAt, &p.ClientKey, ) if errors.Is(err, pgx.ErrNoRows) { return nil, ErrProjectNotFound } if err != nil { return nil, fmt.Errorf("find project by name failed: %w", err) } return p, nil } func (s *pgStore) GetProjectByID(ctx context.Context, id int64) (*Project, error) { query := ` SELECT id, code, name, customer_name, device_type, intended_use, target_market, COALESCE(product_domain, ''), COALESCE(project_type_key, ''), COALESCE(compliance_profile_key, ''), COALESCE(governance_profile, ''), COALESCE(team_mode, ''), COALESCE(execution_mode, ''), COALESCE(data_sensitivity, ''), COALESCE(network_policy, ''), COALESCE(budget_class, ''), COALESCE(problem_statement, ''), COALESCE(target_users, ''), COALESCE(target_outcome, ''), COALESCE(product_shape, ''), current_stage, environment, status, COALESCE(creator_user_id, 1), COALESCE(lifecycle_status, 'planning'), COALESCE(active_workflow_key, ''), COALESCE(workflow_selection_version, 1), COALESCE(workflow_plan_revision, 0), active_workflow_changed_at, active_workflow_changed_by, created_at, updated_at FROM projects WHERE id = $1 ` p := &Project{} err := s.pool.QueryRow(ctx, query, id).Scan( &p.ID, &p.Code, &p.Name, &p.CustomerName, &p.DeviceType, &p.IntendedUse, &p.TargetMarket, &p.ProductDomain, &p.ProjectTypeKey, &p.ComplianceProfileKey, &p.GovernanceProfile, &p.TeamMode, &p.ExecutionMode, &p.DataSensitivity, &p.NetworkPolicy, &p.BudgetClass, &p.ProblemStatement, &p.TargetUsers, &p.TargetOutcome, &p.ProductShape, &p.CurrentStage, &p.Environment, &p.Status, &p.CreatorUserID, &p.LifecycleStatus, &p.ActiveWorkflowKey, &p.WorkflowSelectionVersion, &p.WorkflowPlanRevision, &p.ActiveWorkflowChangedAt, &p.ActiveWorkflowChangedBy, &p.CreatedAt, &p.UpdatedAt, ) if errors.Is(err, pgx.ErrNoRows) { return nil, ErrProjectNotFound } if err != nil { return nil, fmt.Errorf("query project failed: %w", err) } return p, nil } func (s *pgStore) GetProjectByCode(ctx context.Context, code string) (*Project, error) { query := ` SELECT id, code, name, customer_name, device_type, intended_use, target_market, current_stage, environment, status, COALESCE(creator_user_id, 1), COALESCE(lifecycle_status, 'planning'), COALESCE(active_workflow_key, ''), COALESCE(workflow_selection_version, 1), COALESCE(workflow_plan_revision, 0), active_workflow_changed_at, active_workflow_changed_by, created_at, updated_at FROM projects WHERE code = $1 ` p := &Project{} err := s.pool.QueryRow(ctx, query, code).Scan( &p.ID, &p.Code, &p.Name, &p.CustomerName, &p.DeviceType, &p.IntendedUse, &p.TargetMarket, &p.CurrentStage, &p.Environment, &p.Status, &p.CreatorUserID, &p.LifecycleStatus, &p.ActiveWorkflowKey, &p.WorkflowSelectionVersion, &p.WorkflowPlanRevision, &p.ActiveWorkflowChangedAt, &p.ActiveWorkflowChangedBy, &p.CreatedAt, &p.UpdatedAt, ) if errors.Is(err, pgx.ErrNoRows) { return nil, ErrProjectNotFound } if err != nil { return nil, fmt.Errorf("query project by code failed: %w", err) } return p, nil } func (s *pgStore) ListProjectsForUser(ctx context.Context, userID int64) ([]*Project, error) { // 仅创建人或 active 项目成员可见(移除 creator_id=1/NULL 公开规则) query := ` SELECT DISTINCT p.id, p.code, p.name, p.customer_name, p.device_type, p.intended_use, p.target_market, p.current_stage, p.environment, p.status, COALESCE(p.creator_user_id, 0), COALESCE(p.lifecycle_status, 'planning'), COALESCE(p.active_workflow_key, ''), COALESCE(p.workflow_selection_version, 1), COALESCE(p.workflow_plan_revision, 0), p.active_workflow_changed_at, p.active_workflow_changed_by, p.created_at, p.updated_at FROM projects p LEFT JOIN project_members pm ON pm.project_id = p.id AND pm.user_id = $1 AND pm.status = 'active' WHERE (p.creator_user_id = $1 OR pm.user_id IS NOT NULL) AND COALESCE(p.status, 'active') <> 'demo_retired' ORDER BY p.id DESC ` rows, err := s.pool.Query(ctx, query, userID) if err != nil { return nil, fmt.Errorf("list projects failed: %w", err) } defer rows.Close() var list []*Project for rows.Next() { p := &Project{} if err := rows.Scan( &p.ID, &p.Code, &p.Name, &p.CustomerName, &p.DeviceType, &p.IntendedUse, &p.TargetMarket, &p.CurrentStage, &p.Environment, &p.Status, &p.CreatorUserID, &p.LifecycleStatus, &p.ActiveWorkflowKey, &p.WorkflowSelectionVersion, &p.WorkflowPlanRevision, &p.ActiveWorkflowChangedAt, &p.ActiveWorkflowChangedBy, &p.CreatedAt, &p.UpdatedAt, ); err != nil { return nil, err } list = append(list, p) } return list, rows.Err() } func (s *pgStore) ListAllProjects(ctx context.Context) ([]*Project, error) { query := ` SELECT p.id, p.code, p.name, p.customer_name, p.device_type, p.intended_use, p.target_market, p.current_stage, p.environment, p.status, COALESCE(p.creator_user_id, 0), COALESCE(p.lifecycle_status, 'planning'), COALESCE(p.active_workflow_key, ''), COALESCE(p.workflow_selection_version, 1), COALESCE(p.workflow_plan_revision, 0), p.active_workflow_changed_at, p.active_workflow_changed_by, p.created_at, p.updated_at FROM projects p WHERE COALESCE(p.status, 'active') <> 'demo_retired' ORDER BY p.id DESC ` rows, err := s.pool.Query(ctx, query) if err != nil { return nil, fmt.Errorf("list all projects failed: %w", err) } defer rows.Close() var list []*Project for rows.Next() { p := &Project{} if err := rows.Scan( &p.ID, &p.Code, &p.Name, &p.CustomerName, &p.DeviceType, &p.IntendedUse, &p.TargetMarket, &p.CurrentStage, &p.Environment, &p.Status, &p.CreatorUserID, &p.LifecycleStatus, &p.ActiveWorkflowKey, &p.WorkflowSelectionVersion, &p.WorkflowPlanRevision, &p.ActiveWorkflowChangedAt, &p.ActiveWorkflowChangedBy, &p.CreatedAt, &p.UpdatedAt, ); err != nil { return nil, err } list = append(list, p) } return list, rows.Err() } func (s *pgStore) SaveProjectSourceBinding(ctx context.Context, projectID int64, repoPath, permissionScope string, createdBy int64) error { if projectID <= 0 || strings.TrimSpace(repoPath) == "" { return fmt.Errorf("invalid project source binding") } _, err := s.pool.Exec(ctx, ` INSERT INTO project_source_bindings (project_id, source_type, source_ref, permission_scope, created_by) VALUES ($1, 'git', $2, $3, NULLIF($4,0)) ON CONFLICT DO NOTHING `, projectID, strings.TrimSpace(repoPath), permissionScope, createdBy) return err }