Add initial backend implementation
- Go module with Gin, GORM, JWT, excelize dependencies - Configuration loading from YAML with all settings - GORM models for users, categories, components, configurations, alerts - Repository layer for all entities - Services: auth (JWT), pricing (median/average/weighted), components, quotes, configurations, export (CSV/XLSX), alerts - Middleware: JWT auth, role-based access, CORS - HTTP handlers for all API endpoints - Main server with dependency injection and graceful shutdown Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
375
CLAUDE.md
Normal file
375
CLAUDE.md
Normal file
@@ -0,0 +1,375 @@
|
||||
# QuoteForge - Claude Code Instructions
|
||||
|
||||
## Project Overview
|
||||
|
||||
QuoteForge — корпоративный инструмент для конфигурирования серверов и формирования коммерческих предложений (КП). Приложение интегрируется с существующей базой данных RFQ_LOG.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Language:** Go 1.22+
|
||||
- **Web Framework:** Gin (github.com/gin-gonic/gin)
|
||||
- **ORM:** GORM (gorm.io/gorm)
|
||||
- **Database:** MariaDB 11 (existing database RFQ_LOG)
|
||||
- **Frontend:** HTML templates + htmx + Tailwind CSS (CDN)
|
||||
- **Excel Export:** excelize (github.com/xuri/excelize/v2)
|
||||
- **Auth:** JWT (github.com/golang-jwt/jwt/v5)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
quoteforge/
|
||||
├── cmd/
|
||||
│ ├── server/main.go # Main HTTP server
|
||||
│ ├── priceupdater/main.go # Cron job for price updates & alerts
|
||||
│ └── importer/main.go # Import metadata from lot table
|
||||
├── internal/
|
||||
│ ├── config/config.go # YAML config loading
|
||||
│ ├── models/ # GORM models
|
||||
│ ├── handlers/ # Gin HTTP handlers
|
||||
│ ├── services/ # Business logic
|
||||
│ ├── middleware/ # Auth, CORS, roles
|
||||
│ └── repository/ # Database queries
|
||||
├── web/
|
||||
│ ├── templates/ # Go HTML templates
|
||||
│ └── static/ # CSS, JS
|
||||
├── migrations/ # SQL migration files
|
||||
├── config.yaml
|
||||
└── go.mod
|
||||
```
|
||||
|
||||
## Existing Database Tables (READ-ONLY - DO NOT MODIFY)
|
||||
|
||||
These tables are used by other systems. Our app only reads from them:
|
||||
|
||||
```sql
|
||||
-- Component catalog
|
||||
CREATE TABLE lot (
|
||||
lot_name CHAR(255) PRIMARY KEY, -- e.g., "CPU_AMD_9654", "MB_INTEL_4.Sapphire_2S"
|
||||
lot_description VARCHAR(10000)
|
||||
);
|
||||
|
||||
-- Price history from suppliers
|
||||
CREATE TABLE lot_log (
|
||||
lot_log_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
lot CHAR(255) NOT NULL, -- FK → lot.lot_name
|
||||
supplier CHAR(255) NOT NULL, -- FK → supplier.supplier_name
|
||||
date DATE NOT NULL,
|
||||
price DOUBLE NOT NULL,
|
||||
quality CHAR(255),
|
||||
comments VARCHAR(15000),
|
||||
FOREIGN KEY (lot) REFERENCES lot(lot_name),
|
||||
FOREIGN KEY (supplier) REFERENCES supplier(supplier_name)
|
||||
);
|
||||
|
||||
-- Supplier catalog
|
||||
CREATE TABLE supplier (
|
||||
supplier_name CHAR(255) PRIMARY KEY,
|
||||
supplier_comment VARCHAR(10000)
|
||||
);
|
||||
```
|
||||
|
||||
## New Tables (prefix qt_)
|
||||
|
||||
QuoteForge creates these tables:
|
||||
|
||||
```sql
|
||||
-- Users
|
||||
CREATE TABLE qt_users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(100) UNIQUE NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
role ENUM('viewer', 'editor', 'pricing_admin', 'admin') DEFAULT 'viewer',
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Component metadata (extends lot table)
|
||||
CREATE TABLE qt_lot_metadata (
|
||||
lot_name CHAR(255) PRIMARY KEY,
|
||||
category_id INT,
|
||||
vendor VARCHAR(50), -- Parsed from lot_name: CPU_AMD_9654 → "AMD"
|
||||
model VARCHAR(100), -- Parsed: CPU_AMD_9654 → "9654"
|
||||
specs JSON,
|
||||
current_price DECIMAL(12,2),
|
||||
price_method ENUM('manual', 'median', 'average', 'weighted_median') DEFAULT 'median',
|
||||
price_period_days INT DEFAULT 90,
|
||||
price_updated_at TIMESTAMP,
|
||||
request_count INT DEFAULT 0,
|
||||
last_request_date DATE,
|
||||
popularity_score DECIMAL(10,4),
|
||||
FOREIGN KEY (lot_name) REFERENCES lot(lot_name)
|
||||
);
|
||||
|
||||
-- Categories
|
||||
CREATE TABLE qt_categories (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
code VARCHAR(20) UNIQUE NOT NULL, -- MB, CPU, MEM, GPU, SSD, HDD, RAID, NIC, HCA, HBA, DPU, PS
|
||||
name VARCHAR(100) NOT NULL,
|
||||
name_ru VARCHAR(100),
|
||||
display_order INT DEFAULT 0,
|
||||
is_required BOOLEAN DEFAULT FALSE
|
||||
);
|
||||
|
||||
-- Saved configurations
|
||||
CREATE TABLE qt_configurations (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
uuid VARCHAR(36) UNIQUE NOT NULL,
|
||||
user_id INT NOT NULL,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
items JSON NOT NULL, -- [{"lot_name": "CPU_AMD_9654", "quantity": 2, "unit_price": 11500}]
|
||||
total_price DECIMAL(12,2),
|
||||
notes TEXT,
|
||||
is_template BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES qt_users(id)
|
||||
);
|
||||
|
||||
-- Price overrides
|
||||
CREATE TABLE qt_price_overrides (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
lot_name CHAR(255) NOT NULL,
|
||||
price DECIMAL(12,2) NOT NULL,
|
||||
valid_from DATE NOT NULL,
|
||||
valid_until DATE,
|
||||
reason TEXT,
|
||||
created_by INT NOT NULL,
|
||||
FOREIGN KEY (lot_name) REFERENCES lot(lot_name)
|
||||
);
|
||||
|
||||
-- Alerts for pricing admins
|
||||
CREATE TABLE qt_pricing_alerts (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
lot_name CHAR(255) NOT NULL,
|
||||
alert_type ENUM('high_demand_stale_price', 'price_spike', 'price_drop', 'no_recent_quotes', 'trending_no_price') NOT NULL,
|
||||
severity ENUM('low', 'medium', 'high', 'critical') DEFAULT 'medium',
|
||||
message TEXT NOT NULL,
|
||||
details JSON,
|
||||
status ENUM('new', 'acknowledged', 'resolved', 'ignored') DEFAULT 'new',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Usage statistics
|
||||
CREATE TABLE qt_component_usage_stats (
|
||||
lot_name CHAR(255) PRIMARY KEY,
|
||||
quotes_total INT DEFAULT 0,
|
||||
quotes_last_30d INT DEFAULT 0,
|
||||
quotes_last_7d INT DEFAULT 0,
|
||||
total_quantity INT DEFAULT 0,
|
||||
total_revenue DECIMAL(14,2) DEFAULT 0,
|
||||
trend_direction ENUM('up', 'stable', 'down') DEFAULT 'stable',
|
||||
trend_percent DECIMAL(5,2) DEFAULT 0,
|
||||
last_used_at TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
## Key Business Logic
|
||||
|
||||
### 1. Part Number Parsing
|
||||
|
||||
Extract category, vendor, model from lot_name:
|
||||
```go
|
||||
// "CPU_AMD_9654" → category="CPU", vendor="AMD", model="9654"
|
||||
// "MB_INTEL_4.Sapphire_2S_32xDDR5" → category="MB", vendor="INTEL", model="4.Sapphire_2S_32xDDR5"
|
||||
// "MEM_DDR5_64G_5600" → category="MEM", vendor="DDR5", model="64G_5600"
|
||||
// "GPU_NV_RTX_4090_PCIe" → category="GPU", vendor="NV", model="RTX_4090_PCIe"
|
||||
|
||||
func ParsePartNumber(lotName string) (category, vendor, model string) {
|
||||
parts := strings.SplitN(lotName, "_", 3)
|
||||
if len(parts) >= 1 { category = parts[0] }
|
||||
if len(parts) >= 2 { vendor = parts[1] }
|
||||
if len(parts) >= 3 { model = parts[2] }
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Price Calculation Methods
|
||||
|
||||
```go
|
||||
// Median - simple median of prices in period
|
||||
func CalculateMedian(prices []float64) float64
|
||||
|
||||
// Average - arithmetic mean
|
||||
func CalculateAverage(prices []float64) float64
|
||||
|
||||
// Weighted Median - recent prices have higher weight (exponential decay)
|
||||
// weight = e^(-days_since_quote / decay_days)
|
||||
func CalculateWeightedMedian(prices []PricePoint, decayDays int) float64
|
||||
```
|
||||
|
||||
### 3. Price Freshness (color coding)
|
||||
|
||||
```go
|
||||
// Green: < 30 days AND >= 3 quotes
|
||||
// Yellow: 30-60 days OR 1-2 quotes
|
||||
// Orange: 60-90 days
|
||||
// Red: > 90 days OR no price
|
||||
|
||||
func GetPriceFreshness(daysSinceUpdate int, quoteCount int) string {
|
||||
if daysSinceUpdate < 30 && quoteCount >= 3 {
|
||||
return "fresh" // green
|
||||
} else if daysSinceUpdate < 60 {
|
||||
return "normal" // yellow
|
||||
} else if daysSinceUpdate < 90 {
|
||||
return "stale" // orange
|
||||
}
|
||||
return "critical" // red
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Component Sorting
|
||||
|
||||
Sort by: popularity + price freshness. Components without prices go to the bottom.
|
||||
|
||||
```go
|
||||
// Sort score = popularity_score * 10 + freshness_bonus - no_price_penalty
|
||||
// freshness_bonus: fresh=100, normal=50, stale=10, critical=0
|
||||
// no_price_penalty: -1000 if current_price is NULL or 0
|
||||
```
|
||||
|
||||
### 5. Alert Generation
|
||||
|
||||
Generate alerts when:
|
||||
- **high_demand_stale_price** (CRITICAL): >= 5 quotes/month AND price > 60 days old
|
||||
- **trending_no_price** (HIGH): trend_percent > 50% AND no price set
|
||||
- **price_spike** (MEDIUM): price increased > 20% from previous period
|
||||
- **no_recent_quotes** (MEDIUM): popular component, no supplier quotes > 90 days
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Auth
|
||||
```
|
||||
POST /api/auth/login → {"username", "password"} → {"token", "refresh_token"}
|
||||
POST /api/auth/logout
|
||||
POST /api/auth/refresh
|
||||
GET /api/auth/me → current user info
|
||||
```
|
||||
|
||||
### Components
|
||||
```
|
||||
GET /api/components → list with pagination
|
||||
GET /api/components?category=CPU&vendor=AMD → filtered
|
||||
GET /api/components/:lot_name → single component details
|
||||
GET /api/categories → category list
|
||||
```
|
||||
|
||||
### Quote Builder
|
||||
```
|
||||
POST /api/quote/validate → {"items": [...]} → {"valid": bool, "errors": [], "warnings": []}
|
||||
POST /api/quote/calculate → {"items": [...]} → {"items": [...], "total": 45000.00}
|
||||
```
|
||||
|
||||
### Export
|
||||
```
|
||||
POST /api/export/csv → {"items": [...], "name": "Config 1"} → CSV file
|
||||
POST /api/export/xlsx → {"items": [...], "name": "Config 1"} → XLSX file
|
||||
```
|
||||
|
||||
### Configurations
|
||||
```
|
||||
GET /api/configs → list user's configurations
|
||||
POST /api/configs → save new configuration
|
||||
GET /api/configs/:uuid → get by UUID
|
||||
PUT /api/configs/:uuid → update
|
||||
DELETE /api/configs/:uuid → delete
|
||||
GET /api/configs/:uuid/export → export as JSON
|
||||
POST /api/configs/import → import from JSON
|
||||
```
|
||||
|
||||
### Pricing Admin (requires role: pricing_admin or admin)
|
||||
```
|
||||
GET /admin/pricing/stats → dashboard stats
|
||||
GET /admin/pricing/components → components with pricing info
|
||||
GET /admin/pricing/components/:lot_name → component pricing details
|
||||
POST /admin/pricing/update → update price method/value
|
||||
POST /admin/pricing/recalculate-all → recalculate all prices
|
||||
|
||||
GET /admin/pricing/alerts → list alerts
|
||||
POST /admin/pricing/alerts/:id/acknowledge → mark as seen
|
||||
POST /admin/pricing/alerts/:id/resolve → mark as resolved
|
||||
POST /admin/pricing/alerts/:id/ignore → dismiss alert
|
||||
```
|
||||
|
||||
### htmx Partials
|
||||
```
|
||||
GET /partials/components?category=CPU&vendor=AMD → HTML fragment
|
||||
GET /partials/cart → cart HTML
|
||||
GET /partials/summary → price summary HTML
|
||||
```
|
||||
|
||||
## User Roles
|
||||
|
||||
| Role | Permissions |
|
||||
|------|-------------|
|
||||
| viewer | View components, create quotes, export |
|
||||
| editor | + save/load configurations |
|
||||
| pricing_admin | + manage prices, view alerts |
|
||||
| admin | + manage users |
|
||||
|
||||
## Frontend Guidelines
|
||||
|
||||
- **Mobile-first** design
|
||||
- Use **htmx** for interactivity (hx-get, hx-post, hx-target, hx-swap)
|
||||
- Use **Tailwind CSS** via CDN
|
||||
- Minimal custom JavaScript
|
||||
- Color scheme for price freshness:
|
||||
- `text-green-600 bg-green-50` - fresh
|
||||
- `text-yellow-600 bg-yellow-50` - normal
|
||||
- `text-orange-600 bg-orange-50` - stale
|
||||
- `text-red-600 bg-red-50` - critical
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Run development server
|
||||
go run ./cmd/server
|
||||
|
||||
# Run price updater (cron job)
|
||||
go run ./cmd/priceupdater
|
||||
|
||||
# Run importer (one-time setup)
|
||||
go run ./cmd/importer
|
||||
|
||||
# Build for production
|
||||
CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/quoteforge ./cmd/server
|
||||
|
||||
# Run tests
|
||||
go test ./...
|
||||
```
|
||||
|
||||
## Dependencies (go.mod)
|
||||
|
||||
```go
|
||||
module github.com/mchus/quoteforge
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/go-sql-driver/mysql v1.7.1
|
||||
gorm.io/gorm v1.25.5
|
||||
gorm.io/driver/mysql v1.5.2
|
||||
github.com/xuri/excelize/v2 v2.8.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/google/uuid v1.5.0
|
||||
golang.org/x/crypto v0.17.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
```
|
||||
|
||||
## Development Priorities
|
||||
|
||||
1. **Phase 1 (MVP):** Project setup, models, component API, basic UI, CSV export
|
||||
2. **Phase 2:** JWT auth with roles, pricing admin UI, all price methods
|
||||
3. **Phase 3:** Save/load configs, JSON import/export, XLSX export, cron jobs
|
||||
4. **Phase 4:** Usage stats, alerts system, dashboard
|
||||
5. **Phase 5:** Polish, tests, Docker, documentation
|
||||
|
||||
## Code Style
|
||||
|
||||
- Use standard Go formatting (gofmt)
|
||||
- Error handling: always check errors, wrap with context
|
||||
- Logging: use structured logging (slog or zerolog)
|
||||
- Comments: in Russian or English, be consistent
|
||||
- File naming: snake_case for files, PascalCase for types
|
||||
Reference in New Issue
Block a user