0.1.4-prod
added skip shtrih-searsh - [] in connect.json fixed license reader added clearing date-folder after read and before write
This commit is contained in:
10
README.md
10
README.md
@@ -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`).
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ Go-библиотека и консольная утилита для взаим
|
|||||||
1. **Режим автопоиска (первый запуск):**
|
1. **Режим автопоиска (первый запуск):**
|
||||||
* Просто запустите `shtrih-scanner.exe`.
|
* Просто запустите `shtrih-scanner.exe`.
|
||||||
* Программа выполнит полный поиск устройств.
|
* Программа выполнит полный поиск устройств.
|
||||||
* В папке `/date` будут созданы JSON-файлы с данными для каждой найденной ККТ.
|
* В папке `/date` будут созданы JSON-файлы с данными для каждой найденной ККТ.
|
||||||
* Будет создан или перезаписан файл `connect.json` с параметрами найденных устройств.
|
* Будет создан или перезаписан файл `connect.json` с параметрами найденных устройств.
|
||||||
|
|
||||||
2. **Стационарный режим (последующие запуски):**
|
2. **Стационарный режим (последующие запуски):**
|
||||||
|
|||||||
200
main.go
200
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"
|
||||||
@@ -24,7 +26,10 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Глобальная переменная для пути вывода. Это позволяет подменять ее в тестах.
|
// Глобальная переменная для пути вывода. Это позволяет подменять ее в тестах.
|
||||||
var outputDir = "date"
|
var (
|
||||||
|
outputDir = "date"
|
||||||
|
version = "0.1.4-dev"
|
||||||
|
)
|
||||||
|
|
||||||
// --- СТРУКТУРЫ ДЛЯ ПАРСИНГА КОНФИГУРАЦИОННЫХ ФАЙЛОВ ---
|
// --- СТРУКТУРЫ ДЛЯ ПАРСИНГА КОНФИГУРАЦИОННЫХ ФАЙЛОВ ---
|
||||||
|
|
||||||
@@ -47,8 +52,9 @@ type ServiceFile struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PolledDevice struct {
|
type PolledDevice struct {
|
||||||
@@ -59,8 +65,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) {
|
||||||
@@ -77,22 +95,34 @@ func main() {
|
|||||||
log.Println("Работа приложения завершена.")
|
log.Println("Работа приложения завершена.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupLogger() {
|
// loadServiceConfig читает и парсит service.json.
|
||||||
|
// Возвращает конфигурацию или 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) {
|
||||||
|
if config == nil {
|
||||||
|
log.Printf("Предупреждение: файл настроек '%s' не найден или некорректен. Логирование продолжится в консоль.", serviceConfigName)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logDays := serviceFile.Service.LogDays
|
logDays := config.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 {
|
||||||
@@ -111,11 +141,11 @@ func setupLogger() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
setupLogger()
|
// setupLogger() // <--- УДАЛИТЕ ЭТУ СТРОКУ
|
||||||
|
|
||||||
var configFile ConfigFile
|
var configFile ConfigFile
|
||||||
if err := json.Unmarshal(data, &configFile); err != nil {
|
if err := json.Unmarshal(data, &configFile); err != nil {
|
||||||
@@ -124,12 +154,20 @@ func runConfigMode(data []byte) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(configFile.Shtrih) == 0 {
|
// Проверяем наличие секции shtrih в файле, но не пустоту массива.
|
||||||
log.Printf("В файле '%s' не найдено настроек для 'shtrih'. Переключаюсь на режим автопоиска.", configFileName)
|
// Пустой массив shtrih: [] является валидным состоянием.
|
||||||
|
if configFile.Shtrih == nil {
|
||||||
|
log.Printf("В файле '%s' отсутствует секция 'shtrih'. Переключаюсь на режим автопоиска.", configFileName)
|
||||||
runDiscoveryMode()
|
runDiscoveryMode()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(configFile.Shtrih) == 0 {
|
||||||
|
log.Println("Список устройств 'shtrih' в конфигурации пуст. Сканирование не требуется.")
|
||||||
|
// Здесь можно завершить работу, так как опрашивать нечего.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
log.Printf("Найдено %d конфигураций для Штрих-М. Начинаю опрос...", len(configFile.Shtrih))
|
log.Printf("Найдено %d конфигураций для Штрих-М. Начинаю опрос...", len(configFile.Shtrih))
|
||||||
configs := convertSettingsToConfigs(configFile.Shtrih)
|
configs := convertSettingsToConfigs(configFile.Shtrih)
|
||||||
if len(configs) == 0 {
|
if len(configs) == 0 {
|
||||||
@@ -149,7 +187,9 @@ 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))
|
||||||
@@ -198,36 +238,31 @@ func processDevices(configs []shtrih.Config, newDriverFunc func(shtrih.Config) s
|
|||||||
|
|
||||||
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)
|
||||||
@@ -302,23 +337,52 @@ func findSourceWorkstationData() map[string]interface{} {
|
|||||||
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 {
|
// Регулярное выражение для проверки, что строка состоит только из цифр.
|
||||||
return fmt.Errorf("ошибка парсинга JSON: %w", err)
|
isNumeric := regexp.MustCompile(`^[0-9]+$`).MatchString
|
||||||
|
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++
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
currentTime := time.Now().Format("2006-01-02 15:04:05")
|
|
||||||
content["current_time"] = currentTime
|
if deletedCount > 0 {
|
||||||
content["v_time"] = currentTime
|
log.Printf("Очистка завершена. Удалено %d файлов.", deletedCount)
|
||||||
updatedData, err := json.MarshalIndent(content, "", " ")
|
} else {
|
||||||
if err != nil {
|
log.Println("Некорректных файлов для удаления не найдено.")
|
||||||
return fmt.Errorf("ошибка маршалинга JSON: %w", err)
|
|
||||||
}
|
}
|
||||||
return os.WriteFile(filePath, updatedData, 0644)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// saveNewMergedInfo объединяет данные ККТ и данные рабочей станции (в виде map) и сохраняет в новый JSON-файл.
|
// saveNewMergedInfo объединяет данные ККТ и данные рабочей станции (в виде map) и сохраняет в новый JSON-файл.
|
||||||
@@ -370,6 +434,42 @@ func saveNewMergedInfo(kktInfo *shtrih.FiscalInfo, wsData map[string]interface{}
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// saveEmptyShtrihConfig создает или обновляет connect.json, указывая,
|
||||||
|
// что устройства "Штрих-М" не были найдены. Это предотвращает повторные
|
||||||
|
// полные сканирования при последующих запусках.
|
||||||
|
func saveEmptyShtrihConfig() {
|
||||||
|
log.Printf("Сохраняю конфигурацию с пустым списком устройств Штрих-М в '%s'...", configFileName)
|
||||||
|
var configFile ConfigFile
|
||||||
|
|
||||||
|
// Пытаемся прочитать существующий файл, чтобы не затереть другие секции (например, 'atol').
|
||||||
|
data, err := os.ReadFile(configFileName)
|
||||||
|
if err == nil {
|
||||||
|
if err := json.Unmarshal(data, &configFile); err != nil {
|
||||||
|
log.Printf("Предупреждение: файл '%s' поврежден (%v). Он будет перезаписан.", configFileName, err)
|
||||||
|
// В случае ошибки парсинга, начинаем с пустой структуры, чтобы исправить файл.
|
||||||
|
configFile = ConfigFile{}
|
||||||
|
}
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
// Логируем ошибку, если это не "файл не найден".
|
||||||
|
log.Printf("Предупреждение: не удалось прочитать '%s' (%v). Файл будет создан заново.", configFileName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем пустой срез для 'shtrih'.
|
||||||
|
configFile.Shtrih = []ConnectionSettings{}
|
||||||
|
|
||||||
|
// Маршалинг и запись обратно в файл.
|
||||||
|
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 saveConfiguration(polledDevices []PolledDevice) {
|
func saveConfiguration(polledDevices []PolledDevice) {
|
||||||
log.Printf("Сохранение %d найденных конфигураций в файл '%s'...", len(polledDevices), configFileName)
|
log.Printf("Сохранение %d найденных конфигураций в файл '%s'...", len(polledDevices), configFileName)
|
||||||
var configFile ConfigFile
|
var configFile ConfigFile
|
||||||
|
|||||||
@@ -37,22 +37,22 @@ 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"` // ИНН пользователя
|
Inn string `json:"INN"` // ИНН пользователя
|
||||||
FnSerial string `json:"fn_serial"` // Серийный номер фискального накопителя
|
FnSerial string `json:"fn_serial"` // Серийный номер фискального накопителя
|
||||||
RegistrationDate string `json:"datetime_reg"` // Дата и время регистрации ККТ
|
RegistrationDate string `json:"datetime_reg"` // Дата и время регистрации ККТ
|
||||||
FnEndDate string `json:"dateTime_end"` // Дата окончания срока действия ФН
|
FnEndDate string `json:"dateTime_end"` // Дата окончания срока действия ФН
|
||||||
OfdName string `json:"ofdName"` // Наименование ОФД
|
OfdName string `json:"ofdName"` // Наименование ОФД
|
||||||
SoftwareDate string `json:"bootVersion"` // Версия (дата) прошивки ККТ
|
SoftwareDate string `json:"bootVersion"` // Версия (дата) прошивки ККТ
|
||||||
FfdVersion string `json:"ffdVersion"` // Версия ФФД
|
FfdVersion string `json:"ffdVersion"` // Версия ФФД
|
||||||
FnExecution string `json:"fnExecution"` // Исполнение ФН
|
FnExecution string `json:"fnExecution"` // Исполнение ФН
|
||||||
InstalledDriver string `json:"installed_driver"` // Версия установленного COM-драйвера
|
InstalledDriver string `json:"installed_driver"` // Версия установленного COM-драйвера
|
||||||
AttributeExcise bool `json:"attribute_excise"` // Признак торговли подакцизными товарами
|
AttributeExcise bool `json:"attribute_excise"` // Признак торговли подакцизными товарами
|
||||||
AttributeMarked bool `json:"attribute_marked"` // Признак торговли маркированными товарами
|
AttributeMarked bool `json:"attribute_marked"` // Признак торговли маркированными товарами
|
||||||
LicensesRawHex string `json:"licenses,omitempty"` // Строка с лицензиями в HEX-формате
|
SubscriptionInfo string `json:"licenses,omitempty"` // Строка с лицензиями в расшифрованном виде
|
||||||
}
|
}
|
||||||
|
|
||||||
// Driver определяет основной интерфейс для работы с ККТ.
|
// Driver определяет основной интерфейс для работы с ККТ.
|
||||||
@@ -114,10 +114,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 +204,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 +248,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
|
||||||
}
|
}
|
||||||
@@ -564,4 +574,4 @@ func checkIP(ip string, port int32, timeout time.Duration, foundChan chan<- Conf
|
|||||||
foundChan <- config
|
foundChan <- config
|
||||||
driver.Disconnect()
|
driver.Disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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