Files
PriceForge/bible-local/patterns.md
2026-03-01 22:26:50 +03:00

198 lines
5.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Code Patterns
## CSV Export (3-layer architecture)
Used for pricelist export.
**Implementation**:
- Handler: `internal/handlers/pricelist.go``ExportCSV`
- Service: `internal/services/pricelist/service.go``StreamItemsForExport`
- Repository: `internal/repository/pricelist.go``StreamItemsForExport`
- Models: `internal/models/pricelist.go`
---
### 1. Handler Layer
```go
func (h *PricelistHandler) ExportCSV(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
pl, _ := h.service.GetByID(uint(id))
filename := fmt.Sprintf("pricelist_%s.csv", pl.Version)
c.Header("Content-Type", "text/csv; charset=utf-8")
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
// UTF-8 BOM for Excel compatibility
c.Writer.Write([]byte{0xEF, 0xBB, 0xBF})
writer := csv.NewWriter(c.Writer)
writer.Comma = ';' // semicolon for Russian-locale Excel
defer writer.Flush()
isWarehouse := strings.ToLower(pl.Source) == "warehouse"
var header []string
if isWarehouse {
header = []string{"Article", "Category", "Description", "Available", "Partnumbers", "Price, $", "Settings"}
} else {
header = []string{"Article", "Category", "Description", "Price, $", "Settings"}
}
writer.Write(header)
h.service.StreamItemsForExport(uint(id), 500, func(items []models.PricelistItem) error {
for _, item := range items {
writer.Write(buildRow(item, isWarehouse))
}
writer.Flush() // flush after each batch
return nil
})
}
```
### 2. Service Layer
```go
func (s *Service) StreamItemsForExport(id uint, batchSize int, cb func([]models.PricelistItem) error) error {
return s.repo.StreamItemsForExport(id, batchSize, cb)
}
```
### 3. Repository Layer
```go
func (r *PricelistRepository) StreamItemsForExport(pricelistID uint, batchSize int, callback func([]models.PricelistItem) error) error {
offset := 0
for {
var items []models.PricelistItem
err := r.db.Table("qt_pricelist_items AS pi").
Select("pi.*, COALESCE(l.lot_description, '') AS lot_description").
Joins("LEFT JOIN lot AS l ON l.lot_name = pi.lot_name").
Where("pi.pricelist_id = ?", pricelistID).
Order("pi.lot_name").
Offset(offset).Limit(batchSize).
Scan(&items).Error
if err != nil || len(items) == 0 {
break
}
if isWarehouse {
r.enrichWarehouseItems(items) // adds qty, partnumbers
}
if err := callback(items); err != nil {
return err
}
if len(items) < batchSize {
break
}
offset += batchSize
}
return nil
}
```
---
## Streaming (avoid loading all into memory)
```go
// ❌ BAD:
var allItems []Item
db.Find(&allItems) // can OOM on millions of rows
// ✅ GOOD:
for offset := 0; ; offset += batchSize {
var batch []Item
db.Offset(offset).Limit(batchSize).Find(&batch)
processBatch(batch)
if len(batch) < batchSize {
break
}
}
```
**Recommended batch size**: 5001000.
---
## JOIN instead of N+1
```go
// ❌ N+1:
for _, item := range items {
description := getLotDescription(item.LotName) // N queries
}
// ✅ JOIN:
db.Table("items AS i").
Select("i.*, COALESCE(l.description, '') AS description").
Joins("LEFT JOIN lots AS l ON l.name = i.lot_name")
```
---
## UTF-8 BOM for Excel
```go
// Excel on Windows requires BOM for correct UTF-8 display
c.Writer.Write([]byte{0xEF, 0xBB, 0xBF})
```
---
## Semicolon delimiter for Excel (Russian locale)
```go
writer := csv.NewWriter(c.Writer)
writer.Comma = ';'
```
---
## Graceful error handling during streaming
```go
// Cannot return JSON error after streaming has started
if err != nil {
c.String(http.StatusInternalServerError, "Export failed: %v", err)
return
}
```
---
## Virtual Fields (GORM)
```go
type PricelistItem struct {
// Stored fields
ID uint `gorm:"primaryKey"`
LotName string `gorm:"size:255"`
Price float64 `gorm:"type:decimal(12,2)"`
// Stored category snapshot
LotCategory *string `gorm:"column:lot_category;size:50" json:"category,omitempty"`
// Virtual: populated via JOIN
LotDescription string `gorm:"-:migration" json:"lot_description,omitempty"`
// Virtual: populated programmatically
AvailableQty *float64 `gorm:"-" json:"available_qty,omitempty"`
Partnumbers []string `gorm:"-" json:"partnumbers,omitempty"`
}
```
- `gorm:"-:migration"` — no DB column, but mapped on SELECT.
- `gorm:"-"` — fully ignored in all DB operations.
---
## CSV Export Checklist
- [ ] HTTP headers: `Content-Type`, `Content-Disposition`
- [ ] UTF-8 BOM `{0xEF, 0xBB, 0xBF}`
- [ ] Semicolon delimiter `;` for Russian-locale Excel
- [ ] Streaming with batch processing (no full load into memory)
- [ ] JOIN to avoid N+1 queries
- [ ] `writer.Flush()` after each batch
- [ ] Graceful error handling (no JSON after streaming starts)
- [ ] Dynamic headers when needed
- [ ] Conditional enrichment (warehouse vs estimate)