-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
 }
}
asked May 23 at 15:10
\$\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.

answered May 23 at 16:00
\$\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.