Compare commits
2 Commits
6fab3c0a0d
...
6c95d944a1
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c95d944a1 | |||
| 173d4c670c |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
|||||||
*.exe
|
*.exe
|
||||||
*.json
|
*.json
|
||||||
|
*.log
|
||||||
|
*.zip
|
||||||
193
README.md
Normal file
193
README.md
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
# Go-библиотека и утилита для ККТ "Штрих-М" (shtrih-kkt)
|
||||||
|
|
||||||
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
Go-библиотека и консольная утилита для взаимодействия с фискальными регистраторами (ККТ) "Штрих-М" через официальный 32-битный COM-драйвер. Проект разработан с фокусом на безопасный сбор данных в режиме "только чтение" и предоставляет гибкие режимы работы для различных сценариев использования.
|
||||||
|
|
||||||
|
**Важнейшее ограничение:** Приложения, использующие эту библиотеку, **должны быть скомпилированы для 32-битной архитектуры (`GOARCH=386`)** из-за зависимости от 32-битного COM-драйвера.
|
||||||
|
|
||||||
|
## Ключевые возможности
|
||||||
|
|
||||||
|
* **Надежная обертка над COM-драйвером:** Предоставляет безопасный и удобный Go-интерфейс для драйвера "Штрих-М".
|
||||||
|
* **Комплексный сбор данных:** Агрегирует полную информацию о ККТ, включая регистрационные данные, статус ФН, версии ПО, лицензии и атрибуты торговли.
|
||||||
|
* **Умный автопоиск устройств:**
|
||||||
|
* **COM-порты:** Автоматически сканирует все системные COM-порты на двух самых распространенных скоростях (`115200` и `4800`), предотвращая зависания на "портах-призраках".
|
||||||
|
* **TCP/IP (RNDIS):** Параллельно сканирует стандартные для RNDIS-устройств IP-подсети (`192.168.137.0/24`, `192.168.138.0/24`).
|
||||||
|
* **Два режима работы утилиты:**
|
||||||
|
1. **Режим автопоиска:** При первом запуске или отсутствии конфигурации выполняет полный поиск устройств, собирает с них данные и **сохраняет найденные конфигурации** в `connect.json` для последующих быстрых запусков.
|
||||||
|
2. **Стационарный режим:** При наличии файла `connect.json` использует заданные в нем параметры для быстрого опроса конкретных ККТ, пропуская этап сканирования.
|
||||||
|
* **Гибкое управление данными:**
|
||||||
|
* Сохраняет информацию о каждом ККТ в отдельный JSON-файл (`/date/{ЗН_ККТ}.json`).
|
||||||
|
* "Обогащает" данные ККТ информацией о рабочей станции (hostname, TeamViewer ID и т.д.), заимствуя ее из существующих JSON-файлов в папке `/date`.
|
||||||
|
* Автоматически обновляет временные метки (`current_time`, `v_time`) в существующих файлах при повторных опросах.
|
||||||
|
* **Файловое логирование с ротацией:** В стационарном режиме ведет подробный лог в папке `/logs`, настройки которого (срок хранения) задаются в файле `service.json`.
|
||||||
|
* **Тестируемость:** Включает в себя `mockDriver` для написания unit-тестов без необходимости подключения реального оборудования.
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
Библиотека построена на простом интерфейсе `Driver`, что позволяет легко подменять реализации:
|
||||||
|
|
||||||
|
* `comDriver`: Основная реализация для работы с реальным COM-драйвером.
|
||||||
|
* `mockDriver`: Имитационная реализация для unit-тестирования.
|
||||||
|
|
||||||
|
Это обеспечивает слабую связанность и позволяет тестировать логику приложений, использующих библиотеку, в изолированной среде.
|
||||||
|
|
||||||
|
## Начало работы
|
||||||
|
|
||||||
|
### Требования
|
||||||
|
|
||||||
|
1. **Go:** Версия 1.18 или выше.
|
||||||
|
2. **ОС:** Windows (x86 или x64).
|
||||||
|
3. **32-битный (x86) тулчейн Go:** Даже на 64-битной системе для компиляции требуется 32-битный набор инструментов.
|
||||||
|
4. **Драйвер "Штрих-М":** На целевой машине должен быть установлен и зарегистрирован официальный драйвер от "Штрих-М" (например, `DrvFR_4.15_882.exe`).
|
||||||
|
|
||||||
|
### Использование библиотеки в вашем проекте
|
||||||
|
|
||||||
|
1. **Добавьте библиотеку в ваш проект:**
|
||||||
|
```bash
|
||||||
|
go get github.com/your-username/shtrih-kkt/pkg/shtrih
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Пример использования:**
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"shtrih-kkt/pkg/shtrih"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Конфигурация для подключения по COM-порту
|
||||||
|
config := shtrih.Config{
|
||||||
|
ConnectionType: 0,
|
||||||
|
ComName: "COM3",
|
||||||
|
ComNumber: 3,
|
||||||
|
BaudRate: 6, // Индекс для 115200
|
||||||
|
Password: 30,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем новый драйвер
|
||||||
|
driver := shtrih.New(config)
|
||||||
|
|
||||||
|
// Подключаемся
|
||||||
|
if err := driver.Connect(); err != nil {
|
||||||
|
log.Fatalf("Ошибка подключения: %v", err)
|
||||||
|
}
|
||||||
|
// Гарантируем отключение в конце
|
||||||
|
defer driver.Disconnect()
|
||||||
|
|
||||||
|
// Получаем информацию
|
||||||
|
info, err := driver.GetFiscalInfo()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Ошибка получения информации: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Успешно получены данные для ККТ модели: %s\n", info.ModelName)
|
||||||
|
fmt.Printf("Серийный номер: %s\n", info.SerialNumber)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Использование готовой утилиты `shtrih-scanner.exe`
|
||||||
|
|
||||||
|
Утилита предназначена для запуска "рядом" с другими служебными файлами.
|
||||||
|
|
||||||
|
1. **Режим автопоиска (первый запуск):**
|
||||||
|
* Просто запустите `shtrih-scanner.exe`.
|
||||||
|
* Программа выполнит полный поиск устройств.
|
||||||
|
* В папке `/date` будут созданы JSON-файлы с данными для каждой найденной ККТ.
|
||||||
|
* Будет создан или перезаписан файл `connect.json` с параметрами найденных устройств.
|
||||||
|
|
||||||
|
2. **Стационарный режим (последующие запуски):**
|
||||||
|
* Убедитесь, что рядом с `shtrih-scanner.exe` лежит `connect.json`.
|
||||||
|
* Для настройки логирования создайте файл `service.json`.
|
||||||
|
* Запустите `shtrih-scanner.exe`.
|
||||||
|
* Программа быстро опросит устройства из `connect.json` и обновит временные метки в файлах в папке `/date`.
|
||||||
|
|
||||||
|
#### Конфигурационные файлы
|
||||||
|
|
||||||
|
* `connect.json` (генерируется автоматически):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"shtrih": [
|
||||||
|
{
|
||||||
|
"type_connect": 0,
|
||||||
|
"com_port": "COM1",
|
||||||
|
"com_baudrate": "115200"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type_connect": 6,
|
||||||
|
"ip": "192.168.137.111",
|
||||||
|
"ip_port": "7778"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
* `service.json` (создается вручную для настройки логов):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"service": {
|
||||||
|
"log_level": "info",
|
||||||
|
"log_days": 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Сборка проекта
|
||||||
|
|
||||||
|
**Важно:** Сборка должна производиться для архитектуры `386`.
|
||||||
|
|
||||||
|
Откройте терминал в корневой папке проекта.
|
||||||
|
|
||||||
|
**Для PowerShell:**
|
||||||
|
```powershell
|
||||||
|
$env:GOARCH="386"; $env:GOOS="windows"; go build -o shtrih-scanner.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
**Для CMD:**
|
||||||
|
```cmd
|
||||||
|
set GOARCH=386
|
||||||
|
set GOOS=windows
|
||||||
|
go build -o shtrih-scanner.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
**Сборка без консольного окна (для фоновой работы):**
|
||||||
|
```powershell
|
||||||
|
$env:GOARCH="386"; $env:GOOS="windows"; go build -ldflags="-H=windowsgui" -o shtrih-scanner.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## Тестирование
|
||||||
|
|
||||||
|
Для запуска unit-тестов, использующих `mockDriver`, выполните команду в корне проекта:
|
||||||
|
```bash
|
||||||
|
go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
shtrih-kkt/
|
||||||
|
├── go.mod # Файл модуля Go
|
||||||
|
├── main.go # Исходный код утилиты shtrih-scanner.exe
|
||||||
|
├── README.md # Этот файл
|
||||||
|
└── pkg/
|
||||||
|
└── shtrih/
|
||||||
|
├── driver.go # Основная логика библиотеки и реализация comDriver
|
||||||
|
├── mock_driver.go # Реализация mockDriver для тестов
|
||||||
|
└── driver_test.go # Unit-тесты для библиотеки
|
||||||
|
|
||||||
|
---
|
||||||
|
# Файлы, создаваемые во время работы:
|
||||||
|
shtrih-scanner.exe
|
||||||
|
connect.json
|
||||||
|
service.json
|
||||||
|
date/
|
||||||
|
│ └── 0012345678901234.json
|
||||||
|
logs/
|
||||||
|
└── shtrih-scanner.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
Этот проект распространяется под лицензией MIT. См. файл `LICENSE` для получения дополнительной информации.
|
||||||
1
go.mod
1
go.mod
@@ -5,6 +5,7 @@ go 1.23.4
|
|||||||
require (
|
require (
|
||||||
github.com/go-ole/go-ole v1.3.0
|
github.com/go-ole/go-ole v1.3.0
|
||||||
go.bug.st/serial v1.6.4
|
go.bug.st/serial v1.6.4
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -13,5 +13,7 @@ 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/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/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=
|
||||||
|
|||||||
227
main.go
227
main.go
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -10,15 +11,21 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"shtrih-kkt/pkg/shtrih"
|
"shtrih-kkt/pkg/shtrih"
|
||||||
|
|
||||||
|
"gopkg.in/natefinch/lumberjack.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
configFileName = "connect.json"
|
configFileName = "connect.json"
|
||||||
outputDir = "date"
|
serviceConfigName = "service.json"
|
||||||
comSearchTimeout = 220 * time.Millisecond
|
outputDir = "date"
|
||||||
tcpSearchTimeout = 300 * time.Millisecond
|
logsDir = "logs"
|
||||||
|
comSearchTimeout = 200 * time.Millisecond // Уменьшенный таймаут
|
||||||
|
tcpSearchTimeout = 150 * time.Millisecond
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// --- СТРУКТУРЫ ДЛЯ ПАРСИНГА КОНФИГУРАЦИОННЫХ ФАЙЛОВ ---
|
||||||
|
|
||||||
// ConfigFile соответствует структуре файла connect.json
|
// ConfigFile соответствует структуре файла connect.json
|
||||||
type ConfigFile struct {
|
type ConfigFile struct {
|
||||||
Timeout int `json:"timeout_to_ip_port"`
|
Timeout int `json:"timeout_to_ip_port"`
|
||||||
@@ -35,6 +42,17 @@ type ConnectionSettings struct {
|
|||||||
IPPort string `json:"ip_port"`
|
IPPort string `json:"ip_port"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ServiceFile используется для чтения настроек логирования из service.json
|
||||||
|
type ServiceFile struct {
|
||||||
|
Service ServiceConfig `json:"service"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceConfig содержит параметры логирования
|
||||||
|
type ServiceConfig struct {
|
||||||
|
LogLevel string `json:"log_level"`
|
||||||
|
LogDays int `json:"log_days"`
|
||||||
|
}
|
||||||
|
|
||||||
// PolledDevice связывает конфигурацию, использованную для подключения,
|
// PolledDevice связывает конфигурацию, использованную для подключения,
|
||||||
// с фискальной информацией, полученной от устройства.
|
// с фискальной информацией, полученной от устройства.
|
||||||
type PolledDevice struct {
|
type PolledDevice struct {
|
||||||
@@ -42,13 +60,15 @@ type PolledDevice struct {
|
|||||||
Info *shtrih.FiscalInfo
|
Info *shtrih.FiscalInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- ОСНОВНАЯ ЛОГИКА ПРИЛОЖЕНИЯ ---
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
log.Println("Запуск приложения для сбора данных с ККТ Штрих-М...")
|
log.Println("Запуск приложения для сбора данных с ККТ Штрих-М...")
|
||||||
|
|
||||||
configData, err := os.ReadFile(configFileName)
|
configData, err := os.ReadFile(configFileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
log.Printf("Файл конфигурации '%s' не найден. Запускаю режим автопоиска устройств...", configFileName)
|
log.Printf("Файл конфигурации '%s' не найден. Запускаю режим автопоиска...", configFileName)
|
||||||
runDiscoveryMode()
|
runDiscoveryMode()
|
||||||
} else {
|
} else {
|
||||||
log.Fatalf("Ошибка чтения файла конфигурации '%s': %v", configFileName, err)
|
log.Fatalf("Ошибка чтения файла конфигурации '%s': %v", configFileName, err)
|
||||||
@@ -61,11 +81,56 @@ func main() {
|
|||||||
log.Println("Работа приложения завершена.")
|
log.Println("Работа приложения завершена.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setupLogger настраивает запись логов в файл для стационарного режима.
|
||||||
|
func setupLogger() {
|
||||||
|
data, err := os.ReadFile(serviceConfigName)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Предупреждение: файл настроек '%s' не найден. Логирование продолжится в консоль.", serviceConfigName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var serviceFile ServiceFile
|
||||||
|
if err := json.Unmarshal(data, &serviceFile); err != nil {
|
||||||
|
log.Printf("Предупреждение: не удалось прочитать настройки из '%s' (%v). Логирование продолжится в консоль.", serviceConfigName, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем значения по умолчанию, если в файле их нет
|
||||||
|
logDays := serviceFile.Service.LogDays
|
||||||
|
if logDays <= 0 {
|
||||||
|
logDays = 7
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем папку для логов, если ее нет
|
||||||
|
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
||||||
|
log.Printf("Ошибка создания директории для логов '%s': %v. Логирование продолжится в консоль.", logsDir, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logFilePath := filepath.Join(logsDir, "shtrih-scanner.log")
|
||||||
|
|
||||||
|
// Настраиваем ротацию логов
|
||||||
|
lumberjackLogger := &lumberjack.Logger{
|
||||||
|
Filename: logFilePath,
|
||||||
|
MaxSize: 5, // мегабайты
|
||||||
|
MaxBackups: 10,
|
||||||
|
MaxAge: logDays, // дни
|
||||||
|
Compress: true, // сжимать старые файлы
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем вывод логов и в файл, и в консоль
|
||||||
|
log.SetOutput(io.MultiWriter(os.Stdout, lumberjackLogger))
|
||||||
|
log.Printf("Логирование настроено. Уровень: %s, ротация: %d дней. Файл: %s", serviceFile.Service.LogLevel, logDays, logFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
func runConfigMode(data []byte) {
|
func runConfigMode(data []byte) {
|
||||||
|
// Первым делом настраиваем логирование для стационарного режима!
|
||||||
|
setupLogger()
|
||||||
|
|
||||||
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,16 +164,13 @@ func runDiscoveryMode() {
|
|||||||
log.Printf("Найдено %d устройств. Начинаю сбор информации...", len(configs))
|
log.Printf("Найдено %d устройств. Начинаю сбор информации...", len(configs))
|
||||||
polledDevices := processDevices(configs)
|
polledDevices := processDevices(configs)
|
||||||
|
|
||||||
// Если были успешно опрошены какие-либо устройства, сохраняем их конфигурацию
|
|
||||||
if len(polledDevices) > 0 {
|
if len(polledDevices) > 0 {
|
||||||
saveConfiguration(polledDevices)
|
saveConfiguration(polledDevices)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// processDevices - основная функция, реализующая "умную" логику обновления и создания файлов.
|
|
||||||
// Теперь она возвращает срез успешно опрошенных устройств.
|
|
||||||
func processDevices(configs []shtrih.Config) []PolledDevice {
|
func processDevices(configs []shtrih.Config) []PolledDevice {
|
||||||
// Шаг 1: Сначала собираем информацию со всех найденных устройств.
|
var polledDevices []PolledDevice
|
||||||
var polledDevices []PolledDevice // Было: var freshKKTData []*shtrih.FiscalInfo
|
|
||||||
for _, config := range configs {
|
for _, config := range configs {
|
||||||
log.Printf("--- Опрашиваю устройство: %+v ---", config)
|
log.Printf("--- Опрашиваю устройство: %+v ---", config)
|
||||||
driver := shtrih.New(config)
|
driver := shtrih.New(config)
|
||||||
@@ -119,7 +181,7 @@ func processDevices(configs []shtrih.Config) []PolledDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
info, err := driver.GetFiscalInfo()
|
info, err := driver.GetFiscalInfo()
|
||||||
driver.Disconnect() // Отключаемся сразу после получения данных
|
driver.Disconnect()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Ошибка при получении фискальной информации: %v", err)
|
log.Printf("Ошибка при получении фискальной информации: %v", err)
|
||||||
@@ -129,30 +191,26 @@ func processDevices(configs []shtrih.Config) []PolledDevice {
|
|||||||
log.Println("Получена пустая информация или отсутствует серийный номер, данные проигнорированы.")
|
log.Println("Получена пустая информация или отсутствует серийный номер, данные проигнорированы.")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Сохраняем и конфигурацию, и результат
|
|
||||||
polledDevices = append(polledDevices, PolledDevice{Config: config, Info: info})
|
polledDevices = append(polledDevices, PolledDevice{Config: config, Info: info})
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(polledDevices) == 0 {
|
if len(polledDevices) == 0 {
|
||||||
log.Println("--- Не удалось собрать данные ни с одного устройства. Завершение. ---")
|
log.Println("--- Не удалось собрать данные ни с одного устройства. Завершение. ---")
|
||||||
return nil // Возвращаем nil, если ничего не найдено
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("--- Всего собрано данных с %d устройств. Начинаю обработку файлов. ---", len(polledDevices))
|
log.Printf("--- Всего собрано данных с %d устройств. Начинаю обработку файлов. ---", len(polledDevices))
|
||||||
|
|
||||||
// Шаг 2: Ищем "донора" данных о рабочей станции в папке /date.
|
|
||||||
sourceWSDataMap := findSourceWorkstationData()
|
sourceWSDataMap := findSourceWorkstationData()
|
||||||
|
|
||||||
// Шаг 3: Обрабатываем каждого "свежего" ККТ в соответствии с новой логикой.
|
|
||||||
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 {
|
if _, err := os.Stat(filePath); err == nil {
|
||||||
log.Printf("Файл для ККТ %s уже существует. Обновляю временную метку...", kktInfo.SerialNumber)
|
log.Printf("Файл для ККТ %s уже существует. Обновляю временные метки...", kktInfo.SerialNumber)
|
||||||
if err := updateTimestampInFile(filePath); err != nil {
|
if err := updateTimestampInFile(filePath); err != nil {
|
||||||
log.Printf("Не удалось обновить файл %s: %v", filePath, err)
|
log.Printf("Не удалось обновить файл %s: %v", filePath, err)
|
||||||
} else {
|
} else {
|
||||||
@@ -169,7 +227,7 @@ func processDevices(configs []shtrih.Config) []PolledDevice {
|
|||||||
hostname, _ := os.Hostname()
|
hostname, _ := os.Hostname()
|
||||||
wsDataToUse = map[string]interface{}{"hostname": hostname}
|
wsDataToUse = map[string]interface{}{"hostname": hostname}
|
||||||
}
|
}
|
||||||
wsDataToUse["current_time"] = time.Now().Format("2006-01-02 15:04:05")
|
|
||||||
if err := saveNewMergedInfo(kktInfo, wsDataToUse, filePath); err != nil {
|
if err := saveNewMergedInfo(kktInfo, wsDataToUse, filePath); err != nil {
|
||||||
log.Printf("Не удалось создать файл для ККТ %s: %v", kktInfo.SerialNumber, err)
|
log.Printf("Не удалось создать файл для ККТ %s: %v", kktInfo.SerialNumber, err)
|
||||||
} else {
|
} else {
|
||||||
@@ -179,10 +237,11 @@ func processDevices(configs []shtrih.Config) []PolledDevice {
|
|||||||
}
|
}
|
||||||
log.Printf("--- Обработка файлов завершена. Успешно создано/обновлено: %d файлов. ---", successCount)
|
log.Printf("--- Обработка файлов завершена. Успешно создано/обновлено: %d файлов. ---", successCount)
|
||||||
|
|
||||||
return polledDevices // Возвращаем результат
|
return polledDevices
|
||||||
}
|
}
|
||||||
// findSourceWorkstationData ищет в папке /date любой .json файл и извлекает из него
|
|
||||||
// все данные как `map[string]interface{}`.
|
// --- ФУНКЦИИ ДЛЯ РАБОТЫ С ФАЙЛАМИ ---
|
||||||
|
|
||||||
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 {
|
||||||
@@ -204,13 +263,11 @@ func findSourceWorkstationData() map[string]interface{} {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем, что это не файл от нашего ККТ (у него не должно быть поля modelName)
|
|
||||||
// и что у него есть hostname. Это делает выбор донора более надежным.
|
|
||||||
_, hasModelName := content["modelName"]
|
_, hasModelName := content["modelName"]
|
||||||
_, hasHostname := content["hostname"]
|
_, hasHostname := content["hostname"]
|
||||||
if !hasModelName && hasHostname {
|
if !hasModelName && hasHostname {
|
||||||
log.Printf("Найден файл-донор с данными о рабочей станции: %s", filePath)
|
log.Printf("Найден файл-донор с данными о рабочей станции: %s", filePath)
|
||||||
return content // Возвращаем все содержимое файла как карту.
|
return content
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,7 +276,6 @@ func findSourceWorkstationData() map[string]interface{} {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateTimestampInFile читает JSON-файл, обновляет в нем поле current_time и перезаписывает его.
|
|
||||||
func updateTimestampInFile(filePath string) error {
|
func updateTimestampInFile(filePath string) error {
|
||||||
data, err := os.ReadFile(filePath)
|
data, err := os.ReadFile(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -231,7 +287,10 @@ func updateTimestampInFile(filePath string) error {
|
|||||||
return fmt.Errorf("ошибка парсинга JSON: %w", err)
|
return fmt.Errorf("ошибка парсинга JSON: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
content["current_time"] = time.Now().Format("2006-01-02 15:04:05")
|
// Обновляем оба поля времени
|
||||||
|
currentTime := time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
content["current_time"] = currentTime
|
||||||
|
content["v_time"] = currentTime
|
||||||
|
|
||||||
updatedData, err := json.MarshalIndent(content, "", " ")
|
updatedData, err := json.MarshalIndent(content, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -241,22 +300,21 @@ func updateTimestampInFile(filePath string) error {
|
|||||||
return os.WriteFile(filePath, updatedData, 0644)
|
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 {
|
||||||
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)
|
||||||
|
|
||||||
// Сливаем карты. Ключи из wsData перезапишут любые совпадения в kktMap.
|
// Добавляем актуальные временные метки в данные рабочей станции
|
||||||
|
currentTime := time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
wsData["current_time"] = currentTime
|
||||||
|
wsData["v_time"] = currentTime
|
||||||
|
|
||||||
for key, value := range wsData {
|
for key, value := range wsData {
|
||||||
kktMap[key] = value
|
kktMap[key] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
// Удаляем поля, специфичные для ККТ, из данных донора, если они случайно туда попали.
|
|
||||||
// Это предотвратит запись, например, "serialNumber" от АТОЛ в файл Штриха.
|
|
||||||
delete(kktMap, "serialNumber")
|
delete(kktMap, "serialNumber")
|
||||||
|
|
||||||
// Возвращаем серийный номер нашего ККТ, который мы сохранили в структуре kktInfo.
|
|
||||||
kktMap["serialNumber"] = kktInfo.SerialNumber
|
kktMap["serialNumber"] = kktInfo.SerialNumber
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
|
||||||
@@ -276,20 +334,50 @@ func saveNewMergedInfo(kktInfo *shtrih.FiscalInfo, wsData map[string]interface{}
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertSettingsToConfigs преобразует настройки из файла в формат, понятный библиотеке.
|
func saveConfiguration(polledDevices []PolledDevice) {
|
||||||
|
log.Printf("Сохранение %d найденных конфигураций в файл '%s'...", len(polledDevices), configFileName)
|
||||||
|
var configFile ConfigFile
|
||||||
|
data, err := os.ReadFile(configFileName)
|
||||||
|
if err == nil {
|
||||||
|
if err := json.Unmarshal(data, &configFile); err != nil {
|
||||||
|
log.Printf("Предупреждение: файл '%s' поврежден (%v). Он будет перезаписан.", configFileName, err)
|
||||||
|
configFile = ConfigFile{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var newShtrihSettings []ConnectionSettings
|
||||||
|
for _, pd := range polledDevices {
|
||||||
|
newShtrihSettings = append(newShtrihSettings, convertConfigToSettings(pd.Config))
|
||||||
|
}
|
||||||
|
|
||||||
|
configFile.Shtrih = newShtrihSettings
|
||||||
|
|
||||||
|
updatedData, err := json.MarshalIndent(configFile, "", " ")
|
||||||
|
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 convertSettingsToConfigs(settings []ConnectionSettings) []shtrih.Config {
|
func convertSettingsToConfigs(settings []ConnectionSettings) []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,
|
"115200": 6, "57600": 5, "38400": 4, "19200": 3, "9600": 2, "4800": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range settings {
|
for _, s := range settings {
|
||||||
config := shtrih.Config{
|
config := shtrih.Config{ConnectionType: s.TypeConnect, Password: 30}
|
||||||
ConnectionType: s.TypeConnect,
|
|
||||||
Password: 30,
|
|
||||||
}
|
|
||||||
switch s.TypeConnect {
|
switch s.TypeConnect {
|
||||||
case 0: // COM-порт
|
case 0:
|
||||||
comNum, err := strconv.Atoi(s.ComPort[3:])
|
comNum, err := strconv.Atoi(s.ComPort[3:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Некорректное имя COM-порта '%s' в конфигурации, пропуск.", s.ComPort)
|
log.Printf("Некорректное имя COM-порта '%s' в конфигурации, пропуск.", s.ComPort)
|
||||||
@@ -303,7 +391,7 @@ func convertSettingsToConfigs(settings []ConnectionSettings) []shtrih.Config {
|
|||||||
config.ComName = s.ComPort
|
config.ComName = s.ComPort
|
||||||
config.ComNumber = int32(comNum)
|
config.ComNumber = int32(comNum)
|
||||||
config.BaudRate = baudRate
|
config.BaudRate = baudRate
|
||||||
case 6: // TCP/IP
|
case 6:
|
||||||
port, err := strconv.Atoi(s.IPPort)
|
port, err := strconv.Atoi(s.IPPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Некорректный TCP-порт '%s' для IP '%s', пропуск.", s.IPPort, s.IP)
|
log.Printf("Некорректный TCP-порт '%s' для IP '%s', пропуск.", s.IPPort, s.IP)
|
||||||
@@ -320,63 +408,18 @@ func convertSettingsToConfigs(settings []ConnectionSettings) []shtrih.Config {
|
|||||||
return configs
|
return configs
|
||||||
}
|
}
|
||||||
|
|
||||||
// saveConfiguration обновляет файл connect.json, записывая в него
|
|
||||||
// конфигурации успешно найденных и опрошенных устройств.
|
|
||||||
func saveConfiguration(polledDevices []PolledDevice) {
|
|
||||||
log.Printf("Сохранение %d найденных конфигураций в файл '%s'...", len(polledDevices), configFileName)
|
|
||||||
|
|
||||||
// Шаг 1: Читаем существующий файл или создаем новую структуру.
|
|
||||||
var configFile ConfigFile
|
|
||||||
data, err := os.ReadFile(configFileName)
|
|
||||||
if err == nil {
|
|
||||||
// Файл есть, парсим его, чтобы не потерять другие секции (например, "atol")
|
|
||||||
if err := json.Unmarshal(data, &configFile); err != nil {
|
|
||||||
log.Printf("Предупреждение: файл '%s' поврежден (%v). Он будет перезаписан.", configFileName, err)
|
|
||||||
configFile = ConfigFile{} // Создаем пустую структуру в случае ошибки
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Шаг 2: Формируем новый срез настроек для "shtrih".
|
|
||||||
var newShtrihSettings []ConnectionSettings
|
|
||||||
for _, pd := range polledDevices {
|
|
||||||
newShtrihSettings = append(newShtrihSettings, convertConfigToSettings(pd.Config))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Шаг 3: Полностью заменяем секцию "shtrih" новыми данными.
|
|
||||||
configFile.Shtrih = newShtrihSettings
|
|
||||||
|
|
||||||
// Шаг 4: Записываем обновленную структуру обратно в файл.
|
|
||||||
updatedData, err := json.MarshalIndent(configFile, "", " ")
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// convertConfigToSettings преобразует внутренний формат shtrih.Config
|
|
||||||
// в формат ConnectionSettings для записи в connect.json.
|
|
||||||
func convertConfigToSettings(config shtrih.Config) ConnectionSettings {
|
func convertConfigToSettings(config shtrih.Config) ConnectionSettings {
|
||||||
// Карта для обратного преобразования индекса скорости в строку
|
|
||||||
baudRateReverseMap := map[int32]string{
|
baudRateReverseMap := map[int32]string{
|
||||||
6: "115200", 5: "57600", 4: "38400", 3: "19200", 2: "9600",
|
6: "115200", 5: "57600", 4: "38400", 3: "19200", 2: "9600", 1: "4800",
|
||||||
}
|
}
|
||||||
|
|
||||||
settings := ConnectionSettings{
|
settings := ConnectionSettings{TypeConnect: config.ConnectionType}
|
||||||
TypeConnect: config.ConnectionType,
|
|
||||||
}
|
|
||||||
|
|
||||||
switch config.ConnectionType {
|
switch config.ConnectionType {
|
||||||
case 0: // COM-порт
|
case 0:
|
||||||
settings.ComPort = config.ComName
|
settings.ComPort = config.ComName
|
||||||
settings.ComBaudrate = baudRateReverseMap[config.BaudRate]
|
settings.ComBaudrate = baudRateReverseMap[config.BaudRate]
|
||||||
case 6: // TCP/IP
|
case 6:
|
||||||
settings.IP = config.IPAddress
|
settings.IP = config.IPAddress
|
||||||
settings.IPPort = strconv.Itoa(int(config.TCPPort))
|
settings.IPPort = strconv.Itoa(int(config.TCPPort))
|
||||||
}
|
}
|
||||||
|
|||||||
129
pkg/shtrih/driver_test.go
Normal file
129
pkg/shtrih/driver_test.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
// Тесты для пакета shtrih
|
||||||
|
package shtrih
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getSampleFiscalInfo создает пример структуры FiscalInfo для использования в тестах.
|
||||||
|
func getSampleFiscalInfo() *FiscalInfo {
|
||||||
|
return &FiscalInfo{
|
||||||
|
ModelName: "ШТРИХ-М-01Ф",
|
||||||
|
SerialNumber: "0012345678901234",
|
||||||
|
RNM: "0009876543210987",
|
||||||
|
OrganizationName: "ООО Ромашка",
|
||||||
|
Inn: "7701234567",
|
||||||
|
FnSerial: "9960440300112233",
|
||||||
|
FfdVersion: "120",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMockDriver_SuccessfulPath проверяет стандартный успешный сценарий:
|
||||||
|
// подключение -> получение данных -> отключение.
|
||||||
|
func TestMockDriver_SuccessfulPath(t *testing.T) {
|
||||||
|
// Arrange: Готовим данные и создаем мок-драйвер.
|
||||||
|
sampleData := getSampleFiscalInfo()
|
||||||
|
driver := NewMockDriver(sampleData, nil, nil)
|
||||||
|
|
||||||
|
// Act 1: Подключаемся.
|
||||||
|
err := driver.Connect()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Connect() вернул неожиданную ошибку: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act 2: Получаем фискальную информацию.
|
||||||
|
info, err := driver.GetFiscalInfo()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetFiscalInfo() вернул неожиданную ошибку: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act 3: Отключаемся.
|
||||||
|
err = driver.Disconnect()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Disconnect() вернул неожиданную ошибку: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert: Проверяем, что полученные данные соответствуют ожидаемым.
|
||||||
|
if !reflect.DeepEqual(info, sampleData) {
|
||||||
|
t.Errorf("Полученные данные не совпадают с мок-данными.\nПолучено: %+v\nОжидалось: %+v", info, sampleData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert: Проверяем, что все методы были вызваны.
|
||||||
|
// Для этого нам нужно преобразовать интерфейс обратно в конкретный тип мок-драйвера.
|
||||||
|
mock, ok := driver.(*mockDriver)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("Не удалось преобразовать драйвер в *mockDriver для проверки вызовов.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mock.ConnectCalled {
|
||||||
|
t.Error("Ожидалось, что Connect() будет вызван, но этого не произошло.")
|
||||||
|
}
|
||||||
|
if !mock.GetFiscalInfoCalled {
|
||||||
|
t.Error("Ожидалось, что GetFiscalInfo() будет вызван, но этого не произошло.")
|
||||||
|
}
|
||||||
|
if !mock.DisconnectCalled {
|
||||||
|
t.Error("Ожидалось, что Disconnect() будет вызван, но этого не произошло.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMockDriver_ConnectError проверяет, что драйвер корректно обрабатывает
|
||||||
|
// ошибку, возвращаемую при подключении.
|
||||||
|
func TestMockDriver_ConnectError(t *testing.T) {
|
||||||
|
// Arrange: Создаем симулируемую ошибку.
|
||||||
|
simulatedError := fmt.Errorf("порт COM5 занят")
|
||||||
|
driver := NewMockDriver(nil, simulatedError, nil)
|
||||||
|
|
||||||
|
// Act: Пытаемся подключиться.
|
||||||
|
err := driver.Connect()
|
||||||
|
|
||||||
|
// Assert: Проверяем, что была возвращена именно наша ошибка.
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Connect() не вернул ошибку, хотя ожидалось.")
|
||||||
|
}
|
||||||
|
if err != simulatedError {
|
||||||
|
t.Errorf("Connect() вернул неверную ошибку. Получено: %v, Ожидалось: %v", err, simulatedError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMockDriver_GetInfoWhileDisconnected проверяет, что попытка получить
|
||||||
|
// данные без предварительного подключения вернет ошибку.
|
||||||
|
func TestMockDriver_GetInfoWhileDisconnected(t *testing.T) {
|
||||||
|
// Arrange: Создаем стандартный мок-драйвер.
|
||||||
|
driver := NewMockDriver(getSampleFiscalInfo(), nil, nil)
|
||||||
|
|
||||||
|
// Act: Сразу пытаемся получить данные.
|
||||||
|
_, err := driver.GetFiscalInfo()
|
||||||
|
|
||||||
|
// Assert: Проверяем, что получили ошибку.
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("GetFiscalInfo() не вернул ошибку при вызове без подключения.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMockDriver_GetInfoError проверяет, что драйвер корректно обрабатывает
|
||||||
|
// ошибку, возвращаемую при получении данных.
|
||||||
|
func TestMockDriver_GetInfoError(t *testing.T) {
|
||||||
|
// Arrange: Создаем симулируемую ошибку.
|
||||||
|
simulatedError := fmt.Errorf("ошибка чтения ФН")
|
||||||
|
driver := NewMockDriver(nil, nil, simulatedError)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := driver.Connect()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Connect() неожиданно вернул ошибку: %v", err)
|
||||||
|
}
|
||||||
|
info, err := driver.GetFiscalInfo()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("GetFiscalInfo() не вернул ошибку, хотя ожидалось.")
|
||||||
|
}
|
||||||
|
if err != simulatedError {
|
||||||
|
t.Errorf("GetFiscalInfo() вернул неверную ошибку. Получено: %v, Ожидалось: %v", err, simulatedError)
|
||||||
|
}
|
||||||
|
if info != nil {
|
||||||
|
t.Error("GetFiscalInfo() вернул данные вместе с ошибкой, хотя должен был вернуть nil.")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,33 +1,85 @@
|
|||||||
|
// Package shtrih (продолжение)
|
||||||
package shtrih
|
package shtrih
|
||||||
|
|
||||||
// mockDriver — это имитация драйвера для тестирования.
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mockDriver представляет собой имитацию реального драйвера для целей тестирования.
|
||||||
|
// Он реализует интерфейс Driver.
|
||||||
type mockDriver struct {
|
type mockDriver struct {
|
||||||
FiscalInfoToReturn *FiscalInfo
|
// MockData - это структура с фискальными данными, которую вернет GetFiscalInfo.
|
||||||
ErrorToReturn error
|
MockData *FiscalInfo
|
||||||
|
// ConnectErr - ошибка, которую вернет метод Connect, если она задана.
|
||||||
|
ConnectErr error
|
||||||
|
// GetFiscalInfoErr - ошибка, которую вернет метод GetFiscalInfo, если она задана.
|
||||||
|
GetFiscalInfoErr error
|
||||||
|
|
||||||
|
// Внутренние флаги для проверки вызовов в тестах.
|
||||||
|
connected bool
|
||||||
|
ConnectCalled bool
|
||||||
|
DisconnectCalled bool
|
||||||
|
GetFiscalInfoCalled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMock создает новый мок-драйвер.
|
// NewMockDriver является конструктором для создания нового мок-драйвера.
|
||||||
func NewMock(info *FiscalInfo, err error) Driver {
|
// Позволяет заранее определить, какие данные и ошибки будут возвращаться.
|
||||||
|
func NewMockDriver(data *FiscalInfo, connectErr, getInfoErr error) Driver {
|
||||||
return &mockDriver{
|
return &mockDriver{
|
||||||
FiscalInfoToReturn: info,
|
MockData: data,
|
||||||
ErrorToReturn: err,
|
ConnectErr: connectErr,
|
||||||
|
GetFiscalInfoErr: getInfoErr,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Connect имитирует подключение к ККТ.
|
||||||
func (m *mockDriver) Connect() error {
|
func (m *mockDriver) Connect() error {
|
||||||
if m.ErrorToReturn != nil {
|
m.ConnectCalled = true
|
||||||
return m.ErrorToReturn
|
log.Println("Mock Driver: Connect() вызван.")
|
||||||
|
|
||||||
|
// Если была задана ошибка подключения, возвращаем ее.
|
||||||
|
if m.ConnectErr != nil {
|
||||||
|
return m.ConnectErr
|
||||||
}
|
}
|
||||||
|
// Если уже "подключены", ничего не делаем.
|
||||||
|
if m.connected {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
m.connected = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Disconnect имитирует отключение от ККТ.
|
||||||
func (m *mockDriver) Disconnect() error {
|
func (m *mockDriver) Disconnect() error {
|
||||||
|
m.DisconnectCalled = true
|
||||||
|
log.Println("Mock Driver: Disconnect() вызван.")
|
||||||
|
|
||||||
|
// Если не были "подключены", ничего не делаем.
|
||||||
|
if !m.connected {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
m.connected = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetFiscalInfo имитирует получение фискальных данных.
|
||||||
func (m *mockDriver) GetFiscalInfo() (*FiscalInfo, error) {
|
func (m *mockDriver) GetFiscalInfo() (*FiscalInfo, error) {
|
||||||
if m.ErrorToReturn != nil {
|
m.GetFiscalInfoCalled = true
|
||||||
return nil, m.ErrorToReturn
|
log.Println("Mock Driver: GetFiscalInfo() вызван.")
|
||||||
|
|
||||||
|
// Проверяем, было ли установлено "соединение".
|
||||||
|
if !m.connected {
|
||||||
|
return nil, fmt.Errorf("мок-драйвер: не подключен")
|
||||||
}
|
}
|
||||||
return m.FiscalInfoToReturn, nil
|
// Если была задана ошибка получения данных, возвращаем ее.
|
||||||
|
if m.GetFiscalInfoErr != nil {
|
||||||
|
return nil, m.GetFiscalInfoErr
|
||||||
|
}
|
||||||
|
// Если не были предоставлены мок-данные, возвращаем ошибку.
|
||||||
|
if m.MockData == nil {
|
||||||
|
return nil, fmt.Errorf("мок-драйвер: данные для имитации не предоставлены")
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.MockData, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user