Move bizdev/freedoc/intent/projectworkflow/squad giant files into ≤800-line use-case siblings without behavior change; short tests green.
334 lines
14 KiB
Go
334 lines
14 KiB
Go
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()
|
|
}
|