Files
web-giao-ban-go/handlers/issues.go
T

288 lines
7.5 KiB
Go
Raw Normal View History

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!",
})
}