тестовый го бэкэнд

This commit is contained in:
Artem Tsyrulnikov
2026-03-27 00:18:42 +03:00
parent 9edb9d6aca
commit 730d453371
7 changed files with 1314 additions and 0 deletions

726
backend/handler.go Normal file
View File

@@ -0,0 +1,726 @@
package main
import (
"encoding/json"
"fmt"
"io"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gorm.io/gorm"
)
// --- Helpers ---
func parseUUID(c *gin.Context, param string) (uuid.UUID, bool) {
id, err := uuid.Parse(c.Param(param))
if err != nil {
c.JSON(400, gin.H{"detail": "invalid uuid"})
return uuid.Nil, false
}
return id, true
}
func templateAttributeDetails(db *gorm.DB, templateID uuid.UUID) []gin.H {
var allAttrs []Attribute
db.Find(&allAttrs)
var tas []TemplateAttribute
db.Where("template_id = ?", templateID).Preload("Attribute").Find(&tas)
taMap := map[uuid.UUID]*TemplateAttribute{}
for i := range tas {
taMap[tas[i].AttributeID] = &tas[i]
}
result := []gin.H{}
for _, a := range allAttrs {
detail := gin.H{
"attribute_id": a.ID,
"attribute_name": a.Name,
"is_required": a.IsRequired,
"value": nil,
}
if ta, ok := taMap[a.ID]; ok {
detail["value"] = ta.Value
}
result = append(result, detail)
}
return result
}
func templateWithAttrs(db *gorm.DB, t Template) gin.H {
return gin.H{
"id": t.ID,
"name": t.Name,
"description": t.Description,
"elements": json.RawMessage(t.Elements),
"group_id": t.GroupID,
"attributes": templateAttributeDetails(db, t.ID),
"order": t.Order,
"created_at": t.CreatedAt,
"updated_at": t.UpdatedAt,
"deleted_at": t.DeletedAt,
}
}
// --- Templates ---
func getTemplates(c *gin.Context, db *gorm.DB) {
var templates []Template
db.Find(&templates)
result := []gin.H{}
for _, t := range templates {
result = append(result, templateWithAttrs(db, t))
}
c.JSON(200, gin.H{"templates": result})
}
func getTemplate(c *gin.Context, db *gorm.DB) {
id, ok := parseUUID(c, "id")
if !ok {
return
}
var t Template
if db.First(&t, "id = ?", id).Error != nil {
c.JSON(200, gin.H{"template": nil})
return
}
c.JSON(200, gin.H{"template": templateWithAttrs(db, t)})
}
func createTemplate(c *gin.Context, db *gorm.DB) {
var input struct {
Name string `json:"name"`
Description string `json:"description"`
Elements json.RawMessage `json:"elements"`
Attributes []setTemplateAttrInput `json:"attributes"`
GroupID *uuid.UUID `json:"group_id"`
}
if c.BindJSON(&input) != nil {
return
}
t := Template{Name: input.Name, Description: &input.Description, Elements: JSON(input.Elements), GroupID: input.GroupID}
db.Create(&t)
for _, a := range input.Attributes {
db.Create(&TemplateAttribute{TemplateID: t.ID, AttributeID: a.AttributeID, Value: a.Value})
}
c.JSON(200, gin.H{"id": t.ID})
}
type setTemplateAttrInput struct {
AttributeID uuid.UUID `json:"attribute_id"`
Value *string `json:"value"`
}
func patchTemplate(c *gin.Context, db *gorm.DB) {
id, ok := parseUUID(c, "id")
if !ok {
return
}
var t Template
if db.First(&t, "id = ?", id).Error != nil {
c.JSON(404, gin.H{"detail": "Template not found"})
return
}
var input struct {
Name *string `json:"name"`
Description *string `json:"description"`
Elements *json.RawMessage `json:"elements"`
Attributes []setTemplateAttrInput `json:"attributes"`
GroupID interface{} `json:"group_id"`
Order *float64 `json:"order"`
}
if c.BindJSON(&input) != nil {
return
}
updates := map[string]interface{}{}
if input.Name != nil {
updates["name"] = *input.Name
}
if input.Description != nil {
updates["description"] = *input.Description
}
if input.Elements != nil {
updates["elements"] = JSON(*input.Elements)
}
if input.Order != nil {
updates["order"] = *input.Order
}
if input.GroupID != nil {
switch v := input.GroupID.(type) {
case string:
if v == "ungroup" {
updates["group_id"] = nil
} else {
if uid, err := uuid.Parse(v); err == nil {
updates["group_id"] = uid
}
}
}
}
if len(updates) > 0 {
db.Model(&t).Updates(updates)
}
for _, a := range input.Attributes {
res := db.Model(&TemplateAttribute{}).Where("template_id = ? AND attribute_id = ?", id, a.AttributeID).Update("value", a.Value)
if res.RowsAffected == 0 {
db.Create(&TemplateAttribute{TemplateID: id, AttributeID: a.AttributeID, Value: a.Value})
}
}
c.JSON(200, gin.H{"id": t.ID})
}
func deleteTemplate(c *gin.Context, db *gorm.DB) {
id, ok := parseUUID(c, "id")
if !ok {
return
}
res := db.Delete(&Template{}, "id = ?", id)
if res.RowsAffected == 0 {
c.JSON(404, gin.H{"detail": "Template not found"})
return
}
c.JSON(200, gin.H{"success": true, "template_id": id})
}
func copyTemplate(c *gin.Context, db *gorm.DB) {
var input struct {
TemplateID uuid.UUID `json:"template_id"`
NewName string `json:"new_name"`
NewDescription *string `json:"new_description"`
NewAttributes []setTemplateAttrInput `json:"new_attributes"`
GroupID *uuid.UUID `json:"group_id"`
CopyFirstSheetEmpty bool `json:"copy_first_sheet_empty"`
}
if c.BindJSON(&input) != nil {
return
}
var orig Template
if db.First(&orig, "id = ?", input.TemplateID).Error != nil {
c.JSON(400, gin.H{"detail": "Template not found"})
return
}
// Update element IDs
newElements := updateElementIDs(orig.Elements)
newT := Template{Name: input.NewName, Description: input.NewDescription, Elements: newElements, GroupID: input.GroupID}
db.Create(&newT)
// Copy files
var files []File
db.Where("template_id = ?", input.TemplateID).Find(&files)
for _, f := range files {
newF := File{Name: f.Name, Content: f.Content, Cells: f.Cells, TemplateID: &newT.ID}
db.Create(&newF)
}
// Copy sheets
var sheets []Sheet
db.Where("template_id = ?", input.TemplateID).Find(&sheets)
for _, s := range sheets {
cells := s.Cells
if s.Name == "L" && input.CopyFirstSheetEmpty {
cells = JSON("{}")
}
newS := Sheet{Name: s.Name, Cells: cells, TemplateID: newT.ID}
db.Create(&newS)
}
// Copy or set attributes
if len(input.NewAttributes) > 0 {
for _, a := range input.NewAttributes {
db.Create(&TemplateAttribute{TemplateID: newT.ID, AttributeID: a.AttributeID, Value: a.Value})
}
} else {
var origAttrs []TemplateAttribute
db.Where("template_id = ?", input.TemplateID).Find(&origAttrs)
for _, a := range origAttrs {
db.Create(&TemplateAttribute{TemplateID: newT.ID, AttributeID: a.AttributeID, Value: a.Value})
}
}
c.JSON(200, gin.H{"new_template_id": newT.ID})
}
func updateElementIDs(elements JSON) JSON {
var parsed map[string]interface{}
if json.Unmarshal(elements, &parsed) != nil || parsed == nil {
return elements
}
newElements := map[string]interface{}{}
for _, data := range parsed {
newID := uuid.New().String()
if m, ok := data.(map[string]interface{}); ok {
m["id"] = newID
}
newElements[newID] = data
}
result, _ := json.Marshal(newElements)
return JSON(result)
}
// --- Files ---
func getFiles(c *gin.Context, db *gorm.DB) {
var files []File
q := db.Model(&File{})
if tid := c.Query("template_id"); tid != "" {
q = q.Where("template_id = ?", tid)
}
q.Find(&files)
c.JSON(200, gin.H{"files": files})
}
func getFile(c *gin.Context, db *gorm.DB) {
id, ok := parseUUID(c, "id")
if !ok {
return
}
var f File
if db.First(&f, "id = ?", id).Error != nil {
c.JSON(200, gin.H{"file": nil})
return
}
c.JSON(200, gin.H{"file": f})
}
func uploadFile(c *gin.Context, db *gorm.DB) {
templateID, err := uuid.Parse(c.Query("template_id"))
if err != nil {
c.JSON(400, gin.H{"detail": "invalid template_id"})
return
}
fileHeader, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"detail": "file required"})
return
}
f, _ := fileHeader.Open()
defer f.Close()
content, _ := io.ReadAll(f)
cellsData, err := ReadCells(content)
if err != nil {
c.JSON(500, gin.H{"detail": fmt.Sprintf("failed to read cells: %v", err)})
return
}
cellsJSON, _ := json.Marshal(cellsData)
file := File{Name: fileHeader.Filename, Content: content, Cells: JSON(cellsJSON), TemplateID: &templateID}
db.Create(&file)
c.JSON(200, gin.H{"file_id": file.ID, "cells": cellsData})
}
// --- Sheets ---
func getSheets(c *gin.Context, db *gorm.DB) {
var sheets []Sheet
q := db.Model(&Sheet{})
if tid := c.Query("template_id"); tid != "" {
q = q.Where("template_id = ?", tid)
}
q.Find(&sheets)
c.JSON(200, gin.H{"sheets": sheets})
}
func getSheet(c *gin.Context, db *gorm.DB) {
id, ok := parseUUID(c, "id")
if !ok {
return
}
var s Sheet
if db.First(&s, "id = ?", id).Error != nil {
c.JSON(200, gin.H{"sheet": nil})
return
}
c.JSON(200, gin.H{"sheet": s})
}
func createSheet(c *gin.Context, db *gorm.DB) {
var input struct {
Name string `json:"name"`
Cells json.RawMessage `json:"cells"`
TemplateID uuid.UUID `json:"template_id"`
}
if c.BindJSON(&input) != nil {
return
}
s := Sheet{Name: input.Name, Cells: JSON(input.Cells), TemplateID: input.TemplateID}
db.Create(&s)
c.JSON(200, gin.H{"id": s.ID})
}
func patchSheet(c *gin.Context, db *gorm.DB) {
id, ok := parseUUID(c, "id")
if !ok {
return
}
var s Sheet
if db.First(&s, "id = ?", id).Error != nil {
c.JSON(404, gin.H{"detail": "Sheet not found"})
return
}
var input struct {
Name *string `json:"name"`
Cells *json.RawMessage `json:"cells"`
}
if c.BindJSON(&input) != nil {
return
}
updates := map[string]interface{}{}
if input.Name != nil {
updates["name"] = *input.Name
}
if input.Cells != nil {
updates["cells"] = JSON(*input.Cells)
}
if len(updates) > 0 {
db.Model(&s).Updates(updates)
}
c.JSON(200, gin.H{"id": s.ID})
}
// --- Attributes ---
func getAttributes(c *gin.Context, db *gorm.DB) {
var attrs []Attribute
db.Find(&attrs)
c.JSON(200, gin.H{"attributes": attrs})
}
func createAttribute(c *gin.Context, db *gorm.DB) {
var input struct {
Name string `json:"name"`
IsRequired bool `json:"is_required"`
}
if c.BindJSON(&input) != nil {
return
}
var existing Attribute
if db.Where("name = ?", input.Name).First(&existing).Error == nil {
c.JSON(400, gin.H{"detail": fmt.Sprintf("Атрибут с именем '%s' уже существует", input.Name)})
return
}
a := Attribute{Name: input.Name, IsRequired: input.IsRequired}
db.Create(&a)
c.JSON(200, gin.H{"id": a.ID})
}
func getAttributeValues(c *gin.Context, db *gorm.DB) {
id, ok := parseUUID(c, "id")
if !ok {
return
}
var attr Attribute
if db.First(&attr, "id = ?", id).Error != nil {
c.JSON(404, gin.H{"detail": "Attribute not found"})
return
}
type valCount struct {
Value string `json:"value"`
UsageCount int `json:"usage_count"`
}
var values []valCount
db.Model(&TemplateAttribute{}).
Select("value, count(template_id) as usage_count").
Where("attribute_id = ? AND value IS NOT NULL", id).
Group("value").
Order("usage_count DESC").
Find(&values)
c.JSON(200, gin.H{
"attribute_id": attr.ID,
"attribute_name": attr.Name,
"values": values,
"total_count": len(values),
})
}
// --- Standards ---
func getStandards(c *gin.Context, db *gorm.DB) {
var standards []Standard
db.Find(&standards)
c.JSON(200, gin.H{"standards": standards})
}
func getStandard(c *gin.Context, db *gorm.DB) {
id, ok := parseUUID(c, "id")
if !ok {
return
}
var s Standard
if db.First(&s, "id = ?", id).Error != nil {
c.JSON(200, gin.H{"standard": nil})
return
}
c.JSON(200, gin.H{"standard": s})
}
func createStandard(c *gin.Context, db *gorm.DB) {
var input struct {
Name string `json:"name"`
ProtocolName string `json:"protocol_name"`
RegistryNumber string `json:"registry_number"`
Range string `json:"range"`
Accuracy string `json:"accuracy"`
ValidUntil string `json:"valid_until"`
}
if c.BindJSON(&input) != nil {
return
}
validUntil, _ := time.Parse("2006-01-02", input.ValidUntil)
s := Standard{
Name: input.Name, ProtocolName: input.ProtocolName, RegistryNumber: input.RegistryNumber,
Range: input.Range, Accuracy: input.Accuracy, ValidUntil: validUntil,
}
db.Create(&s)
c.JSON(200, gin.H{"id": s.ID})
}
func patchStandard(c *gin.Context, db *gorm.DB) {
id, ok := parseUUID(c, "id")
if !ok {
return
}
var s Standard
if db.First(&s, "id = ?", id).Error != nil {
c.JSON(404, gin.H{"detail": "Standard not found"})
return
}
var raw map[string]interface{}
if c.BindJSON(&raw) != nil {
return
}
db.Model(&s).Updates(raw)
c.JSON(200, gin.H{"id": s.ID})
}
func deleteStandard(c *gin.Context, db *gorm.DB) {
id, ok := parseUUID(c, "id")
if !ok {
return
}
res := db.Delete(&Standard{}, "id = ?", id)
if res.RowsAffected == 0 {
c.JSON(404, gin.H{"detail": "Standard not found"})
return
}
c.JSON(200, gin.H{"message": "Standard deleted successfully"})
}
// --- Laboratories ---
func getLaboratories(c *gin.Context, db *gorm.DB) {
var labs []Laboratory
db.Preload("ConditionTypes").Find(&labs)
c.JSON(200, gin.H{"laboratories": labs})
}
func getLaboratory(c *gin.Context, db *gorm.DB) {
id, ok := parseUUID(c, "id")
if !ok {
return
}
var lab Laboratory
if db.Preload("ConditionTypes").First(&lab, "id = ?", id).Error != nil {
c.JSON(200, gin.H{"laboratory": nil})
return
}
c.JSON(200, gin.H{"laboratory": lab})
}
// --- Daily Conditions ---
func getDailyConditions(c *gin.Context, db *gorm.DB) {
q := db.Model(&DailyCondition{})
if lid := c.Query("laboratory_id"); lid != "" {
q = q.Where("laboratory_id = ?", lid)
}
if sd := c.Query("start_date"); sd != "" {
q = q.Where("measurement_date >= ?", sd)
}
if ed := c.Query("end_date"); ed != "" {
q = q.Where("measurement_date <= ?", ed)
}
var dcs []DailyCondition
q.Find(&dcs)
c.JSON(200, gin.H{"daily_conditions": dcs})
}
func getDailyCondition(c *gin.Context, db *gorm.DB) {
id, ok := parseUUID(c, "id")
if !ok {
return
}
var dc DailyCondition
if db.First(&dc, "id = ?", id).Error != nil {
c.JSON(200, gin.H{"daily_condition": nil})
return
}
c.JSON(200, gin.H{"daily_condition": dc})
}
func createDailyCondition(c *gin.Context, db *gorm.DB) {
var input struct {
LaboratoryID uuid.UUID `json:"laboratory_id"`
MeasurementDate string `json:"measurement_date"`
Conditions json.RawMessage `json:"conditions"`
}
if c.BindJSON(&input) != nil {
return
}
var lab Laboratory
if db.First(&lab, "id = ?", input.LaboratoryID).Error != nil {
c.JSON(404, gin.H{"detail": "Laboratory not found"})
return
}
md, _ := time.Parse("2006-01-02", input.MeasurementDate)
dc := DailyCondition{LaboratoryID: input.LaboratoryID, MeasurementDate: md, Conditions: JSON(input.Conditions)}
db.Create(&dc)
c.JSON(200, gin.H{"id": dc.ID})
}
func updateDailyCondition(c *gin.Context, db *gorm.DB) {
id, ok := parseUUID(c, "id")
if !ok {
return
}
var dc DailyCondition
if db.First(&dc, "id = ?", id).Error != nil {
c.JSON(404, gin.H{"detail": "Daily condition not found"})
return
}
var raw map[string]interface{}
if c.BindJSON(&raw) != nil {
return
}
db.Model(&dc).Updates(raw)
c.JSON(200, gin.H{"id": dc.ID})
}
func deleteDailyCondition(c *gin.Context, db *gorm.DB) {
id, ok := parseUUID(c, "id")
if !ok {
return
}
res := db.Delete(&DailyCondition{}, "id = ?", id)
if res.RowsAffected == 0 {
c.JSON(404, gin.H{"detail": "Daily condition not found"})
return
}
c.JSON(200, gin.H{"message": "Daily condition deleted successfully"})
}
// --- Groups ---
func getGroups(c *gin.Context, db *gorm.DB) {
var groups []Group
db.Order("\"order\", name").Find(&groups)
c.JSON(200, gin.H{"groups": groups})
}
func getGroup(c *gin.Context, db *gorm.DB) {
id, ok := parseUUID(c, "id")
if !ok {
return
}
var g Group
if db.First(&g, "id = ?", id).Error != nil {
c.JSON(200, gin.H{"group": nil})
return
}
c.JSON(200, gin.H{"group": g})
}
func createGroup(c *gin.Context, db *gorm.DB) {
var input struct {
Name string `json:"name"`
Order float64 `json:"order"`
IsVisible *bool `json:"is_visible"`
}
if c.BindJSON(&input) != nil {
return
}
isVisible := true
if input.IsVisible != nil {
isVisible = *input.IsVisible
}
g := Group{Name: input.Name, Order: input.Order, IsVisible: isVisible}
db.Create(&g)
c.JSON(200, gin.H{"id": g.ID})
}
func patchGroup(c *gin.Context, db *gorm.DB) {
id, ok := parseUUID(c, "id")
if !ok {
return
}
var g Group
if db.First(&g, "id = ?", id).Error != nil {
c.JSON(404, gin.H{"detail": "Group not found"})
return
}
var raw map[string]interface{}
if c.BindJSON(&raw) != nil {
return
}
db.Model(&g).Updates(raw)
db.First(&g, "id = ?", id) // reload
c.JSON(200, g)
}
func deleteGroup(c *gin.Context, db *gorm.DB) {
id, ok := parseUUID(c, "id")
if !ok {
return
}
res := db.Delete(&Group{}, "id = ?", id)
c.JSON(200, gin.H{"success": res.RowsAffected > 0})
}
// --- Protocols ---
func createProtocol(c *gin.Context, db *gorm.DB) {
var input struct {
TemplateID uuid.UUID `json:"template_id"`
FileID uuid.UUID `json:"file_id"`
CellsToUpdate map[string]interface{} `json:"cells_to_update"`
PrintArea string `json:"print_area"`
}
if c.BindJSON(&input) != nil {
return
}
var file File
if db.First(&file, "id = ?", input.FileID).Error != nil {
c.JSON(404, gin.H{"detail": "File not found"})
return
}
modified, err := FillCells(input.CellsToUpdate, file.Content, input.PrintArea)
if err != nil {
c.JSON(500, gin.H{"detail": fmt.Sprintf("Excel error: %v", err)})
return
}
protocolID := uuid.New()
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=protocol_%s.xlsx", protocolID))
c.Data(200, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", modified)
}