2025-05-05 23:12:07 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"database/sql"
|
2025-05-07 00:25:34 +03:00
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
2025-05-05 23:12:07 +00:00
|
|
|
"flag"
|
|
|
|
|
"fmt"
|
|
|
|
|
"log"
|
|
|
|
|
"net/http"
|
|
|
|
|
|
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
2025-05-07 00:25:34 +03:00
|
|
|
"gitlab.com/digineat/go-broker-test/cmd/server/validator"
|
|
|
|
|
"gitlab.com/digineat/go-broker-test/internal/model"
|
2025-05-05 23:12:07 +00:00
|
|
|
)
|
|
|
|
|
|
2025-05-07 00:25:34 +03:00
|
|
|
// server holds dependencies for HTTP handlers.
|
|
|
|
|
type server struct {
|
|
|
|
|
db *sql.DB
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handlePostTrades handles POST /trades requests.
|
|
|
|
|
func (s *server) handlePostTrades() http.HandlerFunc {
|
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
trade := &model.Trade{}
|
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(trade); err != nil {
|
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := validator.ValidateTrade(trade); err != nil {
|
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
_, err := s.db.Exec("INSERT INTO trades_q (account, symbol, volume, open_price, close_price, side) VALUES (?, ?, ?, ?, ?, ?)",
|
|
|
|
|
trade.Account, trade.Symbol, trade.Volume, trade.OpenPrice, trade.ClosePrice, trade.Side)
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handleGetStats handles GET /stats/{acc} requests.
|
|
|
|
|
func (s *server) handleGetStats() http.HandlerFunc {
|
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
acc := r.PathValue("acc")
|
|
|
|
|
if acc == "" {
|
|
|
|
|
http.Error(w, "Account is required", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var accStats model.AccountStats
|
|
|
|
|
err := s.db.QueryRow("SELECT account, trades_count, profit, updated_at FROM account_stats WHERE account = ?", acc).
|
|
|
|
|
Scan(&accStats.Account, &accStats.TradesCount, &accStats.Profit, &accStats.UpdatedAt)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
emptyStats := model.AccountStats{
|
|
|
|
|
Account: acc,
|
|
|
|
|
TradesCount: 0,
|
|
|
|
|
Profit: 0,
|
|
|
|
|
}
|
|
|
|
|
if jsonErr := json.NewEncoder(w).Encode(emptyStats); jsonErr != nil {
|
|
|
|
|
http.Error(w, jsonErr.Error(), http.StatusInternalServerError)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
if err := json.NewEncoder(w).Encode(accStats); err != nil {
|
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handleGetHealthz handles GET /healthz requests.
|
|
|
|
|
func (s *server) handleGetHealthz() http.HandlerFunc {
|
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if err := s.db.Ping(); err != nil {
|
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
|
// Избегаем log.Fatalf в хендлерах для лучшей управляемости и тестируемости.
|
|
|
|
|
// Можно логировать ошибку стандартным log.Printf или другой системой логирования.
|
|
|
|
|
log.Printf("Health check: Failed to ping database: %v", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
// Можно добавить тело ответа "OK", если это требуется.
|
|
|
|
|
// _, _ = w.Write([]byte("OK"))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-05 23:12:07 +00:00
|
|
|
func main() {
|
|
|
|
|
// Command line flags
|
|
|
|
|
dbPath := flag.String("db", "data.db", "path to SQLite database")
|
|
|
|
|
listenAddr := flag.String("listen", "8080", "HTTP server listen address")
|
|
|
|
|
flag.Parse()
|
|
|
|
|
|
|
|
|
|
// Initialize database connection
|
|
|
|
|
db, err := sql.Open("sqlite3", *dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("Failed to open database: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer db.Close()
|
|
|
|
|
|
|
|
|
|
// Test database connection
|
|
|
|
|
if err := db.Ping(); err != nil {
|
|
|
|
|
log.Fatalf("Failed to ping database: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-07 00:25:34 +03:00
|
|
|
// Create server instance
|
|
|
|
|
app := &server{db: db}
|
|
|
|
|
|
2025-05-05 23:12:07 +00:00
|
|
|
// Initialize HTTP server
|
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
|
|
2025-05-07 00:25:34 +03:00
|
|
|
// Register handlers
|
|
|
|
|
mux.HandleFunc("POST /trades", app.handlePostTrades())
|
|
|
|
|
mux.HandleFunc("GET /stats/{acc}", app.handleGetStats())
|
|
|
|
|
mux.HandleFunc("GET /healthz", app.handleGetHealthz())
|
2025-05-05 23:12:07 +00:00
|
|
|
|
|
|
|
|
// Start server
|
|
|
|
|
serverAddr := fmt.Sprintf(":%s", *listenAddr)
|
|
|
|
|
log.Printf("Starting server on %s", serverAddr)
|
|
|
|
|
if err := http.ListenAndServe(serverAddr, mux); err != nil {
|
|
|
|
|
log.Fatalf("Server failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|