package handlers import ( "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "strings" "sync" "time" "web-giao-ban-go/logger" ) type AttachmentInfo struct { Filename string `json:"filename"` OriginalName string `json:"originalname"` Size int64 `json:"size"` MimeType string `json:"mimetype"` URL string `json:"url"` } type Issue struct { ID string `json:"id"` Title string `json:"title"` Category string `json:"category"` Module string `json:"module"` Priority string `json:"priority"` Database string `json:"database"` DatabaseName string `json:"databaseName"` Period string `json:"period"` Reporter string `json:"reporter"` Department string `json:"department"` Description string `json:"description"` Status string `json:"status"` // CHO_XU_LY, DANG_XU_LY, DA_XU_LY, DONG HandlerNotes string `json:"handlerNotes"` ResolvedBy string `json:"resolvedBy"` ResolvedAt *string `json:"resolvedAt"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` Attachment *AttachmentInfo `json:"attachment,omitempty"` } var ( issuesMu sync.RWMutex dataFile = "data/issues.json" uploadsDir = "uploads/issues" ) func EnsureDirs() { _ = os.MkdirAll("data", 0755) _ = os.MkdirAll(uploadsDir, 0755) if _, err := os.Stat(dataFile); os.IsNotExist(err) { _ = os.WriteFile(dataFile, []byte("[]"), 0644) } } func getAllIssues() []Issue { EnsureDirs() issuesMu.RLock() defer issuesMu.RUnlock() raw, err := os.ReadFile(dataFile) if err != nil { return []Issue{} } var list []Issue if err := json.Unmarshal(raw, &list); err != nil { return []Issue{} } return list } func saveIssues(list []Issue) error { EnsureDirs() issuesMu.Lock() defer issuesMu.Unlock() data, err := json.MarshalIndent(list, "", " ") if err != nil { return err } tmp := dataFile + ".tmp" if err := os.WriteFile(tmp, data, 0644); err != nil { return err } return os.Rename(tmp, dataFile) } func generateIssueID(all []Issue) string { now := time.Now() prefix := fmt.Sprintf("ISS-%s", now.Format("20060102")) count := 1 for _, item := range all { if strings.HasPrefix(item.ID, prefix) { count++ } } return fmt.Sprintf("%s-%03d", prefix, count) } // GET /api/issues func HandleGetIssues(w http.ResponseWriter, r *http.Request) { list := getAllIssues() w.Header().Set("Content-Type", "application/json; charset=utf-8") _ = json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "data": list, }) } // POST /api/issues func HandleCreateIssue(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") // Limit 25MB if err := r.ParseMultipartForm(25 << 20); err != nil { w.WriteHeader(http.StatusBadRequest) _ = json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "message": "File quá lớn (tối đa 25MB)"}) return } title := strings.TrimSpace(r.FormValue("title")) if title == "" { title = "Sự cố không có tiêu đề" } all := getAllIssues() id := generateIssueID(all) nowStr := time.Now().Format(time.RFC3339) var attach *AttachmentInfo file, header, err := r.FormFile("attachment") if err == nil && file != nil { defer file.Close() ext := filepath.Ext(header.Filename) cleanBase := strings.Map(func(r rune) rune { if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '-' { return r } return '_' }, strings.TrimSuffix(header.Filename, ext)) safeFilename := fmt.Sprintf("%s_%d%s", cleanBase, time.Now().UnixNano()/1e6, ext) destPath := filepath.Join(uploadsDir, safeFilename) dst, err := os.Create(destPath) if err == nil { defer dst.Close() _, _ = io.Copy(dst, file) attach = &AttachmentInfo{ Filename: safeFilename, OriginalName: header.Filename, Size: header.Size, MimeType: header.Header.Get("Content-Type"), URL: "/uploads/issues/" + safeFilename, } } } newIssue := Issue{ ID: id, Title: title, Category: r.FormValue("category"), Module: r.FormValue("module"), Priority: r.FormValue("priority"), Database: r.FormValue("database"), DatabaseName: r.FormValue("databaseName"), Period: r.FormValue("period"), Reporter: strings.TrimSpace(r.FormValue("reporter")), Department: strings.TrimSpace(r.FormValue("department")), Description: strings.TrimSpace(r.FormValue("description")), Status: "CHO_XU_LY", CreatedAt: nowStr, UpdatedAt: nowStr, Attachment: attach, } all = append([]Issue{newIssue}, all...) _ = saveIssues(all) logger.Info("Đã ghi nhận Issue mới: [%s] %s từ %s", id, title, newIssue.Reporter) _ = json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "data": newIssue, "message": "Đã ghi nhận báo lỗi thành công!", }) } // PUT /api/issues/{id} func HandleUpdateIssue(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") pathParts := strings.Split(r.URL.Path, "/") id := pathParts[len(pathParts)-1] var body struct { Status string `json:"status"` ResolvedBy string `json:"resolvedBy"` HandlerNotes string `json:"handlerNotes"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { w.WriteHeader(http.StatusBadRequest) _ = json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "message": "Dữ liệu không hợp lệ"}) return } all := getAllIssues() found := false var updated Issue nowStr := time.Now().Format(time.RFC3339) for i := range all { if all[i].ID == id { if body.Status != "" { all[i].Status = body.Status } all[i].ResolvedBy = body.ResolvedBy all[i].HandlerNotes = body.HandlerNotes all[i].UpdatedAt = nowStr if (body.Status == "DA_XU_LY" || body.Status == "DONG") && all[i].ResolvedAt == nil { all[i].ResolvedAt = &nowStr } updated = all[i] found = true break } } if !found { w.WriteHeader(http.StatusNotFound) _ = json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "message": "Không tìm thấy issue"}) return } _ = saveIssues(all) logger.Info("Cập nhật Issue [%s]: Trạng thái=%s, Kỹ thuật=%s", id, updated.Status, updated.ResolvedBy) _ = json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "data": updated, "message": "Cập nhật issue thành công!", }) } // DELETE /api/issues/{id} func HandleDeleteIssue(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") pathParts := strings.Split(r.URL.Path, "/") id := pathParts[len(pathParts)-1] all := getAllIssues() filtered := make([]Issue, 0, len(all)) var removed *Issue for _, item := range all { if item.ID == id { cpy := item removed = &cpy continue } filtered = append(filtered, item) } if removed == nil { w.WriteHeader(http.StatusNotFound) _ = json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "message": "Không tìm thấy issue"}) return } if removed.Attachment != nil && removed.Attachment.Filename != "" { _ = os.Remove(filepath.Join(uploadsDir, removed.Attachment.Filename)) } _ = saveIssues(filtered) logger.Info("Đã xóa Issue [%s]", id) _ = json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": "Đã xóa issue thành công!", }) }