feat: DH-HIS Web Giao Ban Go v1.0.1 (Favicon, Exe icon, System Tray, Gitea auto-update)
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
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!",
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user