package service import ( "context" "encoding/json" "fmt" "os" "path/filepath" "time" "gcy_hpc_server/internal/model" "gcy_hpc_server/internal/store" "go.uber.org/zap" ) // ApplicationService handles parameter validation, script rendering, and job // submission for parameterized HPC applications. type ApplicationService struct { store *store.ApplicationStore jobSvc *JobService workDirBase string logger *zap.Logger taskSvc *TaskService } func NewApplicationService(store *store.ApplicationStore, jobSvc *JobService, workDirBase string, logger *zap.Logger, taskSvc ...*TaskService) *ApplicationService { var ts *TaskService if len(taskSvc) > 0 { ts = taskSvc[0] } return &ApplicationService{store: store, jobSvc: jobSvc, workDirBase: workDirBase, logger: logger, taskSvc: ts} } // ListApplications delegates to the store. func (s *ApplicationService) ListApplications(ctx context.Context, page, pageSize int) ([]model.Application, int, error) { return s.store.List(ctx, page, pageSize) } // CreateApplication delegates to the store. func (s *ApplicationService) CreateApplication(ctx context.Context, req *model.CreateApplicationRequest) (int64, error) { return s.store.Create(ctx, req) } // GetApplication delegates to the store. func (s *ApplicationService) GetApplication(ctx context.Context, id int64) (*model.Application, error) { return s.store.GetByID(ctx, id) } // UpdateApplication delegates to the store. func (s *ApplicationService) UpdateApplication(ctx context.Context, id int64, req *model.UpdateApplicationRequest) error { return s.store.Update(ctx, id, req) } // DeleteApplication delegates to the store. func (s *ApplicationService) DeleteApplication(ctx context.Context, id int64) error { return s.store.Delete(ctx, id) } // SubmitFromApplication orchestrates the full submission flow. // When TaskService is available, it delegates to ProcessTaskSync which creates // an hpc_tasks record and runs the full pipeline. Otherwise falls back to the // original direct implementation. func (s *ApplicationService) SubmitFromApplication(ctx context.Context, applicationID int64, values map[string]string) (*model.JobResponse, error) { if s.taskSvc != nil { req := &model.CreateTaskRequest{ AppID: applicationID, Values: values, InputFileIDs: nil, // old API has no file_ids concept TaskName: "", } return s.taskSvc.ProcessTaskSync(ctx, req) } // Fallback: original direct logic when TaskService not available app, err := s.store.GetByID(ctx, applicationID) if err != nil { return nil, fmt.Errorf("get application: %w", err) } if app == nil { return nil, fmt.Errorf("application %d not found", applicationID) } var params []model.ParameterSchema if len(app.Parameters) > 0 { if err := json.Unmarshal(app.Parameters, ¶ms); err != nil { return nil, fmt.Errorf("parse parameters: %w", err) } } if err := ValidateParams(params, values); err != nil { return nil, err } rendered := RenderScript(app.ScriptTemplate, params, values) workDir := "" if s.workDirBase != "" { safeName := SanitizeDirName(app.Name) subDir := time.Now().Format("20060102_150405") + "_" + RandomSuffix(4) workDir = filepath.Join(s.workDirBase, safeName, subDir) if err := os.MkdirAll(workDir, 0777); err != nil { return nil, fmt.Errorf("create work directory %s: %w", workDir, err) } // 绕过 umask,确保整条路径都有写权限 for dir := workDir; dir != s.workDirBase; dir = filepath.Dir(dir) { os.Chmod(dir, 0777) } os.Chmod(s.workDirBase, 0777) } req := &model.SubmitJobRequest{Script: rendered, WorkDir: workDir} return s.jobSvc.SubmitJob(ctx, req) }