Compare commits
4 Commits
6c95d944a1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ac80c029f | |||
| e9fa4f30e1 | |||
| a02209c698 | |||
| 6c3950953e |
@@ -12,11 +12,11 @@ Go-библиотека и консольная утилита для взаим
|
|||||||
* **Комплексный сбор данных:** Агрегирует полную информацию о ККТ, включая регистрационные данные, статус ФН, версии ПО, лицензии и атрибуты торговли.
|
* **Комплексный сбор данных:** Агрегирует полную информацию о ККТ, включая регистрационные данные, статус ФН, версии ПО, лицензии и атрибуты торговли.
|
||||||
* **Умный автопоиск устройств:**
|
* **Умный автопоиск устройств:**
|
||||||
* **COM-порты:** Автоматически сканирует все системные COM-порты на двух самых распространенных скоростях (`115200` и `4800`), предотвращая зависания на "портах-призраках".
|
* **COM-порты:** Автоматически сканирует все системные COM-порты на двух самых распространенных скоростях (`115200` и `4800`), предотвращая зависания на "портах-призраках".
|
||||||
* **TCP/IP (RNDIS):** Параллельно сканирует стандартные для RNDIS-устройств IP-подсети (`192.168.137.0/24`, `192.168.138.0/24`).
|
* **TCP/IP (RNDIS):** Cканирует стандартные для RNDIS-устройств IP-подсети (`192.168.137.0/24`, `192.168.138.0/24`).
|
||||||
* **Два режима работы утилиты:**
|
* **Два режима работы утилиты:**
|
||||||
1. **Режим автопоиска:** При первом запуске или отсутствии конфигурации выполняет полный поиск устройств, собирает с них данные и **сохраняет найденные конфигурации** в `connect.json` для последующих быстрых запусков.
|
1. **Режим автопоиска:** При первом запуске или отсутствии конфигурации выполняет полный поиск устройств, собирает с них данные и **сохраняет найденные конфигурации** в `connect.json` для последующих быстрых запусков.
|
||||||
2. **Стационарный режим:** При наличии файла `connect.json` использует заданные в нем параметры для быстрого опроса конкретных ККТ, пропуская этап сканирования.
|
2. **Стационарный режим:** При наличии файла `connect.json` использует заданные в нем параметры для быстрого опроса конкретных ККТ, пропуская этап сканирования.
|
||||||
* **Гибкое управление данными:**
|
* **Управление данными:**
|
||||||
* Сохраняет информацию о каждом ККТ в отдельный JSON-файл (`/date/{ЗН_ККТ}.json`).
|
* Сохраняет информацию о каждом ККТ в отдельный JSON-файл (`/date/{ЗН_ККТ}.json`).
|
||||||
* "Обогащает" данные ККТ информацией о рабочей станции (hostname, TeamViewer ID и т.д.), заимствуя ее из существующих JSON-файлов в папке `/date`.
|
* "Обогащает" данные ККТ информацией о рабочей станции (hostname, TeamViewer ID и т.д.), заимствуя ее из существующих JSON-файлов в папке `/date`.
|
||||||
* Автоматически обновляет временные метки (`current_time`, `v_time`) в существующих файлах при повторных опросах.
|
* Автоматически обновляет временные метки (`current_time`, `v_time`) в существующих файлах при повторных опросах.
|
||||||
@@ -36,8 +36,8 @@ Go-библиотека и консольная утилита для взаим
|
|||||||
|
|
||||||
### Требования
|
### Требования
|
||||||
|
|
||||||
1. **Go:** Версия 1.18 или выше.
|
1. **Go:** Версия 1.23 или выше.
|
||||||
2. **ОС:** Windows (x86 или x64).
|
2. **Windows:** Поддержка Windows 7 и выше.
|
||||||
3. **32-битный (x86) тулчейн Go:** Даже на 64-битной системе для компиляции требуется 32-битный набор инструментов.
|
3. **32-битный (x86) тулчейн Go:** Даже на 64-битной системе для компиляции требуется 32-битный набор инструментов.
|
||||||
4. **Драйвер "Штрих-М":** На целевой машине должен быть установлен и зарегистрирован официальный драйвер от "Штрих-М" (например, `DrvFR_4.15_882.exe`).
|
4. **Драйвер "Штрих-М":** На целевой машине должен быть установлен и зарегистрирован официальный драйвер от "Штрих-М" (например, `DrvFR_4.15_882.exe`).
|
||||||
|
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module shtrih-kkt
|
module shtrih-kkt
|
||||||
|
|
||||||
go 1.23.4
|
go 1.17
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-ole/go-ole v1.3.0
|
github.com/go-ole/go-ole v1.3.0
|
||||||
|
|||||||
8
go.sum
8
go.sum
@@ -1,11 +1,17 @@
|
|||||||
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
|
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
|
||||||
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
|
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
|
go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
|
||||||
@@ -13,7 +19,9 @@ go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
|
|||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
395
main.go
395
main.go
@@ -7,7 +7,9 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"shtrih-kkt/pkg/shtrih"
|
"shtrih-kkt/pkg/shtrih"
|
||||||
@@ -18,22 +20,27 @@ import (
|
|||||||
const (
|
const (
|
||||||
configFileName = "connect.json"
|
configFileName = "connect.json"
|
||||||
serviceConfigName = "service.json"
|
serviceConfigName = "service.json"
|
||||||
outputDir = "date"
|
|
||||||
logsDir = "logs"
|
logsDir = "logs"
|
||||||
comSearchTimeout = 200 * time.Millisecond // Уменьшенный таймаут
|
comSearchTimeout = 200 * time.Millisecond
|
||||||
tcpSearchTimeout = 150 * time.Millisecond
|
tcpSearchTimeout = 200 * time.Millisecond
|
||||||
|
)
|
||||||
|
|
||||||
|
// Глобальная переменная для пути вывода. Это позволяет подменять ее в тестах.
|
||||||
|
var (
|
||||||
|
outputDir = "date"
|
||||||
|
version = "0.1.5-dev"
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- СТРУКТУРЫ ДЛЯ ПАРСИНГА КОНФИГУРАЦИОННЫХ ФАЙЛОВ ---
|
// --- СТРУКТУРЫ ДЛЯ ПАРСИНГА КОНФИГУРАЦИОННЫХ ФАЙЛОВ ---
|
||||||
|
|
||||||
// ConfigFile соответствует структуре файла connect.json
|
// ConfigFile используется для чтения секции "shtrih" из connect.json.
|
||||||
|
// Остальные секции файла игнорируются при чтении, но сохраняются при записи.
|
||||||
type ConfigFile struct {
|
type ConfigFile struct {
|
||||||
Timeout int `json:"timeout_to_ip_port"`
|
// Если ключ "shtrih" в JSON отсутствует, это поле будет nil.
|
||||||
Shtrih []ConnectionSettings `json:"shtrih"`
|
// Если ключ есть, но массив пуст (shtrih: []), поле будет пустым срезом.
|
||||||
Atol []interface{} `json:"atol"`
|
Shtrih []ConnectionSettings `json:"shtrih"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConnectionSettings описывает один блок настроек подключения для Штрих-М
|
|
||||||
type ConnectionSettings struct {
|
type ConnectionSettings struct {
|
||||||
TypeConnect int32 `json:"type_connect"`
|
TypeConnect int32 `json:"type_connect"`
|
||||||
ComPort string `json:"com_port"`
|
ComPort string `json:"com_port"`
|
||||||
@@ -42,19 +49,16 @@ type ConnectionSettings struct {
|
|||||||
IPPort string `json:"ip_port"`
|
IPPort string `json:"ip_port"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceFile используется для чтения настроек логирования из service.json
|
|
||||||
type ServiceFile struct {
|
type ServiceFile struct {
|
||||||
Service ServiceConfig `json:"service"`
|
Service ServiceConfig `json:"service"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceConfig содержит параметры логирования
|
|
||||||
type ServiceConfig struct {
|
type ServiceConfig struct {
|
||||||
LogLevel string `json:"log_level"`
|
LogLevel string `json:"log_level"`
|
||||||
LogDays int `json:"log_days"`
|
LogDays int `json:"log_days"`
|
||||||
|
UpdateURL string `json:"update_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PolledDevice связывает конфигурацию, использованную для подключения,
|
|
||||||
// с фискальной информацией, полученной от устройства.
|
|
||||||
type PolledDevice struct {
|
type PolledDevice struct {
|
||||||
Config shtrih.Config
|
Config shtrih.Config
|
||||||
Info *shtrih.FiscalInfo
|
Info *shtrih.FiscalInfo
|
||||||
@@ -63,8 +67,20 @@ type PolledDevice struct {
|
|||||||
// --- ОСНОВНАЯ ЛОГИКА ПРИЛОЖЕНИЯ ---
|
// --- ОСНОВНАЯ ЛОГИКА ПРИЛОЖЕНИЯ ---
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
log.Println("Запуск приложения для сбора данных с ККТ Штрих-М...")
|
log.Printf("Запуск приложения для сбора данных с ККТ Штрих-М, версия: %s", version)
|
||||||
|
|
||||||
|
// Загружаем сервисную конфигурацию в самом начале.
|
||||||
|
serviceConfig := loadServiceConfig()
|
||||||
|
|
||||||
|
// Настраиваем файловое логирование.
|
||||||
|
setupLogger(serviceConfig)
|
||||||
|
|
||||||
|
// В фоне запускаем проверку обновлений, если URL указан.
|
||||||
|
if serviceConfig != nil {
|
||||||
|
go checkForUpdates(version, serviceConfig.UpdateURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Основная логика приложения.
|
||||||
configData, err := os.ReadFile(configFileName)
|
configData, err := os.ReadFile(configFileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@@ -81,27 +97,39 @@ func main() {
|
|||||||
log.Println("Работа приложения завершена.")
|
log.Println("Работа приложения завершена.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// setupLogger настраивает запись логов в файл для стационарного режима.
|
// loadServiceConfig читает и парсит service.json.
|
||||||
func setupLogger() {
|
// Возвращает конфигурацию или nil, если файл не найден или поврежден.
|
||||||
|
func loadServiceConfig() *ServiceConfig {
|
||||||
data, err := os.ReadFile(serviceConfigName)
|
data, err := os.ReadFile(serviceConfigName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Предупреждение: файл настроек '%s' не найден. Логирование продолжится в консоль.", serviceConfigName)
|
if !os.IsNotExist(err) {
|
||||||
return
|
log.Printf("Предупреждение: ошибка чтения файла '%s': %v.", serviceConfigName, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var serviceFile ServiceFile
|
var serviceFile ServiceFile
|
||||||
if err := json.Unmarshal(data, &serviceFile); err != nil {
|
if err := json.Unmarshal(data, &serviceFile); err != nil {
|
||||||
log.Printf("Предупреждение: не удалось прочитать настройки из '%s' (%v). Логирование продолжится в консоль.", serviceConfigName, err)
|
log.Printf("Предупреждение: не удалось прочитать настройки из '%s' (%v).", serviceConfigName, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &serviceFile.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupLogger(config *ServiceConfig) {
|
||||||
|
// setupLogger настраивает систему логирования на основе конфигурации.
|
||||||
|
// Если конфигурация не передана или повреждена, логирование продолжается в консоль.
|
||||||
|
// Создает директорию для логов и настраивает ротацию с использованием lumberjack.
|
||||||
|
if config == nil {
|
||||||
|
log.Printf("Предупреждение: файл настроек '%s' не найден или некорректен. Логирование продолжится в консоль.", serviceConfigName)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Устанавливаем значения по умолчанию, если в файле их нет
|
logDays := config.LogDays
|
||||||
logDays := serviceFile.Service.LogDays
|
|
||||||
if logDays <= 0 {
|
if logDays <= 0 {
|
||||||
logDays = 7
|
logDays = 7 // Значение по умолчанию
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаем папку для логов, если ее нет
|
|
||||||
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
||||||
log.Printf("Ошибка создания директории для логов '%s': %v. Логирование продолжится в консоль.", logsDir, err)
|
log.Printf("Ошибка создания директории для логов '%s': %v. Логирование продолжится в консоль.", logsDir, err)
|
||||||
return
|
return
|
||||||
@@ -109,34 +137,41 @@ func setupLogger() {
|
|||||||
|
|
||||||
logFilePath := filepath.Join(logsDir, "shtrih-scanner.log")
|
logFilePath := filepath.Join(logsDir, "shtrih-scanner.log")
|
||||||
|
|
||||||
// Настраиваем ротацию логов
|
|
||||||
lumberjackLogger := &lumberjack.Logger{
|
lumberjackLogger := &lumberjack.Logger{
|
||||||
Filename: logFilePath,
|
Filename: logFilePath,
|
||||||
MaxSize: 5, // мегабайты
|
MaxSize: 5,
|
||||||
MaxBackups: 10,
|
MaxBackups: 10,
|
||||||
MaxAge: logDays, // дни
|
MaxAge: logDays,
|
||||||
Compress: true, // сжимать старые файлы
|
Compress: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Устанавливаем вывод логов и в файл, и в консоль
|
|
||||||
log.SetOutput(io.MultiWriter(os.Stdout, lumberjackLogger))
|
log.SetOutput(io.MultiWriter(os.Stdout, lumberjackLogger))
|
||||||
log.Printf("Логирование настроено. Уровень: %s, ротация: %d дней. Файл: %s", serviceFile.Service.LogLevel, logDays, logFilePath)
|
log.Printf("Логирование настроено. Уровень: %s, ротация: %d дней. Файл: %s", config.LogLevel, logDays, logFilePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runConfigMode(data []byte) {
|
func runConfigMode(data []byte) {
|
||||||
// Первым делом настраиваем логирование для стационарного режима!
|
// runConfigMode запускает приложение в стационарном режиме с использованием
|
||||||
setupLogger()
|
// конфигурации из файла connect.json. Парсит настройки устройств и запускает
|
||||||
|
// процесс опроса ККТ. При ошибках парсинга переключается на режим автопоиска.
|
||||||
|
|
||||||
var configFile ConfigFile
|
var configFile ConfigFile
|
||||||
if err := json.Unmarshal(data, &configFile); err != nil {
|
if err := json.Unmarshal(data, &configFile); err != nil {
|
||||||
log.Printf("Ошибка парсинга JSON из '%s': %v. Переключаюсь на режим автопоиска.", configFileName, err)
|
log.Printf("Ошибка парсинга JSON из '%s': %v. Переключаюсь на режим автопоиска.", configFileName, err)
|
||||||
runDiscoveryMode() // В случае ошибки автопоиск будет логировать только в консоль
|
runDiscoveryMode()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем наличие секции shtrih в файле, но не пустоту массива.
|
||||||
|
// Пустой массив shtrih: [] является валидным состоянием.
|
||||||
|
if configFile.Shtrih == nil {
|
||||||
|
log.Printf("В файле '%s' отсутствует секция 'shtrih'. Переключаюсь на режим автопоиска.", configFileName)
|
||||||
|
runDiscoveryMode()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(configFile.Shtrih) == 0 {
|
if len(configFile.Shtrih) == 0 {
|
||||||
log.Printf("В файле '%s' не найдено настроек для 'shtrih'. Переключаюсь на режим автопоиска.", configFileName)
|
log.Println("Список устройств 'shtrih' в конфигурации пуст. Сканирование не требуется.")
|
||||||
runDiscoveryMode()
|
// Здесь можно завершить работу, так как опрашивать нечего.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,10 +182,14 @@ func runConfigMode(data []byte) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
processDevices(configs)
|
// Передаем конструктор реального драйвера shtrih.New
|
||||||
|
processDevices(configs, shtrih.New)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runDiscoveryMode() {
|
func runDiscoveryMode() {
|
||||||
|
// runDiscoveryMode запускает приложение в режиме автопоиска устройств.
|
||||||
|
// Выполняет сканирование COM-портов и TCP-сетей для обнаружения ККТ Штрих-М.
|
||||||
|
// При обнаружении устройств сохраняет их конфигурацию для последующих запусков.
|
||||||
configs, err := shtrih.SearchDevices(comSearchTimeout, tcpSearchTimeout)
|
configs, err := shtrih.SearchDevices(comSearchTimeout, tcpSearchTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Во время поиска устройств произошла ошибка: %v", err)
|
log.Printf("Во время поиска устройств произошла ошибка: %v", err)
|
||||||
@@ -158,22 +197,28 @@ func runDiscoveryMode() {
|
|||||||
|
|
||||||
if len(configs) == 0 {
|
if len(configs) == 0 {
|
||||||
log.Println("В ходе сканирования не найдено ни одного устройства Штрих-М.")
|
log.Println("В ходе сканирования не найдено ни одного устройства Штрих-М.")
|
||||||
return
|
// Сохраняем информацию об отсутствии устройств, чтобы не сканировать в следующий раз.
|
||||||
|
saveEmptyShtrihConfig()
|
||||||
|
return // Завершаем работу, так как устройств нет.
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Найдено %d устройств. Начинаю сбор информации...", len(configs))
|
log.Printf("Найдено %d устройств. Начинаю сбор информации...", len(configs))
|
||||||
polledDevices := processDevices(configs)
|
// Передаем конструктор реального драйвера shtrih.New
|
||||||
|
polledDevices := processDevices(configs, shtrih.New)
|
||||||
|
|
||||||
if len(polledDevices) > 0 {
|
if len(polledDevices) > 0 {
|
||||||
saveConfiguration(polledDevices)
|
saveConfiguration(polledDevices)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func processDevices(configs []shtrih.Config) []PolledDevice {
|
// processDevices принимает функцию-фабрику `newDriverFunc` для создания драйвера.
|
||||||
|
// Это позволяет подменять реальный драйвер на мок-драйвер в тестах.
|
||||||
|
func processDevices(configs []shtrih.Config, newDriverFunc func(shtrih.Config) shtrih.Driver) []PolledDevice {
|
||||||
var polledDevices []PolledDevice
|
var polledDevices []PolledDevice
|
||||||
for _, config := range configs {
|
for _, config := range configs {
|
||||||
log.Printf("--- Опрашиваю устройство: %+v ---", config)
|
log.Printf("--- Опрашиваю устройство: %+v ---", config)
|
||||||
driver := shtrih.New(config)
|
// Используем переданную функцию-фабрику для создания драйвера
|
||||||
|
driver := newDriverFunc(config)
|
||||||
|
|
||||||
if err := driver.Connect(); err != nil {
|
if err := driver.Connect(); err != nil {
|
||||||
log.Printf("Не удалось подключиться к устройству: %v", err)
|
log.Printf("Не удалось подключиться к устройству: %v", err)
|
||||||
@@ -203,36 +248,31 @@ func processDevices(configs []shtrih.Config) []PolledDevice {
|
|||||||
|
|
||||||
sourceWSDataMap := findSourceWorkstationData()
|
sourceWSDataMap := findSourceWorkstationData()
|
||||||
|
|
||||||
|
cleanupDateDirectory()
|
||||||
|
|
||||||
var successCount int
|
var successCount int
|
||||||
for _, pd := range polledDevices {
|
for _, pd := range polledDevices {
|
||||||
kktInfo := pd.Info
|
kktInfo := pd.Info
|
||||||
fileName := fmt.Sprintf("%s.json", kktInfo.SerialNumber)
|
fileName := fmt.Sprintf("%s.json", kktInfo.SerialNumber)
|
||||||
filePath := filepath.Join(outputDir, fileName)
|
filePath := filepath.Join(outputDir, fileName)
|
||||||
|
|
||||||
if _, err := os.Stat(filePath); err == nil {
|
// Определяем, какие данные о рабочей станции использовать.
|
||||||
log.Printf("Файл для ККТ %s уже существует. Обновляю временные метки...", kktInfo.SerialNumber)
|
var wsDataToUse map[string]interface{}
|
||||||
if err := updateTimestampInFile(filePath); err != nil {
|
if sourceWSDataMap != nil {
|
||||||
log.Printf("Не удалось обновить файл %s: %v", filePath, err)
|
log.Printf("Готовлю данные для ККТ %s, используя информацию из файла-донора.", kktInfo.SerialNumber)
|
||||||
} else {
|
wsDataToUse = sourceWSDataMap
|
||||||
log.Printf("Файл %s успешно обновлен.", filePath)
|
|
||||||
successCount++
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
var wsDataToUse map[string]interface{}
|
log.Printf("Готовлю данные для ККТ %s с базовой информацией о рабочей станции (донор не найден).", kktInfo.SerialNumber)
|
||||||
if sourceWSDataMap != nil {
|
hostname, _ := os.Hostname()
|
||||||
log.Printf("Создаю новый файл для ККТ %s, используя данные о рабочей станции из файла-донора.", kktInfo.SerialNumber)
|
wsDataToUse = map[string]interface{}{"hostname": hostname}
|
||||||
wsDataToUse = sourceWSDataMap
|
}
|
||||||
} else {
|
|
||||||
log.Printf("Создаю первичный файл для ККТ %s с базовыми данными о рабочей станции.", kktInfo.SerialNumber)
|
|
||||||
hostname, _ := os.Hostname()
|
|
||||||
wsDataToUse = map[string]interface{}{"hostname": hostname}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := saveNewMergedInfo(kktInfo, wsDataToUse, filePath); err != nil {
|
// Безусловно сохраняем/перезаписываем файл.
|
||||||
log.Printf("Не удалось создать файл для ККТ %s: %v", kktInfo.SerialNumber, err)
|
if err := saveNewMergedInfo(kktInfo, wsDataToUse, filePath); err != nil {
|
||||||
} else {
|
log.Printf("Не удалось создать/перезаписать файл для ККТ %s: %v", kktInfo.SerialNumber, err)
|
||||||
successCount++
|
} else {
|
||||||
}
|
// Логика в saveNewMergedInfo уже выводит сообщение об успехе.
|
||||||
|
successCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Printf("--- Обработка файлов завершена. Успешно создано/обновлено: %d файлов. ---", successCount)
|
log.Printf("--- Обработка файлов завершена. Успешно создано/обновлено: %d файлов. ---", successCount)
|
||||||
@@ -242,86 +282,156 @@ func processDevices(configs []shtrih.Config) []PolledDevice {
|
|||||||
|
|
||||||
// --- ФУНКЦИИ ДЛЯ РАБОТЫ С ФАЙЛАМИ ---
|
// --- ФУНКЦИИ ДЛЯ РАБОТЫ С ФАЙЛАМИ ---
|
||||||
|
|
||||||
|
// findSourceWorkstationData ищет в папке /date файл с данными о рабочей станции.
|
||||||
|
// Логика поиска:
|
||||||
|
// 1. Ищет "идеальный" донор: файл с "hostname", но без "modelName". Если находит - сразу возвращает его.
|
||||||
|
// 2. Если идеальный не найден, ищет "первый подходящий": любой файл с "hostname", даже если там есть "modelName".
|
||||||
func findSourceWorkstationData() map[string]interface{} {
|
func findSourceWorkstationData() map[string]interface{} {
|
||||||
files, err := os.ReadDir(outputDir)
|
files, err := os.ReadDir(outputDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var firstCandidate map[string]interface{} // Переменная для хранения "первого подходящего" кандидата
|
||||||
|
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
if !file.IsDir() && filepath.Ext(file.Name()) == ".json" {
|
if file.IsDir() || filepath.Ext(file.Name()) != ".json" {
|
||||||
filePath := filepath.Join(outputDir, file.Name())
|
continue
|
||||||
data, err := os.ReadFile(filePath)
|
}
|
||||||
if err != nil {
|
|
||||||
log.Printf("Предупреждение: не удалось прочитать файл-донор %s: %v", filePath, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var content map[string]interface{}
|
filePath := filepath.Join(outputDir, file.Name())
|
||||||
if err := json.Unmarshal(data, &content); err != nil {
|
data, err := os.ReadFile(filePath)
|
||||||
log.Printf("Предупреждение: не удалось распарсить JSON из файла-донора %s: %v", filePath, err)
|
if err != nil {
|
||||||
continue
|
log.Printf("Предупреждение: не удалось прочитать файл-донор %s: %v", filePath, err)
|
||||||
}
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
_, hasModelName := content["modelName"]
|
var content map[string]interface{}
|
||||||
_, hasHostname := content["hostname"]
|
if err := json.Unmarshal(data, &content); err != nil {
|
||||||
if !hasModelName && hasHostname {
|
log.Printf("Предупреждение: не удалось распарсить JSON из файла-донора %s: %v", filePath, err)
|
||||||
log.Printf("Найден файл-донор с данными о рабочей станции: %s", filePath)
|
continue
|
||||||
return content
|
}
|
||||||
}
|
|
||||||
|
// Проверяем наличие ключевых полей
|
||||||
|
_, hasModelName := content["modelName"]
|
||||||
|
_, hasHostname := content["hostname"]
|
||||||
|
|
||||||
|
// Если у файла нет hostname, он нам точно не интересен
|
||||||
|
if !hasHostname {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сценарий 1: Найден "идеальный" донор (без modelName)
|
||||||
|
if !hasModelName {
|
||||||
|
log.Printf("Найден идеальный файл-донор с данными о рабочей станции: %s", filePath)
|
||||||
|
return content // Сразу возвращаем его
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сценарий 2: Файл не идеальный, но подходит как кандидат (есть и hostname, и modelName)
|
||||||
|
// Сохраняем только самого первого кандидата из списка файлов.
|
||||||
|
if firstCandidate == nil {
|
||||||
|
firstCandidate = content
|
||||||
|
log.Printf("Найден файл-кандидат на роль донора (будет использован, если не найдется идеальный): %s", filePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// После проверки всех файлов, если мы так и не вернули идеального донора,
|
||||||
|
// используем первого подходящего кандидата, которого нашли.
|
||||||
|
if firstCandidate != nil {
|
||||||
|
log.Println("Идеальный донор не найден, используется первый подходящий файл-кандидат.")
|
||||||
|
return firstCandidate
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если мы дошли до сюда, значит не было найдено ни одного файла с полем "hostname".
|
||||||
log.Println("В папке /date не найдено файлов-доноров. Будут использованы базовые данные.")
|
log.Println("В папке /date не найдено файлов-доноров. Будут использованы базовые данные.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateTimestampInFile(filePath string) error {
|
// cleanupDateDirectory сканирует рабочую директорию и удаляет файлы,
|
||||||
data, err := os.ReadFile(filePath)
|
// имя которых (без расширения) содержит нечисловые символы.
|
||||||
|
// Это необходимо для очистки временных/донорских файлов перед записью актуальных данных.
|
||||||
|
func cleanupDateDirectory() {
|
||||||
|
log.Println("Запуск очистки рабочей директории от временных файлов...")
|
||||||
|
|
||||||
|
files, err := os.ReadDir(outputDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ошибка чтения файла: %w", err)
|
// Если директория еще не создана, это не ошибка. Просто выходим.
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
log.Printf("Директория '%s' не найдена, очистка не требуется.", outputDir)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("Ошибка чтения директории '%s' при очистке: %v", outputDir, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var content map[string]interface{}
|
// Регулярное выражение для проверки, что строка состоит только из цифр.
|
||||||
if err := json.Unmarshal(data, &content); err != nil {
|
isNumeric := regexp.MustCompile(`^[0-9]+$`).MatchString
|
||||||
return fmt.Errorf("ошибка парсинга JSON: %w", err)
|
deletedCount := 0
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if file.IsDir() || filepath.Ext(file.Name()) != ".json" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем имя файла без расширения .json
|
||||||
|
baseName := strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))
|
||||||
|
|
||||||
|
if !isNumeric(baseName) {
|
||||||
|
filePath := filepath.Join(outputDir, file.Name())
|
||||||
|
log.Printf("Обнаружен некорректный файл '%s'. Удаляю...", file.Name())
|
||||||
|
if err := os.Remove(filePath); err != nil {
|
||||||
|
log.Printf("Не удалось удалить файл '%s': %v", filePath, err)
|
||||||
|
} else {
|
||||||
|
log.Printf("Файл '%s' успешно удален.", file.Name())
|
||||||
|
deletedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем оба поля времени
|
if deletedCount > 0 {
|
||||||
currentTime := time.Now().Format("2006-01-02 15:04:05")
|
log.Printf("Очистка завершена. Удалено %d файлов.", deletedCount)
|
||||||
content["current_time"] = currentTime
|
} else {
|
||||||
content["v_time"] = currentTime
|
log.Println("Некорректных файлов для удаления не найдено.")
|
||||||
|
|
||||||
updatedData, err := json.MarshalIndent(content, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ошибка маршалинга JSON: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.WriteFile(filePath, updatedData, 0644)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// saveNewMergedInfo объединяет данные ККТ и данные рабочей станции (в виде map) и сохраняет в новый JSON-файл.
|
||||||
|
// Данные от ККТ имеют приоритет и перезаписывают одноименные поля из данных донора.
|
||||||
func saveNewMergedInfo(kktInfo *shtrih.FiscalInfo, wsData map[string]interface{}, filePath string) error {
|
func saveNewMergedInfo(kktInfo *shtrih.FiscalInfo, wsData map[string]interface{}, filePath string) error {
|
||||||
|
// Шаг 1: Преобразуем данные от нашего ККТ (Штрих) в map.
|
||||||
var kktMap map[string]interface{}
|
var kktMap map[string]interface{}
|
||||||
kktJSON, _ := json.Marshal(kktInfo)
|
kktJSON, _ := json.Marshal(kktInfo)
|
||||||
json.Unmarshal(kktJSON, &kktMap)
|
json.Unmarshal(kktJSON, &kktMap)
|
||||||
|
|
||||||
// Добавляем актуальные временные метки в данные рабочей станции
|
// Шаг 2: Создаем итоговую карту. Начинаем с данных донора, чтобы они были "внизу".
|
||||||
currentTime := time.Now().Format("2006-01-02 15:04:05")
|
// Мы делаем копию wsData, чтобы не изменять оригинальную карту, которая может быть использована в других итерациях.
|
||||||
wsData["current_time"] = currentTime
|
finalMap := make(map[string]interface{})
|
||||||
wsData["v_time"] = currentTime
|
|
||||||
|
|
||||||
for key, value := range wsData {
|
for key, value := range wsData {
|
||||||
kktMap[key] = value
|
finalMap[key] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(kktMap, "serialNumber")
|
// Шаг 3: "Накладываем" данные от нашего ККТ поверх.
|
||||||
kktMap["serialNumber"] = kktInfo.SerialNumber
|
// Все совпадающие ключи будут перезаписаны значениями от Штриха.
|
||||||
|
for key, value := range kktMap {
|
||||||
|
// Пропускаем пустые значения от ККТ, чтобы случайно не затереть
|
||||||
|
// хорошее значение из донора пустым.
|
||||||
|
if s, ok := value.(string); ok && s == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
finalMap[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Шаг 4: Устанавливаем актуальные временные метки. Они всегда должны быть свежими.
|
||||||
|
currentTime := time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
finalMap["current_time"] = currentTime
|
||||||
|
finalMap["v_time"] = currentTime
|
||||||
|
|
||||||
|
// Шаг 5: Создаем директорию и сохраняем файл.
|
||||||
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
|
||||||
return fmt.Errorf("не удалось создать директорию '%s': %w", outputDir, err)
|
return fmt.Errorf("не удалось создать директорию '%s': %w", outputDir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
finalJSON, err := json.MarshalIndent(kktMap, "", " ")
|
finalJSON, err := json.MarshalIndent(finalMap, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ошибка маршалинга итогового JSON: %w", err)
|
return fmt.Errorf("ошибка маршалинга итогового JSON: %w", err)
|
||||||
}
|
}
|
||||||
@@ -334,46 +444,99 @@ func saveNewMergedInfo(kktInfo *shtrih.FiscalInfo, wsData map[string]interface{}
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveConfiguration(polledDevices []PolledDevice) {
|
// saveEmptyShtrihConfig создает или обновляет connect.json, указывая,
|
||||||
log.Printf("Сохранение %d найденных конфигураций в файл '%s'...", len(polledDevices), configFileName)
|
// что устройства "Штрих-М" не были найдены. Функция работает неразрушающим
|
||||||
var configFile ConfigFile
|
// образом, сохраняя все остальные данные в файле.
|
||||||
|
func saveEmptyShtrihConfig() {
|
||||||
|
log.Printf("Сохраняю конфигурацию с пустым списком устройств Штрих-М в '%s'...", configFileName)
|
||||||
|
|
||||||
|
// Используем map[string]interface{} для редактирования JSON.
|
||||||
|
configMap := make(map[string]interface{})
|
||||||
|
|
||||||
|
// Пытаемся прочитать существующий файл, чтобы не затереть другие секции.
|
||||||
data, err := os.ReadFile(configFileName)
|
data, err := os.ReadFile(configFileName)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if err := json.Unmarshal(data, &configFile); err != nil {
|
// Если файл есть, парсим его в нашу карту.
|
||||||
|
if err := json.Unmarshal(data, &configMap); err != nil {
|
||||||
log.Printf("Предупреждение: файл '%s' поврежден (%v). Он будет перезаписан.", configFileName, err)
|
log.Printf("Предупреждение: файл '%s' поврежден (%v). Он будет перезаписан.", configFileName, err)
|
||||||
configFile = ConfigFile{}
|
// В случае ошибки парсинга, начинаем с пустой карты, чтобы исправить файл.
|
||||||
|
configMap = make(map[string]interface{})
|
||||||
}
|
}
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
// Логируем ошибку, если это не "файл не найден".
|
||||||
|
log.Printf("Предупреждение: не удалось прочитать '%s' (%v). Файл будет создан заново.", configFileName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Устанавливаем или обновляем только ключ 'shtrih'.
|
||||||
|
configMap["shtrih"] = []ConnectionSettings{}
|
||||||
|
|
||||||
|
// Маршалинг и запись обратно в файл.
|
||||||
|
updatedData, err := json.MarshalIndent(configMap, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Ошибка: не удалось преобразовать пустую конфигурацию в JSON: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(configFileName, updatedData, 0644); err != nil {
|
||||||
|
log.Printf("Ошибка: не удалось записать пустую конфигурацию в файл '%s': %v", configFileName, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("Файл '%s' успешно обновлен с отметкой об отсутствии устройств Штрих-М.", configFileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveConfiguration(polledDevices []PolledDevice) {
|
||||||
|
// saveConfiguration сохраняет конфигурацию найденных устройств в файл connect.json.
|
||||||
|
// Функция работает неразрушающим образом, сохраняя все остальные секции файла.
|
||||||
|
// Преобразует внутренние структуры shtrih.Config в формат ConnectionSettings для JSON.
|
||||||
|
log.Printf("Сохранение %d найденных конфигураций в файл '%s'...", len(polledDevices), configFileName)
|
||||||
|
|
||||||
|
// Используем map[string]interface{} для неразрушающего редактирования JSON.
|
||||||
|
configMap := make(map[string]interface{})
|
||||||
|
|
||||||
|
// Пытаемся прочитать существующий файл, чтобы не затереть другие секции.
|
||||||
|
data, err := os.ReadFile(configFileName)
|
||||||
|
if err == nil {
|
||||||
|
// Если файл есть, парсим его в нашу карту.
|
||||||
|
if err := json.Unmarshal(data, &configMap); err != nil {
|
||||||
|
log.Printf("Предупреждение: файл '%s' поврежден (%v). Он будет перезаписан.", configFileName, err)
|
||||||
|
// В случае ошибки парсинга, начинаем с пустой карты.
|
||||||
|
configMap = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
log.Printf("Предупреждение: не удалось прочитать '%s' (%v). Файл будет создан заново.", configFileName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Готовим новый срез с настройками для устройств Штрих-М.
|
||||||
var newShtrihSettings []ConnectionSettings
|
var newShtrihSettings []ConnectionSettings
|
||||||
for _, pd := range polledDevices {
|
for _, pd := range polledDevices {
|
||||||
newShtrihSettings = append(newShtrihSettings, convertConfigToSettings(pd.Config))
|
newShtrihSettings = append(newShtrihSettings, convertConfigToSettings(pd.Config))
|
||||||
}
|
}
|
||||||
|
|
||||||
configFile.Shtrih = newShtrihSettings
|
// Обновляем в карте только ключ 'shtrih'. Все остальные ключи остаются нетронутыми.
|
||||||
|
configMap["shtrih"] = newShtrihSettings
|
||||||
|
|
||||||
updatedData, err := json.MarshalIndent(configFile, "", " ")
|
// Маршалинг и запись обратно в файл.
|
||||||
|
updatedData, err := json.MarshalIndent(configMap, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Ошибка: не удалось преобразовать конфигурацию в JSON: %v", err)
|
log.Printf("Ошибка: не удалось преобразовать конфигурацию в JSON: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(configFileName, updatedData, 0644); err != nil {
|
if err := os.WriteFile(configFileName, updatedData, 0644); err != nil {
|
||||||
log.Printf("Ошибка: не удалось записать конфигурацию в файл '%s': %v", configFileName, err)
|
log.Printf("Ошибка: не удалось записать конфигурацию в файл '%s': %v", configFileName, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Конфигурация успешно сохранена в '%s'.", configFileName)
|
log.Printf("Конфигурация успешно сохранена в '%s'.", configFileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ---
|
// --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ---
|
||||||
|
|
||||||
func convertSettingsToConfigs(settings []ConnectionSettings) []shtrih.Config {
|
func convertSettingsToConfigs(settings []ConnectionSettings) []shtrih.Config {
|
||||||
|
// convertSettingsToConfigs преобразует настройки подключения из JSON-формата
|
||||||
|
// в внутренние структуры shtrih.Config. Выполняет валидацию параметров
|
||||||
|
// и пропускает некорректные конфигурации с соответствующим логированием.
|
||||||
var configs []shtrih.Config
|
var configs []shtrih.Config
|
||||||
baudRateMap := map[string]int32{
|
baudRateMap := map[string]int32{
|
||||||
"115200": 6, "57600": 5, "38400": 4, "19200": 3, "9600": 2, "4800": 1,
|
"115200": 6, "57600": 5, "38400": 4, "19200": 3, "9600": 2, "4800": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range settings {
|
for _, s := range settings {
|
||||||
config := shtrih.Config{ConnectionType: s.TypeConnect, Password: 30}
|
config := shtrih.Config{ConnectionType: s.TypeConnect, Password: 30}
|
||||||
switch s.TypeConnect {
|
switch s.TypeConnect {
|
||||||
@@ -412,9 +575,7 @@ func convertConfigToSettings(config shtrih.Config) ConnectionSettings {
|
|||||||
baudRateReverseMap := map[int32]string{
|
baudRateReverseMap := map[int32]string{
|
||||||
6: "115200", 5: "57600", 4: "38400", 3: "19200", 2: "9600", 1: "4800",
|
6: "115200", 5: "57600", 4: "38400", 3: "19200", 2: "9600", 1: "4800",
|
||||||
}
|
}
|
||||||
|
|
||||||
settings := ConnectionSettings{TypeConnect: config.ConnectionType}
|
settings := ConnectionSettings{TypeConnect: config.ConnectionType}
|
||||||
|
|
||||||
switch config.ConnectionType {
|
switch config.ConnectionType {
|
||||||
case 0:
|
case 0:
|
||||||
settings.ComPort = config.ComName
|
settings.ComPort = config.ComName
|
||||||
|
|||||||
187
main_test.go
Normal file
187
main_test.go
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"shtrih-kkt/pkg/shtrih"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// loadCanonicalKKTData загружает эталонные данные ККТ из файла для тестов.
|
||||||
|
// Путь к файлу указывается относительно корня проекта.
|
||||||
|
func loadCanonicalKKTData(t *testing.T, path string) *shtrih.FiscalInfo {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Критическая ошибка: не удалось прочитать канонический файл данных '%s': %v", path, err)
|
||||||
|
}
|
||||||
|
var info shtrih.FiscalInfo
|
||||||
|
if err := json.Unmarshal(data, &info); err != nil {
|
||||||
|
t.Fatalf("Критическая ошибка: не удалось распарсить JSON из канонического файла '%s': %v", path, err)
|
||||||
|
}
|
||||||
|
return &info
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestProcessDevices_NewFileFromDonor проверяет сценарий, когда для ККТ еще нет
|
||||||
|
// файла, но есть файл-донор с данными о рабочей станции.
|
||||||
|
func TestProcessDevices_NewFileFromDonor(t *testing.T) {
|
||||||
|
// --- Arrange (Подготовка) ---
|
||||||
|
|
||||||
|
// 1. Создаем временную директорию для теста, чтобы не засорять реальную папку `date`.
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalOutputDir := outputDir
|
||||||
|
outputDir = tempDir
|
||||||
|
defer func() { outputDir = originalOutputDir }()
|
||||||
|
|
||||||
|
// 2. Загружаем канонические данные для мок-драйвера из файла.
|
||||||
|
// Этот файл должен быть создан заранее и помещен в pkg/shtrih/testdata/
|
||||||
|
mockKKTData := loadCanonicalKKTData(t, "pkg/shtrih/testdata/canonical_kkt_data.json")
|
||||||
|
|
||||||
|
// 3. Создаем файл-донор с уникальными данными о рабочей станции во временной папке.
|
||||||
|
donorData := map[string]interface{}{
|
||||||
|
"hostname": "DONOR-PC",
|
||||||
|
"teamviewer_id": "999888777",
|
||||||
|
"vc": "3.0-donor-test",
|
||||||
|
}
|
||||||
|
donorBytes, _ := json.Marshal(donorData)
|
||||||
|
donorFilePath := filepath.Join(tempDir, "donor.json")
|
||||||
|
if err := os.WriteFile(donorFilePath, donorBytes, 0644); err != nil {
|
||||||
|
t.Fatalf("Не удалось создать тестовый файл-донор: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Создаем "фабрику", которая вернет мок-драйвер с нашими каноническими данными.
|
||||||
|
mockDriverFactory := func(config shtrih.Config) shtrih.Driver {
|
||||||
|
return shtrih.NewMockDriver(mockKKTData, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Готовим входные данные для processDevices.
|
||||||
|
testConfigs := []shtrih.Config{{ConnectionType: 6, IPAddress: "127.0.0.1"}}
|
||||||
|
|
||||||
|
// --- Act (Действие) ---
|
||||||
|
|
||||||
|
processDevices(testConfigs, mockDriverFactory)
|
||||||
|
|
||||||
|
// --- Assert (Проверка) ---
|
||||||
|
|
||||||
|
// 1. Проверяем, что был создан правильный JSON-файл.
|
||||||
|
expectedFileName := mockKKTData.SerialNumber + ".json"
|
||||||
|
resultFilePath := filepath.Join(tempDir, expectedFileName)
|
||||||
|
if _, err := os.Stat(resultFilePath); os.IsNotExist(err) {
|
||||||
|
t.Fatalf("Ожидалось, что будет создан файл '%s', но он не найден.", resultFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Читаем созданный файл и проверяем его содержимое.
|
||||||
|
resultBytes, err := os.ReadFile(resultFilePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Не удалось прочитать результирующий файл '%s': %v", resultFilePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resultMap map[string]interface{}
|
||||||
|
if err := json.Unmarshal(resultBytes, &resultMap); err != nil {
|
||||||
|
t.Fatalf("Не удалось распарсить JSON из результирующего файла: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Проверяем, что данные из ККТ и донора корректно слились.
|
||||||
|
// Проверка поля от ККТ.
|
||||||
|
if resultMap["modelName"] != mockKKTData.ModelName {
|
||||||
|
t.Errorf("Поле 'modelName' неверно. Ожидалось '%s', получено '%v'", mockKKTData.ModelName, resultMap["modelName"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка полей от донора.
|
||||||
|
if resultMap["hostname"] != donorData["hostname"] {
|
||||||
|
t.Errorf("Поле 'hostname' из донора не было добавлено. Ожидалось '%s', получено '%v'", donorData["hostname"], resultMap["hostname"])
|
||||||
|
}
|
||||||
|
if resultMap["vc"] != donorData["vc"] {
|
||||||
|
t.Errorf("Поле 'vc' из донора не было добавлено. Ожидалось '%s', получено '%v'", donorData["vc"], resultMap["vc"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка автоматически сгенерированных полей времени.
|
||||||
|
if _, ok := resultMap["current_time"]; !ok {
|
||||||
|
t.Error("Отсутствует обязательное поле 'current_time'.")
|
||||||
|
}
|
||||||
|
if _, ok := resultMap["v_time"]; !ok {
|
||||||
|
t.Error("Отсутствует обязательное поле 'v_time'.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFindSourceWorkstationData_FileHandling проверяет непосредственно логику
|
||||||
|
// поиска и чтения донор-файла в смоделированной файловой структуре.
|
||||||
|
func TestFindSourceWorkstationData_FileHandling(t *testing.T) {
|
||||||
|
|
||||||
|
// --- Сценарий 1: В папке /date есть правильный донор-файл ---
|
||||||
|
t.Run("when valid donor file exists", func(t *testing.T) {
|
||||||
|
// Arrange: Готовим файловую систему
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalOutputDir := outputDir
|
||||||
|
outputDir = tempDir // Указываем, что наша папка "date" находится во временной директории
|
||||||
|
defer func() { outputDir = originalOutputDir }()
|
||||||
|
|
||||||
|
// Создаем донор-файл с уникальными данными для проверки
|
||||||
|
donorData := map[string]interface{}{
|
||||||
|
"hostname": "REAL-DONOR-PC",
|
||||||
|
"vc": "v_from_real_file",
|
||||||
|
}
|
||||||
|
donorBytes, _ := json.Marshal(donorData)
|
||||||
|
if err := os.WriteFile(filepath.Join(tempDir, "donor_to_find.json"), donorBytes, 0644); err != nil {
|
||||||
|
t.Fatalf("Не удалось создать тестовый файл-донор: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем "файл-ловушку" от другого ККТ, который должен быть проигнорирован
|
||||||
|
kktTrapData := map[string]interface{}{
|
||||||
|
"modelName": "SOME-OTHER-KKT",
|
||||||
|
"serialNumber": "TRAP000001",
|
||||||
|
"hostname": "FAKE-HOSTNAME",
|
||||||
|
}
|
||||||
|
trapBytes, _ := json.Marshal(kktTrapData)
|
||||||
|
if err := os.WriteFile(filepath.Join(tempDir, "000001.json"), trapBytes, 0644); err != nil {
|
||||||
|
t.Fatalf("Не удалось создать файл-ловушку: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act: Вызываем тестируемую функцию
|
||||||
|
resultMap := findSourceWorkstationData()
|
||||||
|
|
||||||
|
// Assert: Проверяем результат
|
||||||
|
if resultMap == nil {
|
||||||
|
t.Fatal("findSourceWorkstationData() вернула nil, хотя ожидались данные из донора.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if resultMap["hostname"] != "REAL-DONOR-PC" {
|
||||||
|
t.Errorf("Hostname из донора прочитан неверно. Ожидалось 'REAL-DONOR-PC', получено '%v'", resultMap["hostname"])
|
||||||
|
}
|
||||||
|
if resultMap["vc"] != "v_from_real_file" {
|
||||||
|
t.Errorf("Поле 'vc' из донора прочитано неверно. Ожидалось 'v_from_real_file', получено '%v'", resultMap["vc"])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- Сценарий 2: В папке /date нет подходящих файлов ---
|
||||||
|
t.Run("when no valid donor file exists", func(t *testing.T) {
|
||||||
|
// Arrange: Готовим файловую систему
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalOutputDir := outputDir
|
||||||
|
outputDir = tempDir
|
||||||
|
defer func() { outputDir = originalOutputDir }()
|
||||||
|
|
||||||
|
// Создаем только "файл-ловушку", который не должен считаться донором
|
||||||
|
kktTrapData := map[string]interface{}{
|
||||||
|
"modelName": "SOME-OTHER-KKT",
|
||||||
|
"serialNumber": "TRAP000001",
|
||||||
|
"hostname": "FAKE-HOSTNAME",
|
||||||
|
}
|
||||||
|
trapBytes, _ := json.Marshal(kktTrapData)
|
||||||
|
if err := os.WriteFile(filepath.Join(tempDir, "000001.json"), trapBytes, 0644); err != nil {
|
||||||
|
t.Fatalf("Не удалось создать файл-ловушку: %v", err)
|
||||||
|
}
|
||||||
|
// Создаем текстовый файл, который тоже должен быть проигнорирован
|
||||||
|
if err := os.WriteFile(filepath.Join(tempDir, "readme.txt"), []byte("info"), 0644); err != nil {
|
||||||
|
t.Fatalf("Не удалось создать текстовый файл: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act: Вызываем тестируемую функцию
|
||||||
|
resultMap := findSourceWorkstationData()
|
||||||
|
|
||||||
|
// Assert: Проверяем, что функция ничего не нашла
|
||||||
|
if resultMap != nil {
|
||||||
|
t.Fatal("findSourceWorkstationData() вернула данные, хотя в папке не было валидных доноров.")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -37,22 +37,23 @@ type Config struct {
|
|||||||
|
|
||||||
// FiscalInfo содержит агрегированную информацию о фискальном регистраторе.
|
// FiscalInfo содержит агрегированную информацию о фискальном регистраторе.
|
||||||
type FiscalInfo struct {
|
type FiscalInfo struct {
|
||||||
ModelName string `json:"modelName"` // Наименование модели ККТ
|
ModelName string `json:"modelName"` // Наименование модели ККТ
|
||||||
SerialNumber string `json:"serialNumber"` // Заводской номер ККТ
|
SerialNumber string `json:"serialNumber"` // Заводской номер ККТ
|
||||||
RNM string `json:"RNM"` // Регистрационный номер машины (РНМ)
|
RNM string `json:"RNM"` // Регистрационный номер машины (РНМ)
|
||||||
OrganizationName string `json:"organizationName"` // Наименование организации пользователя
|
OrganizationName string `json:"organizationName"` // Наименование организации пользователя
|
||||||
Inn string `json:"INN"` // ИНН пользователя
|
Address string `json:"address"` // Адрес установки ККТ
|
||||||
FnSerial string `json:"fn_serial"` // Серийный номер фискального накопителя
|
Inn string `json:"INN"` // ИНН пользователя
|
||||||
RegistrationDate string `json:"datetime_reg"` // Дата и время регистрации ККТ
|
FnSerial string `json:"fn_serial"` // Серийный номер фискального накопителя
|
||||||
FnEndDate string `json:"dateTime_end"` // Дата окончания срока действия ФН
|
RegistrationDate string `json:"datetime_reg"` // Дата и время регистрации ККТ
|
||||||
OfdName string `json:"ofdName"` // Наименование ОФД
|
FnEndDate string `json:"dateTime_end"` // Дата окончания срока действия ФН
|
||||||
SoftwareDate string `json:"bootVersion"` // Версия (дата) прошивки ККТ
|
OfdName string `json:"ofdName"` // Наименование ОФД
|
||||||
FfdVersion string `json:"ffdVersion"` // Версия ФФД
|
SoftwareDate string `json:"bootVersion"` // Версия (дата) прошивки ККТ
|
||||||
FnExecution string `json:"fnExecution"` // Исполнение ФН
|
FfdVersion string `json:"ffdVersion"` // Версия ФФД
|
||||||
InstalledDriver string `json:"installed_driver"` // Версия установленного COM-драйвера
|
FnExecution string `json:"fnExecution"` // Исполнение ФН
|
||||||
AttributeExcise bool `json:"attribute_excise"` // Признак торговли подакцизными товарами
|
InstalledDriver string `json:"installed_driver"` // Версия установленного COM-драйвера
|
||||||
AttributeMarked bool `json:"attribute_marked"` // Признак торговли маркированными товарами
|
AttributeExcise bool `json:"attribute_excise"` // Признак торговли подакцизными товарами
|
||||||
LicensesRawHex string `json:"licenses,omitempty"` // Строка с лицензиями в HEX-формате
|
AttributeMarked bool `json:"attribute_marked"` // Признак торговли маркированными товарами
|
||||||
|
SubscriptionInfo string `json:"licenses,omitempty"` // Строка с лицензиями в расшифрованном виде
|
||||||
}
|
}
|
||||||
|
|
||||||
// Driver определяет основной интерфейс для работы с ККТ.
|
// Driver определяет основной интерфейс для работы с ККТ.
|
||||||
@@ -114,10 +115,11 @@ func (d *comDriver) Connect() error {
|
|||||||
// Установка свойств подключения в зависимости от типа.
|
// Установка свойств подключения в зависимости от типа.
|
||||||
oleutil.PutProperty(d.dispatch, "ConnectionType", d.config.ConnectionType)
|
oleutil.PutProperty(d.dispatch, "ConnectionType", d.config.ConnectionType)
|
||||||
oleutil.PutProperty(d.dispatch, "Password", d.config.Password)
|
oleutil.PutProperty(d.dispatch, "Password", d.config.Password)
|
||||||
if d.config.ConnectionType == 0 { // COM-порт
|
switch d.config.ConnectionType {
|
||||||
|
case 0: // COM-порт
|
||||||
oleutil.PutProperty(d.dispatch, "ComNumber", d.config.ComNumber)
|
oleutil.PutProperty(d.dispatch, "ComNumber", d.config.ComNumber)
|
||||||
oleutil.PutProperty(d.dispatch, "BaudRate", d.config.BaudRate)
|
oleutil.PutProperty(d.dispatch, "BaudRate", d.config.BaudRate)
|
||||||
} else if d.config.ConnectionType == 6 { // TCP/IP
|
case 6: // TCP/IP
|
||||||
oleutil.PutProperty(d.dispatch, "IPAddress", d.config.IPAddress)
|
oleutil.PutProperty(d.dispatch, "IPAddress", d.config.IPAddress)
|
||||||
oleutil.PutProperty(d.dispatch, "TCPPort", d.config.TCPPort)
|
oleutil.PutProperty(d.dispatch, "TCPPort", d.config.TCPPort)
|
||||||
oleutil.PutProperty(d.dispatch, "UseIPAddress", true)
|
oleutil.PutProperty(d.dispatch, "UseIPAddress", true)
|
||||||
@@ -203,10 +205,19 @@ func (d *comDriver) getBaseDeviceInfo(info *FiscalInfo) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Получаем и расшифровываем информацию о лицензии
|
||||||
if _, err := oleutil.CallMethod(d.dispatch, "ReadFeatureLicenses"); err == nil {
|
if _, err := oleutil.CallMethod(d.dispatch, "ReadFeatureLicenses"); err == nil {
|
||||||
if errCheck := d.checkError(); errCheck == nil {
|
if errCheck := d.checkError(); errCheck == nil {
|
||||||
info.LicensesRawHex, _ = d.getPropertyString("License")
|
hexLicense, _ := d.getPropertyString("License")
|
||||||
|
info.SubscriptionInfo = decodeLicense(hexLicense)
|
||||||
|
if info.SubscriptionInfo != "" {
|
||||||
|
log.Printf("Информация о лицензии успешно расшифрована: %s", info.SubscriptionInfo)
|
||||||
|
} else if hexLicense != "" {
|
||||||
|
log.Printf("Не удалось распознать формат полученной лицензии: %s", hexLicense)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("Предупреждение: команда ReadFeatureLicenses не выполнена, информация о лицензиях недоступна.")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -238,7 +249,7 @@ func (d *comDriver) getFiscalizationInfo(info *FiscalInfo) error {
|
|||||||
|
|
||||||
workMode, _ := d.getPropertyInt32("WorkMode")
|
workMode, _ := d.getPropertyInt32("WorkMode")
|
||||||
workModeEx, _ := d.getPropertyInt32("WorkModeEx")
|
workModeEx, _ := d.getPropertyInt32("WorkModeEx")
|
||||||
info.AttributeMarked = (workMode & 0x10) != 0 // Бит 4 - признак торговли маркированными товарами
|
info.AttributeMarked = (workMode & 0x10) != 0 // Бит 4 - признак торговли маркированными товарами
|
||||||
info.AttributeExcise = (workModeEx & 0x01) != 0 // Бит 0 - признак торговли подакцизными товарами
|
info.AttributeExcise = (workModeEx & 0x01) != 0 // Бит 0 - признак торговли подакцизными товарами
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -289,6 +300,10 @@ func (d *comDriver) getInfoFromTables(info *FiscalInfo) error {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
info.OfdName = strings.TrimSpace(ofdName)
|
info.OfdName = strings.TrimSpace(ofdName)
|
||||||
}
|
}
|
||||||
|
address, err := d.readTableField(18, 1, 9)
|
||||||
|
if err == nil {
|
||||||
|
info.Address = strings.TrimSpace(address)
|
||||||
|
}
|
||||||
|
|
||||||
// Версия ФФД хранится в виде кода: 2 - "1.05", 4 - "1.2"
|
// Версия ФФД хранится в виде кода: 2 - "1.05", 4 - "1.2"
|
||||||
ffdValueStr, err := d.readTableField(17, 1, 17)
|
ffdValueStr, err := d.readTableField(17, 1, 17)
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
package shtrih
|
package shtrih
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -127,3 +129,50 @@ func TestMockDriver_GetInfoError(t *testing.T) {
|
|||||||
t.Error("GetFiscalInfo() вернул данные вместе с ошибкой, хотя должен был вернуть nil.")
|
t.Error("GetFiscalInfo() вернул данные вместе с ошибкой, хотя должен был вернуть nil.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadMockDataFromFile читает "канонический" JSON-файл и преобразует его
|
||||||
|
// в структуру FiscalInfo для использования в мок-драйвере.
|
||||||
|
func loadMockDataFromFile(filePath string) (*FiscalInfo, error) {
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("не удалось прочитать файл с мок-данными '%s': %w", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var info FiscalInfo
|
||||||
|
if err := json.Unmarshal(data, &info); err != nil {
|
||||||
|
return nil, fmt.Errorf("не удалось распарсить JSON из файла '%s': %w", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMockDriver_WithDataFromFile проверяет полный цикл работы мок-драйвера,
|
||||||
|
// используя для этого данные, загруженные из реального JSON-файла.
|
||||||
|
func TestMockDriver_WithDataFromFile(t *testing.T) {
|
||||||
|
// Arrange 1: Загружаем данные из нашего "золотого" файла.
|
||||||
|
// Путь указывается относительно корня пакета.
|
||||||
|
canonicalData, err := loadMockDataFromFile("testdata/canonical_kkt_data.json")
|
||||||
|
if err != nil {
|
||||||
|
// Если файл не найден или поврежден, тест должен провалиться.
|
||||||
|
t.Fatalf("Подготовка теста провалилась: не удалось загрузить мок-данные: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arrange 2: Создаем мок-драйвер, передавая ему загруженные данные.
|
||||||
|
driver := NewMockDriver(canonicalData, nil, nil)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
if err := driver.Connect(); err != nil {
|
||||||
|
t.Fatalf("Connect() вернул неожиданную ошибку: %v", err)
|
||||||
|
}
|
||||||
|
defer driver.Disconnect()
|
||||||
|
|
||||||
|
info, err := driver.GetFiscalInfo()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetFiscalInfo() вернул неожиданную ошибку: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert: Сверяем, что драйвер вернул в точности те данные, что были в файле.
|
||||||
|
if !reflect.DeepEqual(info, canonicalData) {
|
||||||
|
t.Errorf("Данные, возвращенные мок-драйвером, не совпадают с данными из файла.\nПолучено: %+v\nОжидалось: %+v", info, canonicalData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
95
pkg/shtrih/license.go
Normal file
95
pkg/shtrih/license.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
// Файл: pkg/shtrih/license.go
|
||||||
|
package shtrih
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// licenseInfo хранит информацию о квартале и годе для конкретного суффикса лицензии.
|
||||||
|
type licenseInfo struct {
|
||||||
|
quarter string
|
||||||
|
year int
|
||||||
|
}
|
||||||
|
|
||||||
|
// licenseMap сопоставляет суффиксы HEX-лицензий с датой окончания подписки.
|
||||||
|
// Ключи должны быть в верхнем регистре.
|
||||||
|
var licenseMap = map[string]licenseInfo{
|
||||||
|
// 2027
|
||||||
|
"FFFFFFFF": {"4", 2027},
|
||||||
|
"FFFFFF7F": {"3", 2027},
|
||||||
|
"FFFFFF3F": {"2", 2027},
|
||||||
|
"FFFFFF1F": {"1", 2027},
|
||||||
|
// 2026
|
||||||
|
"FFFFFF0F": {"4", 2026},
|
||||||
|
"FFFFFF07": {"3", 2026},
|
||||||
|
"FFFFFF03": {"2", 2026},
|
||||||
|
"FFFFFF01": {"1", 2026},
|
||||||
|
// 2025
|
||||||
|
"FFFFFF00": {"4", 2025},
|
||||||
|
"FFFF7F00": {"3", 2025},
|
||||||
|
"FFFF3F00": {"2", 2025},
|
||||||
|
"FFFF1F00": {"1", 2025},
|
||||||
|
// 2024
|
||||||
|
"FFFF0F00": {"4", 2024},
|
||||||
|
"FFFF0700": {"3", 2024},
|
||||||
|
"FFFF0300": {"2", 2024},
|
||||||
|
"FFFF0100": {"1", 2024},
|
||||||
|
// 2023
|
||||||
|
"FFFF": {"4", 2023},
|
||||||
|
"FF7F": {"3", 2023},
|
||||||
|
"FF3F": {"2", 2023},
|
||||||
|
"FF1F": {"1", 2023},
|
||||||
|
// 2022
|
||||||
|
"FF0F": {"4", 2022},
|
||||||
|
"FF07": {"3", 2022},
|
||||||
|
"FF03": {"2", 2022},
|
||||||
|
"FF01": {"1", 2022},
|
||||||
|
// 2021
|
||||||
|
"FF00": {"4", 2021},
|
||||||
|
"7F00": {"3", 2021},
|
||||||
|
"3F00": {"2", 2021},
|
||||||
|
"1F00": {"1", 2021},
|
||||||
|
// 2020
|
||||||
|
"0F00": {"4", 2020},
|
||||||
|
"0700": {"3", 2020},
|
||||||
|
"0300": {"2", 2020},
|
||||||
|
"0100": {"1", 2020},
|
||||||
|
}
|
||||||
|
|
||||||
|
// sortedLicenseKeys хранит ключи из licenseMap, отсортированные по убыванию длины.
|
||||||
|
// Это необходимо, чтобы длинные суффиксы ("FFFFFFFF") проверялись раньше коротких ("FFFF").
|
||||||
|
var sortedLicenseKeys []string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
keys := make([]string, 0, len(licenseMap))
|
||||||
|
for k := range licenseMap {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Slice(keys, func(i, j int) bool {
|
||||||
|
return len(keys[i]) > len(keys[j])
|
||||||
|
})
|
||||||
|
sortedLicenseKeys = keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeLicense расшифровывает HEX-строку лицензии в человекочитаемый формат.
|
||||||
|
// Она ищет наиболее длинное известное вхождение кода лицензии внутри всей строки.
|
||||||
|
// Если лицензия не распознана, возвращает пустую строку.
|
||||||
|
func decodeLicense(hex string) string {
|
||||||
|
if hex == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
upperHex := strings.ToUpper(hex)
|
||||||
|
|
||||||
|
// Итерируемся по ключам, отсортированным от самого длинного к самому короткому.
|
||||||
|
for _, licenseCode := range sortedLicenseKeys {
|
||||||
|
// Проверяем, содержится ли код лицензии ГДЕ-ЛИБО в большой HEX-строке.
|
||||||
|
if strings.Contains(upperHex, licenseCode) {
|
||||||
|
info := licenseMap[licenseCode]
|
||||||
|
return fmt.Sprintf("Подписка до %s квартала %d года", info.quarter, info.year)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
41
updater.go
Normal file
41
updater.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// Файл: updater.go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
// В будущем здесь будет импорт библиотеки для автообновления, например:
|
||||||
|
// "github.com/minio/selfupdate"
|
||||||
|
)
|
||||||
|
|
||||||
|
// checkForUpdates — это функция-заглушка для механизма автообновления.
|
||||||
|
// Она будет выполняться в фоне, чтобы не блокировать основную работу утилиты.
|
||||||
|
func checkForUpdates(currentVersion string, updateURL string) {
|
||||||
|
if updateURL == "" {
|
||||||
|
// URL обновлений не указан в service.json, ничего не делаем.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if currentVersion == "0.0.0-dev" {
|
||||||
|
log.Println("Проверка обновлений пропущена: утилита запущена в режиме разработки.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Текущая версия: %s. Проверка обновлений по адресу: %s", currentVersion, updateURL)
|
||||||
|
|
||||||
|
// ===================================================================================
|
||||||
|
// ЗДЕСЬ БУДЕТ РЕАЛИЗОВАНА ЛОГИКА АВТООБНОВЛЕНИЯ
|
||||||
|
// -----------------------------------------------------------------------------------
|
||||||
|
// Примерный псевдокод с использованием библиотеки selfupdate:
|
||||||
|
//
|
||||||
|
// resp, err := http.Get(updateURL)
|
||||||
|
// if err != nil { ... }
|
||||||
|
// defer resp.Body.Close()
|
||||||
|
//
|
||||||
|
// err := selfupdate.Apply(resp.Body, selfupdate.Options{})
|
||||||
|
// if err != nil {
|
||||||
|
// // Обработка ошибок, возможно откат
|
||||||
|
// }
|
||||||
|
// log.Println("Приложение успешно обновлено!")
|
||||||
|
// ===================================================================================
|
||||||
|
|
||||||
|
log.Println("Функционал автообновления пока не реализован.")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user