SafeDecimal Architecture¶
Архитектурный принцип¶
ЗАПРЕЩЕНО использовать decimal.Decimal в API response структурах и применять .Float64() костыли.
ОБЯЗАТЕЛЬНО использовать SafeDecimal для:
- ✅ Все финансовые поля в API responses (json тэги)
- ✅ Все денежные суммы, проценты, курсы, комиссии
- ✅ Config структуры с финансовыми лимитами
- ✅ Investment amounts, yields, balances
Технические преимущества¶
Умная JSON сериализация¶
type InvestmentResponse struct {
Amount SafeDecimal `json:"amount"`
// Малые суммы → numbers, большие → strings
}
Логика сериализации: - Малые суммы (в пределах JavaScript safe integer) → JSON number - Большие суммы (> 9,007,199,254,740,991) → JSON string
Frontend совместимость¶
- ✅ TypeScript ожидает numbers - получает numbers для малых сумм
- ✅ JavaScript безопасность - учитывает IEEE 754 ограничения
- ✅ Точность вычислений - использует
decimal.Decimalвнутри - ✅ Устранение костылей - никаких
.Float64()вызовов в коде
Использование в коде¶
Backend (Go)¶
import "github.com/aiseeq/saga/backend/shared/models"
// Создание SafeDecimal
amount := models.MustParseSafeDecimal("100.50")
balance := models.NewSafeDecimalFromFloat64(1000.0)
// Операции (возвращают SafeDecimal)
sum := amount.Add(balance)
result := amount.Multiply(models.NewSafeDecimalFromInt(2))
// API Response
type BalanceResponse struct {
Balance SafeDecimal `json:"balance"`
Invested SafeDecimal `json:"invested"`
Yield SafeDecimal `json:"yield"`
}
SmartJSONEncoder¶
Для legacy структур с decimal.Decimal система применяет автоматическое преобразование на HTTP response уровне:
// backend/shared/http/helpers.go
func SendSuccess(w http.ResponseWriter, data interface{}) {
// SmartJSONEncoder автоматически конвертирует decimal.Decimal → SafeDecimal
// ТОЛЬКО для API responses, без изменения внутренних структур
}
Категорически запрещено¶
// ❌ decimal.Decimal в API responses
type BadResponse struct {
Amount decimal.Decimal `json:"amount"` // НЕ сериализуется корректно!
}
// ❌ .Float64() костыли
amount := decimal.NewFromFloat(balance).Float64() // Потеря точности!
// ❌ float64 для денежных операций
var balance float64 = 1000.50 // IEEE 754 проблемы!
Правильные паттерны¶
// ✅ SafeDecimal везде для финансов
type InvestmentData struct {
Amount SafeDecimal `json:"amount"`
APY SafeDecimal `json:"apy"`
MinInvestment SafeDecimal `json:"min_investment"`
}
// ✅ Операции с SafeDecimal
func CalculateYield(amount SafeDecimal, apy SafeDecimal) SafeDecimal {
return amount.Multiply(apy).Divide(models.NewSafeDecimalFromInt(100))
}
// ✅ Валидация
func ValidateAmount(amount SafeDecimal) error {
if amount.LessThanOrEqual(models.NewSafeDecimalFromInt(0)) {
return errors.New("amount must be positive")
}
return nil
}
Система синхронизации Go ↔ PostgreSQL¶
SafeDecimal автоматически интегрируется с PostgreSQL через:
- Database mapping:
NUMERICтип в PostgreSQL → SafeDecimal в Go - Type generation:
make generate-typesсоздает TypeScript типы - API compatibility: SmartJSONEncoder обеспечивает корректную сериализацию
Пример миграции¶
-- PostgreSQL schema
CREATE TABLE investments (
id SERIAL PRIMARY KEY,
amount NUMERIC(20, 6) NOT NULL, -- 6 decimals для финансовой точности
apy NUMERIC(5, 2) NOT NULL -- 2 decimals для процентов
);
// Go model
type Investment struct {
ID int64 `json:"id"`
Amount SafeDecimal `json:"amount" db:"amount"`
APY SafeDecimal `json:"apy" db:"apy"`
}
// TypeScript (автогенерация)
interface Investment {
id: number;
amount: number | string; // Умная сериализация!
apy: number | string;
}
Финансовая точность¶
IEEE 754 ограничения¶
JavaScript/TypeScript используют IEEE 754 double-precision (64-bit): - Safe integer range: ±9,007,199,254,740,991 - Precision: ~15-17 decimal digits - Проблемы: 0.1 + 0.2 ≠ 0.3 в JavaScript
Решение SafeDecimal¶
- Arbitrary precision:
decimal.Decimalбиблиотека (Go) - Smart serialization: numbers для safe integers, strings для больших значений
- Frontend handling: TypeScript корректно обрабатывает оба типа
Тестирование¶
func TestSafeDecimalJSON(t *testing.T) {
// Малая сумма → number
small := models.NewSafeDecimalFromFloat64(100.50)
json := small.MarshalJSON()
assert.Equal(t, "100.50", string(json))
// Большая сумма → string
large := models.MustParseSafeDecimal("10000000000000000")
json = large.MarshalJSON()
assert.Equal(t, "\"10000000000000000\"", string(json))
}
Связанная документация¶
Best Practices¶
- Всегда используй SafeDecimal для любых финансовых данных
- Никогда не используй float64 для денег
- Избегай .Float64() конверсий - потеря точности
- Валидируй входные данные перед созданием SafeDecimal
- Тестируй граничные случаи (малые/большие суммы, отрицательные значения)
📋 Метаданные¶
Версия: 2.4.82
Обновлено: 2025-10-21
Статус: Published