sorry for my English. I create paysystem. I want to structure the project based on https://github.com/golang-standards/project-layout. I would also like to hear general comments on the code. Thank you all:
My code main.go:
package main
import (
"encoding/json"
"errors"
"fmt"
"log"
"os"
"os/exec"
"strconv"
"sync"
"time"
"github.com/joho/godotenv"
"github.com/shopspring/decimal"
"github.com/valyala/fasthttp"
)
type Response struct {
ResultCode string `json:"resultCode"`
Payload []Operation `json:"payload"`
}
type FFF struct {
ResultCode string `json:"resultCode"`
Payload []any `json:"payload"`
}
type Operation struct {
ID string `json:"id"`
Type string `json:"type"`
Amount Amount `json:"amount"`
CreatedAt CreatedAt `json:"operationTime"`
}
type CreatedAt struct {
Milliseconds int64 `json:"milliseconds"`
}
type Amount struct {
Sum decimal.Decimal `json:"value"`
}
type Payment struct {
Text string `json:"text"`
Status string `json:"status"`
}
type Session struct {
Value string `json:"value"`
Status string `json:"status"`
}
const (
parallelism = 4
requestRate = 5 * time.Second
ResultOK = "OK"
ResultInsufficientPrivileges = "INSUFFICIENT_PRIVILEGES"
OpTypeCredit = "Credit"
SessionStatusOK = "OK"
StatusPaid = "paid"
StatusMade = "made"
StatusError = "error"
reset = "\033[0m"
red = "\033[31m"
green = "\033[32m"
yellow = "\033[33m"
)
var (
// Конфигурация для bank
bankAccount string
bankWUID string
bankCategory string
bankHost string
bankPath string
// Конфигурация для системы
systemKey string
systemHost string
systemPath string
client = &fasthttp.Client{MaxConnsPerHost: parallelism}
jobChan = make(chan struct{}, parallelism)
processedPaymentIDs sync.Map
)
// openChrome открывает указанный URL в Google Chrome.
func openChrome(url string) error {
if url == "" {
return fmt.Errorf("url must not be empty")
}
cmd := exec.Command("open", "-a", "Google Chrome", url)
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start command: %w", err)
}
// Если нужно дождаться завершения процесса, можно использовать cmd.Wait()
if err := cmd.Wait(); err != nil {
return fmt.Errorf("command execution failed: %w", err)
}
return nil
}
func FetchSessionID() (string, error) {
url := "https://www.tbank.ru/login"
openChrome(url)
time.Sleep(time.Second * 10)
getSessionID := exec.Command("bin/getSessionID")
output, err := getSessionID.Output()
if err != nil {
return "", fmt.Errorf("не удалось выполнить команду: %w", err)
}
var session Session
err = json.Unmarshal(output, &session)
if err != nil {
return "", fmt.Errorf("не удалось распарсить JSON: %w", err)
}
if session.Status != SessionStatusOK {
return "", errors.New("не удалось получить сессию: " + session.Value)
}
return session.Value, nil
}
func ProcessPayment(paidAt int64, sum decimal.Decimal) {
var uri fasthttp.URI
uri.SetScheme("https")
uri.SetHost(systemHost)
uri.SetPath(systemPath)
q := uri.QueryArgs()
q.Add("do", "pay_payment_v2")
q.Add("key", systemKey)
q.Add("paid_at", strconv.FormatInt(paidAt, 10))
q.Add("sum", sum.StringFixed(2))
req := fasthttp.AcquireRequest()
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(resp)
req.SetRequestURI(uri.String())
req.Header.SetMethod(fasthttp.MethodPost)
req.Header.SetContentType("application/x-www-form-urlencoded")
req.SetBody([]byte(q.String()))
if err := fasthttp.Do(req, resp); err != nil {
log.Println("Ошибка запроса:", err)
return
}
statusCode := resp.StatusCode()
if statusCode != fasthttp.StatusOK {
log.Printf("⚠️ HTTP %d: %s\n", statusCode, resp.Body())
return
}
var payment Payment
if err := json.Unmarshal(resp.Body(), &payment); err != nil {
log.Println("Ошибка декодирования JSON:", err)
return
}
file, err := os.OpenFile("payments.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatal(err)
}
defer file.Close()
logger := log.New(file, "", log.LstdFlags|log.Lshortfile)
details := fmt.Sprintf("SUM: %s DATE: %d", sum.StringFixed(2), paidAt)
switch payment.Status {
case StatusMade:
log.Println(green + payment.Text + reset)
logger.Println("[INFO]", payment.Text, details)
case StatusPaid:
log.Println(yellow + payment.Text + reset)
logger.Println("[WARN]", payment.Text, details)
case StatusError:
log.Println(red + payment.Text + reset)
logger.Println("[ERROR]", payment.Text, details)
default:
log.Println(red + "Unknown error" + reset)
logger.Println("[ERROR] Unknown error")
}
}
func fetchData() {
defer func() { <-jobChan }()
now := time.Now().UTC()
end := now.UnixMilli()
start := now.Add(-24 * time.Hour).UnixMilli()
var uri fasthttp.URI
uri.SetScheme("https")
uri.SetHost(bankHost)
uri.SetPath(bankPath)
BankSessionID, err := FetchSessionID()
if err != nil {
log.Printf("Ошибка получения сессии банка: %v", err)
}
q := uri.QueryArgs()
q.Add("start", strconv.FormatInt(start, 10))
q.Add("end", strconv.FormatInt(end, 10))
q.Add("account", bankAccount)
q.Add("spendingCategory", bankCategory)
q.Add("sessionid", BankSessionID)
q.Add("wuid", bankWUID)
req := fasthttp.AcquireRequest()
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(resp)
req.SetRequestURI(uri.String())
req.Header.SetMethod(fasthttp.MethodGet)
if err := client.DoTimeout(req, resp, requestRate); err != nil {
log.Println("Ошибка запроса:", err)
return
}
statusCode := resp.StatusCode()
if statusCode != fasthttp.StatusOK {
log.Printf("⚠️ HTTP %d: %s\n", statusCode, resp.Body())
return
}
var apiResponse Response
if err := json.Unmarshal(resp.Body(), &apiResponse); err != nil {
log.Println("Ошибка декодирования JSON:", err)
return
}
log.Println(apiResponse)
if apiResponse.ResultCode != ResultOK {
switch apiResponse.ResultCode {
case ResultInsufficientPrivileges:
log.Println("⚠️ Сессия устарела")
return
default:
log.Printf("⚠️ Не известная ошибка, код %s", apiResponse.ResultCode)
return
}
}
for _, op := range apiResponse.Payload {
if op.Type != OpTypeCredit {
continue
}
if _, exists := processedPaymentIDs.LoadOrStore(op.ID, struct{}{}); exists {
continue
}
paidAt := op.CreatedAt.Milliseconds / 1000
sum := op.Amount.Sum
go ProcessPayment(paidAt, sum)
}
}
func init() {
if err := godotenv.Load(); err != nil {
log.Fatal("Ошибка загрузки .env файла:", err)
}
bankAccount = os.Getenv("BANK_ACCOUNT")
bankWUID = os.Getenv("BANK_WUID")
bankCategory = os.Getenv("BANK_CATEGORY")
bankHost = os.Getenv("BANK_HOST")
bankPath = os.Getenv("BANK_PATH")
systemKey = os.Getenv("SYSTEM_KEY")
systemHost = os.Getenv("SYSTEM_HOST")
systemPath = os.Getenv("SYSTEM_PATH")
}
func main() {
ticker := time.NewTicker(requestRate)
defer ticker.Stop()
log.Println("🚀 Запуск клиента...")
for {
select {
case jobChan <- struct{}{}:
go fetchData()
default:
log.Println("⏳ Пропуск: все воркеры заняты")
time.Sleep(time.Second)
}
<-ticker.C
}
}