Files
Agent-System/internal/projectworkflow/store_pg_project.go
T

346 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()
}
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
}