-1
\$\begingroup\$

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
    }

}
\$\endgroup\$
0

1 Answer 1

0
\$\begingroup\$

obscure identifier

type FFF struct {

This holds a resultCode and a payload. It needs a comment describing the term from the Business Domain that it's talking about. Alternatively, consider deleting it, as it is unused.

mélange de langues

There are three kinds of users of this software.

  1. Anglophone
  2. Russophone
  3. bilingual

The developer comments are uniformly in Russian except where they reference an identifier, which I guess is OK.

OTOH a user of this software will always see e.g. "Запуск клиента", and will sometimes see a subset of the error messages. A user who speaks only Russian may come to rely on this software, and then be surprised by an error which they cannot explain and cannot even read.

Recommend you pick a single language for all user output.

reciprocal

This is the wrong name:

    requestRate = 5 * time.Second

The intent was for a timeout interval of 5000 msec. But the name is promising a rate of one request every 200 msec (5 Hz, five events per second).

race

This is regrettable:

    openChrome(url)
    time.Sleep(time.Second * 10)

Either we paused too little, or too long. Prefer to wait on or poll for a success event.

The enclosing loop suggests thattime.NewTicker( requestRate ) was supposed to be responsible for the pacing, rather than this ten-second pause down in a helper routine.


Overall, the code looks fine. Ship it.

\$\endgroup\$

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.