288 lines
7.5 KiB
Go
288 lines
7.5 KiB
Go
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!",
|
|
})
|
|
}
|