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
}
}
1 Answer 1
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.
- Anglophone
- Russophone
- 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.