您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

475 行
15KB

  1. package handlers
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net/http"
  6. "net/url"
  7. "strconv"
  8. "strings"
  9. "github.com/go-chi/chi/v5"
  10. "qctextbuilder/internal/buildsvc"
  11. "qctextbuilder/internal/config"
  12. "qctextbuilder/internal/domain"
  13. "qctextbuilder/internal/onboarding"
  14. "qctextbuilder/internal/templatesvc"
  15. )
  16. type UI struct {
  17. templateSvc *templatesvc.Service
  18. onboardSvc *onboarding.Service
  19. buildSvc buildsvc.Service
  20. cfg config.Config
  21. render htmlRenderer
  22. }
  23. type htmlRenderer interface {
  24. Render(w http.ResponseWriter, name string, data any)
  25. }
  26. type pageData struct {
  27. Title string
  28. Msg string
  29. Err string
  30. Current string
  31. }
  32. type homePageData struct {
  33. pageData
  34. TemplateCount int
  35. }
  36. type settingsPageData struct {
  37. pageData
  38. QCBaseURL string
  39. PollIntervalSeconds int
  40. PollTimeoutSeconds int
  41. PollMaxConcurrent int
  42. TokenConfigured bool
  43. LanguageOutputMode string
  44. }
  45. type templatesPageData struct {
  46. pageData
  47. Templates []domain.Template
  48. }
  49. type templateFieldView struct {
  50. Path string
  51. FieldKind string
  52. IsEnabled bool
  53. IsRequiredByUs bool
  54. DisplayLabel string
  55. DisplayOrder int
  56. Notes string
  57. SampleValue string
  58. }
  59. type templateDetailPageData struct {
  60. pageData
  61. Detail *templatesvc.TemplateDetail
  62. Fields []templateFieldView
  63. }
  64. type buildFieldView struct {
  65. Path string
  66. DisplayLabel string
  67. SampleValue string
  68. Value string
  69. }
  70. type buildNewPageData struct {
  71. pageData
  72. Templates []domain.Template
  73. SelectedTemplateID int64
  74. SelectedManifestID string
  75. EnabledFields []buildFieldView
  76. Form buildFormInput
  77. }
  78. type buildFormInput struct {
  79. RequestName string
  80. CompanyName string
  81. BusinessType string
  82. Username string
  83. Email string
  84. Phone string
  85. OrgNumber string
  86. StartDate string
  87. Mission string
  88. DescriptionShort string
  89. DescriptionLong string
  90. SiteLanguage string
  91. AddressLine1 string
  92. AddressLine2 string
  93. AddressCity string
  94. AddressRegion string
  95. AddressZIP string
  96. AddressCountry string
  97. }
  98. type buildDetailPageData struct {
  99. pageData
  100. Build *domain.SiteBuild
  101. EffectiveGlobal []byte
  102. CanPoll bool
  103. CanFetchEditorURL bool
  104. AutoRefreshSeconds int
  105. }
  106. func NewUI(templateSvc *templatesvc.Service, onboardSvc *onboarding.Service, buildSvc buildsvc.Service, cfg config.Config, render htmlRenderer) *UI {
  107. return &UI{templateSvc: templateSvc, onboardSvc: onboardSvc, buildSvc: buildSvc, cfg: cfg, render: render}
  108. }
  109. func (u *UI) Home(w http.ResponseWriter, r *http.Request) {
  110. templates, err := u.templateSvc.ListTemplates(r.Context())
  111. if err != nil {
  112. u.render.Render(w, "home", homePageData{pageData: basePageData(r, "Home", "/"), TemplateCount: 0})
  113. return
  114. }
  115. u.render.Render(w, "home", homePageData{pageData: basePageData(r, "Home", "/"), TemplateCount: len(templates)})
  116. }
  117. func (u *UI) Settings(w http.ResponseWriter, r *http.Request) {
  118. u.render.Render(w, "settings", settingsPageData{
  119. pageData: basePageData(r, "Settings", "/settings"),
  120. QCBaseURL: u.cfg.QCBaseURL,
  121. PollIntervalSeconds: u.cfg.PollIntervalSeconds,
  122. PollTimeoutSeconds: u.cfg.PollTimeoutSeconds,
  123. PollMaxConcurrent: u.cfg.PollMaxConcurrent,
  124. TokenConfigured: strings.TrimSpace(u.cfg.QCToken) != "",
  125. LanguageOutputMode: "EN",
  126. })
  127. }
  128. func (u *UI) Templates(w http.ResponseWriter, r *http.Request) {
  129. templates, err := u.templateSvc.ListTemplates(r.Context())
  130. if err != nil {
  131. http.Error(w, err.Error(), http.StatusBadRequest)
  132. return
  133. }
  134. u.render.Render(w, "templates", templatesPageData{pageData: basePageData(r, "Templates", "/templates"), Templates: templates})
  135. }
  136. func (u *UI) SyncTemplates(w http.ResponseWriter, r *http.Request) {
  137. if _, err := u.templateSvc.SyncAITemplates(r.Context()); err != nil {
  138. http.Redirect(w, r, "/templates?err="+urlQuery(err.Error()), http.StatusSeeOther)
  139. return
  140. }
  141. http.Redirect(w, r, "/templates?msg=sync+done", http.StatusSeeOther)
  142. }
  143. func (u *UI) TemplateDetail(w http.ResponseWriter, r *http.Request) {
  144. templateID, ok := parseTemplateID(w, r)
  145. if !ok {
  146. return
  147. }
  148. detail, err := u.templateSvc.GetTemplateDetail(r.Context(), templateID)
  149. if err != nil {
  150. http.Error(w, err.Error(), http.StatusNotFound)
  151. return
  152. }
  153. fields := make([]templateFieldView, 0, len(detail.Fields))
  154. for _, f := range detail.Fields {
  155. fields = append(fields, templateFieldView{
  156. Path: f.Path,
  157. FieldKind: f.FieldKind,
  158. IsEnabled: f.IsEnabled,
  159. IsRequiredByUs: f.IsRequiredByUs,
  160. DisplayLabel: f.DisplayLabel,
  161. DisplayOrder: f.DisplayOrder,
  162. Notes: f.Notes,
  163. SampleValue: f.SampleValue,
  164. })
  165. }
  166. u.render.Render(w, "template_detail", templateDetailPageData{pageData: basePageData(r, "Template Detail", "/templates"), Detail: detail, Fields: fields})
  167. }
  168. func (u *UI) OnboardTemplate(w http.ResponseWriter, r *http.Request) {
  169. templateID, ok := parseTemplateID(w, r)
  170. if !ok {
  171. return
  172. }
  173. if _, _, err := u.onboardSvc.OnboardTemplate(r.Context(), templateID); err != nil {
  174. http.Redirect(w, r, fmt.Sprintf("/templates/%d?err=%s", templateID, urlQuery(err.Error())), http.StatusSeeOther)
  175. return
  176. }
  177. http.Redirect(w, r, fmt.Sprintf("/templates/%d?msg=onboarding+done", templateID), http.StatusSeeOther)
  178. }
  179. func (u *UI) UpdateTemplateFields(w http.ResponseWriter, r *http.Request) {
  180. templateID, ok := parseTemplateID(w, r)
  181. if !ok {
  182. return
  183. }
  184. if err := r.ParseForm(); err != nil {
  185. http.Redirect(w, r, fmt.Sprintf("/templates/%d?err=%s", templateID, urlQuery("invalid form")), http.StatusSeeOther)
  186. return
  187. }
  188. count, _ := strconv.Atoi(r.FormValue("field_count"))
  189. patches := make([]onboarding.FieldPatch, 0, count)
  190. for i := 0; i < count; i++ {
  191. path := strings.TrimSpace(r.FormValue(fmt.Sprintf("field_path_%d", i)))
  192. if path == "" {
  193. continue
  194. }
  195. enabled := r.FormValue(fmt.Sprintf("field_enabled_%d", i)) == "on"
  196. required := r.FormValue(fmt.Sprintf("field_required_%d", i)) == "on"
  197. label := r.FormValue(fmt.Sprintf("field_label_%d", i))
  198. notes := r.FormValue(fmt.Sprintf("field_notes_%d", i))
  199. order, err := strconv.Atoi(strings.TrimSpace(r.FormValue(fmt.Sprintf("field_order_%d", i))))
  200. if err != nil {
  201. http.Redirect(w, r, fmt.Sprintf("/templates/%d?err=%s", templateID, urlQuery("invalid display order")), http.StatusSeeOther)
  202. return
  203. }
  204. patches = append(patches, onboarding.FieldPatch{
  205. Path: path,
  206. IsEnabled: boolPtr(enabled),
  207. IsRequiredByUs: boolPtr(required),
  208. DisplayLabel: strPtr(label),
  209. DisplayOrder: intPtr(order),
  210. Notes: strPtr(notes),
  211. })
  212. }
  213. manifestID := r.FormValue("manifest_id")
  214. if _, _, err := u.onboardSvc.UpdateTemplateFields(r.Context(), templateID, manifestID, patches); err != nil {
  215. http.Redirect(w, r, fmt.Sprintf("/templates/%d?err=%s", templateID, urlQuery(err.Error())), http.StatusSeeOther)
  216. return
  217. }
  218. http.Redirect(w, r, fmt.Sprintf("/templates/%d?msg=fields+saved", templateID), http.StatusSeeOther)
  219. }
  220. func (u *UI) BuildNew(w http.ResponseWriter, r *http.Request) {
  221. selectedTemplateID, _ := strconv.ParseInt(strings.TrimSpace(r.URL.Query().Get("template_id")), 10, 64)
  222. data, err := u.loadBuildNewPageData(r, basePageData(r, "New Build", "/builds/new"), selectedTemplateID, buildFormInput{}, nil)
  223. if err != nil {
  224. http.Error(w, err.Error(), http.StatusBadRequest)
  225. return
  226. }
  227. u.render.Render(w, "build_new", data)
  228. }
  229. func (u *UI) CreateBuild(w http.ResponseWriter, r *http.Request) {
  230. if err := r.ParseForm(); err != nil {
  231. http.Redirect(w, r, "/builds/new?err=invalid+form", http.StatusSeeOther)
  232. return
  233. }
  234. form := buildFormInputFromRequest(r)
  235. fieldValues := parseBuildFieldValues(r)
  236. templateID, err := strconv.ParseInt(strings.TrimSpace(r.FormValue("template_id")), 10, 64)
  237. if err != nil || templateID <= 0 {
  238. http.Redirect(w, r, "/builds/new?err=invalid+template", http.StatusSeeOther)
  239. return
  240. }
  241. result, err := u.buildSvc.StartBuild(r.Context(), buildsvc.StartBuildRequest{
  242. TemplateID: templateID,
  243. RequestName: form.RequestName,
  244. GlobalData: buildsvc.BuildGlobalData(buildsvc.GlobalDataInput{
  245. CompanyName: form.CompanyName,
  246. BusinessType: form.BusinessType,
  247. Username: form.Username,
  248. Email: form.Email,
  249. Phone: form.Phone,
  250. OrgNumber: form.OrgNumber,
  251. StartDate: form.StartDate,
  252. Mission: form.Mission,
  253. DescriptionShort: form.DescriptionShort,
  254. DescriptionLong: form.DescriptionLong,
  255. SiteLanguage: form.SiteLanguage,
  256. AddressLine1: form.AddressLine1,
  257. AddressLine2: form.AddressLine2,
  258. AddressCity: form.AddressCity,
  259. AddressRegion: form.AddressRegion,
  260. AddressZIP: form.AddressZIP,
  261. AddressCountry: form.AddressCountry,
  262. }),
  263. FieldValues: fieldValues,
  264. })
  265. if err != nil {
  266. data, loadErr := u.loadBuildNewPageData(r, pageData{
  267. Title: "New Build",
  268. Err: err.Error(),
  269. Current: "/builds/new",
  270. }, templateID, form, fieldValues)
  271. if loadErr != nil {
  272. http.Error(w, loadErr.Error(), http.StatusBadRequest)
  273. return
  274. }
  275. u.render.Render(w, "build_new", data)
  276. return
  277. }
  278. http.Redirect(w, r, fmt.Sprintf("/builds/%s?msg=build+started", result.BuildID), http.StatusSeeOther)
  279. }
  280. func (u *UI) BuildDetail(w http.ResponseWriter, r *http.Request) {
  281. buildID := strings.TrimSpace(chi.URLParam(r, "id"))
  282. build, err := u.buildSvc.GetBuild(r.Context(), buildID)
  283. if err != nil {
  284. http.Error(w, err.Error(), http.StatusNotFound)
  285. return
  286. }
  287. status := strings.ToLower(strings.TrimSpace(build.QCStatus))
  288. canPoll := status == "queued" || status == "processing"
  289. canFetchEditor := (status == "done" || status == "failed" || status == "timeout") &&
  290. build.QCSiteID != nil &&
  291. strings.TrimSpace(build.QCEditorURL) == ""
  292. autoRefresh := 0
  293. if canPoll && u.cfg.PollIntervalSeconds > 0 {
  294. autoRefresh = u.cfg.PollIntervalSeconds
  295. }
  296. effectiveGlobal := build.GlobalDataJSON
  297. if payloadGlobal, err := extractGlobalDataFromFinalPayload(build.FinalSitesPayload); err == nil && len(payloadGlobal) > 0 {
  298. effectiveGlobal = payloadGlobal
  299. }
  300. u.render.Render(w, "build_detail", buildDetailPageData{
  301. pageData: basePageData(r, "Build Detail", "/builds"),
  302. Build: build,
  303. EffectiveGlobal: effectiveGlobal,
  304. CanPoll: canPoll,
  305. CanFetchEditorURL: canFetchEditor,
  306. AutoRefreshSeconds: autoRefresh,
  307. })
  308. }
  309. func (u *UI) PollBuild(w http.ResponseWriter, r *http.Request) {
  310. buildID := strings.TrimSpace(chi.URLParam(r, "id"))
  311. if err := u.buildSvc.PollOnce(r.Context(), buildID); err != nil {
  312. http.Redirect(w, r, fmt.Sprintf("/builds/%s?err=%s", buildID, urlQuery(err.Error())), http.StatusSeeOther)
  313. return
  314. }
  315. http.Redirect(w, r, fmt.Sprintf("/builds/%s?msg=poll+done", buildID), http.StatusSeeOther)
  316. }
  317. func (u *UI) FetchEditorURL(w http.ResponseWriter, r *http.Request) {
  318. buildID := strings.TrimSpace(chi.URLParam(r, "id"))
  319. if err := u.buildSvc.FetchEditorURL(r.Context(), buildID); err != nil {
  320. http.Redirect(w, r, fmt.Sprintf("/builds/%s?err=%s", buildID, urlQuery(err.Error())), http.StatusSeeOther)
  321. return
  322. }
  323. http.Redirect(w, r, fmt.Sprintf("/builds/%s?msg=editor+url+loaded", buildID), http.StatusSeeOther)
  324. }
  325. func basePageData(r *http.Request, title, current string) pageData {
  326. q := r.URL.Query()
  327. return pageData{Title: title, Msg: q.Get("msg"), Err: q.Get("err"), Current: current}
  328. }
  329. func parseTemplateID(w http.ResponseWriter, r *http.Request) (int64, bool) {
  330. rawID := chi.URLParam(r, "id")
  331. templateID, err := strconv.ParseInt(rawID, 10, 64)
  332. if err != nil {
  333. http.Error(w, "invalid template id", http.StatusBadRequest)
  334. return 0, false
  335. }
  336. return templateID, true
  337. }
  338. func urlQuery(s string) string {
  339. return url.QueryEscape(s)
  340. }
  341. func boolPtr(v bool) *bool { return &v }
  342. func intPtr(v int) *int { return &v }
  343. func strPtr(v string) *string {
  344. return &v
  345. }
  346. func (u *UI) loadBuildNewPageData(r *http.Request, page pageData, selectedTemplateID int64, form buildFormInput, fieldValues map[string]string) (buildNewPageData, error) {
  347. templates, err := u.templateSvc.ListTemplates(r.Context())
  348. if err != nil {
  349. return buildNewPageData{}, err
  350. }
  351. data := buildNewPageData{
  352. pageData: page,
  353. Templates: templates,
  354. SelectedTemplateID: selectedTemplateID,
  355. Form: form,
  356. }
  357. if selectedTemplateID <= 0 {
  358. return data, nil
  359. }
  360. detail, err := u.templateSvc.GetTemplateDetail(r.Context(), selectedTemplateID)
  361. if err != nil || detail.Manifest == nil {
  362. return data, nil
  363. }
  364. data.SelectedManifestID = detail.Manifest.ID
  365. for _, f := range detail.Fields {
  366. if !f.IsEnabled || f.FieldKind != "text" {
  367. continue
  368. }
  369. data.EnabledFields = append(data.EnabledFields, buildFieldView{
  370. Path: f.Path,
  371. DisplayLabel: f.DisplayLabel,
  372. SampleValue: f.SampleValue,
  373. Value: strings.TrimSpace(fieldValues[f.Path]),
  374. })
  375. }
  376. return data, nil
  377. }
  378. func buildFormInputFromRequest(r *http.Request) buildFormInput {
  379. return buildFormInput{
  380. RequestName: strings.TrimSpace(r.FormValue("request_name")),
  381. CompanyName: strings.TrimSpace(r.FormValue("company_name")),
  382. BusinessType: strings.TrimSpace(r.FormValue("business_type")),
  383. Username: strings.TrimSpace(r.FormValue("username")),
  384. Email: strings.TrimSpace(r.FormValue("email")),
  385. Phone: strings.TrimSpace(r.FormValue("phone")),
  386. OrgNumber: strings.TrimSpace(r.FormValue("org_number")),
  387. StartDate: strings.TrimSpace(r.FormValue("start_date")),
  388. Mission: strings.TrimSpace(r.FormValue("mission")),
  389. DescriptionShort: strings.TrimSpace(r.FormValue("description_short")),
  390. DescriptionLong: strings.TrimSpace(r.FormValue("description_long")),
  391. SiteLanguage: strings.TrimSpace(r.FormValue("site_language")),
  392. AddressLine1: strings.TrimSpace(r.FormValue("address_line1")),
  393. AddressLine2: strings.TrimSpace(r.FormValue("address_line2")),
  394. AddressCity: strings.TrimSpace(r.FormValue("address_city")),
  395. AddressRegion: strings.TrimSpace(r.FormValue("address_region")),
  396. AddressZIP: strings.TrimSpace(r.FormValue("address_zip")),
  397. AddressCountry: strings.TrimSpace(r.FormValue("address_country")),
  398. }
  399. }
  400. func parseBuildFieldValues(r *http.Request) map[string]string {
  401. fieldValues := map[string]string{}
  402. count, _ := strconv.Atoi(strings.TrimSpace(r.FormValue("field_count")))
  403. for i := 0; i < count; i++ {
  404. path := strings.TrimSpace(r.FormValue(fmt.Sprintf("field_path_%d", i)))
  405. value := strings.TrimSpace(r.FormValue(fmt.Sprintf("field_value_%d", i)))
  406. if path != "" {
  407. fieldValues[path] = value
  408. }
  409. }
  410. return fieldValues
  411. }
  412. func extractGlobalDataFromFinalPayload(raw []byte) ([]byte, error) {
  413. if len(raw) == 0 {
  414. return nil, nil
  415. }
  416. var payload struct {
  417. GlobalData map[string]any `json:"globalData"`
  418. }
  419. if err := json.Unmarshal(raw, &payload); err != nil {
  420. return nil, err
  421. }
  422. if len(payload.GlobalData) == 0 {
  423. return nil, nil
  424. }
  425. data, err := json.Marshal(payload.GlobalData)
  426. if err != nil {
  427. return nil, err
  428. }
  429. return data, nil
  430. }