Add API documentation and remove unused single-price endpoint.
Document all client HTTP calls in docs/API.md, link from README, and delete dead GetPriceItem code. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
46564ee942
commit
800b2971b8
69
README.md
69
README.md
@ -1,67 +1,136 @@
|
||||
# ЭльФиСА — Электронная Фармация
|
||||
|
||||
|
||||
|
||||
WinForms-клиент «Электронная Фармация» с современным UI на базе библиотеки **Elfisa.UI** (тема **PharmaTrust**).
|
||||
|
||||
|
||||
|
||||
## Структура
|
||||
|
||||
|
||||
|
||||
```
|
||||
|
||||
src/
|
||||
|
||||
Elfisa.UI/ — UI-библиотека (ThemeManager, Modern* контролы)
|
||||
|
||||
ElectronicPharmacy/ — основное приложение (SQLite, API, бизнес-логика)
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Сборка
|
||||
|
||||
|
||||
|
||||
Требуется .NET SDK 9+ (или MSBuild 16+) и .NET Framework 4.7.2.
|
||||
|
||||
|
||||
|
||||
```powershell
|
||||
|
||||
dotnet build Elfisa.sln -c Release
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
Запуск:
|
||||
|
||||
|
||||
|
||||
```powershell
|
||||
|
||||
.\src\ElectronicPharmacy\bin\Release\Электронная` Фармация.exe
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
База `efClient.db` копируется в output (`Data\efClient.db` или рядом с exe). Путь задаётся через `AppConfig.SqliteDbPath`.
|
||||
|
||||
|
||||
|
||||
## Настройки
|
||||
|
||||
|
||||
|
||||
В диалоге **Авторизация** (и в `Settings`):
|
||||
|
||||
|
||||
|
||||
- **URL API** — базовый адрес сервера (по умолчанию из `AppConfig`)
|
||||
|
||||
- **Location ID** — UUID точки для отправки заказов
|
||||
|
||||
- **Region ID** — опциональный фильтр при загрузке прайса
|
||||
|
||||
|
||||
|
||||
Переменная окружения `ELF_API_BASE_URL` переопределяет URL API.
|
||||
|
||||
Подробное описание всех HTTP-запросов — в [docs/API.md](docs/API.md).
|
||||
|
||||
|
||||
|
||||
## Архив для заказчика
|
||||
|
||||
|
||||
|
||||
```powershell
|
||||
|
||||
.\dist\build-release.ps1
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
Создаёт `dist\Электронная-Фармация.zip` с Release-сборкой.
|
||||
|
||||
|
||||
|
||||
## Основной цикл (V2)
|
||||
|
||||
|
||||
|
||||
1. Старт → проверка БД → выбор грузополучателя → прайс-лист
|
||||
|
||||
2. **Загрузить** — прайс с API в SQLite, обновление открытых вкладок прайса
|
||||
|
||||
3. Фильтр поставщика, поиск, набор в корзину с проверкой остатка
|
||||
|
||||
4. **Сохранить заказ** — мин. сумма по поставщику, уникальный номер, грузополучатель в `Orders`
|
||||
|
||||
5. **Отправить** — POST `/api/buyer/orders`, статус `ОТПРАВЛЕН` только у отправленного заказа
|
||||
|
||||
6. **Заказы** — фильтры по статусу, поставщику, грузополучателю; отправка одного заказа
|
||||
|
||||
7. **Отказы / Накладные** — синхронизация с `/api/buyer/invoices`
|
||||
|
||||
|
||||
|
||||
## Приёмочный чек-лист
|
||||
|
||||
|
||||
|
||||
1. Старт → БД → выбор точки → прайс
|
||||
|
||||
2. Загрузить → прайс обновился, корзина очищена с предупреждением
|
||||
|
||||
3. Фильтр поставщика + поиск + количество + проверка остатка
|
||||
|
||||
4. Сохранить заказ (мин. сумма, корректный номер, грузополучатель)
|
||||
|
||||
5. Отправить → только выбранные заказы → статус / ошибка API
|
||||
|
||||
6. Журнал: фильтры, комментарий, удаление `НОВЫЙ`
|
||||
|
||||
7. Отказы: загрузка с API + детали позиций
|
||||
|
||||
8. Авторизация + `location_id` в настройках
|
||||
|
||||
9. Единый UI: `UiDialogs` / toast, без debug-панелей загрузки
|
||||
|
||||
|
||||
290
docs/API.md
Normal file
290
docs/API.md
Normal file
@ -0,0 +1,290 @@
|
||||
# API — Электронная Фармация
|
||||
|
||||
Документация по HTTP-запросам, которые использует десктоп-клиент.
|
||||
|
||||
Реализация: `src/ElectronicPharmacy/Classes/ApiClient.cs`, `DataSender.cs`, `InvoiceSyncService.cs`.
|
||||
|
||||
---
|
||||
|
||||
## Базовый URL
|
||||
|
||||
```
|
||||
{ApiBaseUrl}
|
||||
```
|
||||
|
||||
|
||||
| Источник | Приоритет |
|
||||
| -------------------------------------------------------------------- | --------------------------- |
|
||||
| Настройки пользователя (`Settings.ApiBaseUrl`, окно **Авторизация**) | 1 |
|
||||
| Переменная окружения `ELF_API_BASE_URL` | 2 |
|
||||
| Значение по умолчанию | `http://195.34.241.84:9988` |
|
||||
|
||||
|
||||
**Таймаут:** 30 секунд (`ApiClient`). Для отправки заказов (`DataSender`) таймаут не задан явно.
|
||||
|
||||
**Формат:** JSON, кодировка UTF-8.
|
||||
|
||||
---
|
||||
|
||||
## Авторизация
|
||||
|
||||
Все запросы, кроме логина, требуют заголовок:
|
||||
|
||||
```
|
||||
Authorization: Bearer {JWT}
|
||||
```
|
||||
|
||||
Токен сохраняется в `Settings.stringToken` после успешного входа.
|
||||
|
||||
---
|
||||
|
||||
## Эндпоинты
|
||||
|
||||
### 1. Вход
|
||||
|
||||
|
||||
| | |
|
||||
| --------------- | ------------- |
|
||||
| **Метод** | `POST` |
|
||||
| **Путь** | `/auth/login` |
|
||||
| **Авторизация** | не требуется |
|
||||
|
||||
|
||||
**Где в приложении:** окно **Авторизация** (`HF_Registration`), загрузка прайса (`HF_DownloadDataFromServer` — если токен отсутствует).
|
||||
|
||||
**Запрос:**
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "логин",
|
||||
"password": "пароль"
|
||||
}
|
||||
```
|
||||
|
||||
**Ответ `200`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "jwt..."
|
||||
}
|
||||
```
|
||||
|
||||
**Ошибка `401`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "описание ошибки"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Сводный прайс
|
||||
|
||||
|
||||
| | |
|
||||
| --------- | ---------------------------------------- |
|
||||
| **Метод** | `GET` |
|
||||
| **Путь** | `/api/supplier-prices/summary` |
|
||||
| **Query** | `supplier_id` (опц.), `region_id` (опц.) |
|
||||
|
||||
|
||||
**Где в приложении:** меню **Обмен → Загрузить**, кнопка обновления прайса в главном окне (`HF_DownloadDataFromServer`).
|
||||
|
||||
**Примеры:**
|
||||
|
||||
```http
|
||||
GET /api/supplier-prices/summary
|
||||
GET /api/supplier-prices/summary?region_id={RegionId}
|
||||
GET /api/supplier-prices/summary?supplier_id={uuid}
|
||||
```
|
||||
|
||||
**Ответ `200`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"supplier_price_id": "uuid",
|
||||
"guid_es": "uuid",
|
||||
"supplier_id": "uuid",
|
||||
"supplier_name": "Катрен",
|
||||
"drug_name": "Название препарата",
|
||||
"inn": "МНН",
|
||||
"cure_form": "таб.",
|
||||
"barcode": "...",
|
||||
"price": 123.45,
|
||||
"quantity": 10,
|
||||
"region_id": "...",
|
||||
"region_name": "...",
|
||||
"last_price_date": "2026-07-14T00:00:00Z",
|
||||
"match_method": "BARCODE",
|
||||
"match_confidence": 0.95,
|
||||
"trade_name": "...",
|
||||
"dosage": "...",
|
||||
"registry_price": 100.00,
|
||||
"instruction_guid": "...",
|
||||
"description": "...",
|
||||
"storing_condition": "...",
|
||||
"expiry_period": "12.2027",
|
||||
"producer_name": "...",
|
||||
"registry_date": "...",
|
||||
"es_code": 123456,
|
||||
"registry_status": "..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**После ответа:** данные записываются в локальную SQLite-таблицу `PriceList` (полная перезапись).
|
||||
|
||||
**Класс:** `ApiClient.GetPriceSummaryAsync`
|
||||
|
||||
---
|
||||
|
||||
### 3. Отправка заказа
|
||||
|
||||
|
||||
| | |
|
||||
| ---------------- | ------------------- |
|
||||
| **Метод** | `POST` |
|
||||
| **Путь** | `/api/buyer/orders` |
|
||||
| **Content-Type** | `application/json` |
|
||||
|
||||
|
||||
**Где в приложении:**
|
||||
|
||||
- меню **Обмен → Отправить** — все заказы со статусом `НОВЫЙ` (`DataSender.Main`);
|
||||
- страница **Заказы**, кнопка **ОТПРАВИТЬ** — один выбранный заказ (`DataSender.SendSingleOrderAsync`).
|
||||
|
||||
**Запрос** (один заказ = один POST):
|
||||
|
||||
```json
|
||||
{
|
||||
"location_id": "uuid точки из Settings.LocationId",
|
||||
"comment": "комментарий или null",
|
||||
"items": [
|
||||
{
|
||||
"supplier_price_id": "uuid позиции из прайса",
|
||||
"qty": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Условия отправки:**
|
||||
|
||||
- в локальной БД заказ имеет статус `НОВЫЙ`;
|
||||
- у каждой позиции заполнен `supplier_price_id`;
|
||||
- `qty` > 0;
|
||||
- задан `location_id` в настройках.
|
||||
|
||||
**При успехе:** локально `Orders.OrderState` → `ОТПРАВЛЕН` (только для отправленного заказа).
|
||||
|
||||
**При ошибке:** toast с HTTP-кодом и телом ответа сервера.
|
||||
|
||||
**Класс:** `DataSender`
|
||||
|
||||
---
|
||||
|
||||
### 4. Накладные и отказы
|
||||
|
||||
|
||||
| | |
|
||||
| --------- | --------------------- |
|
||||
| **Метод** | `GET` |
|
||||
| **Путь** | `/api/buyer/invoices` |
|
||||
|
||||
|
||||
**Где в приложении:** кнопка **Обновить с сервера** на вкладках **Накладные** и **Отказы** (`InvoiceSyncService`).
|
||||
|
||||
**Ответ `200`:** массив объектов
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"InvoiceNumber": "12345",
|
||||
"InvoiceDate": "2026-07-14",
|
||||
"SupplierName": "Поставщик",
|
||||
"ConsigneeName": "Грузополучатель",
|
||||
"InvoiceSum": "10000.00",
|
||||
"RefuseSum": "500.00"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**После ответа:** `INSERT OR REPLACE` в локальную таблицу `Invoice`.
|
||||
|
||||
**Класс:** `ApiClient.GetInvoicesAsync`
|
||||
|
||||
---
|
||||
|
||||
## Сводная таблица
|
||||
|
||||
|
||||
| Действие в UI | HTTP | Эндпоинт | Класс / метод |
|
||||
| --------------------------- | ------ | ------------------------------ | -------------------------------- |
|
||||
| Авторизация | `POST` | `/auth/login` | `ApiClient.LoginAsync` |
|
||||
| Загрузить прайс | `GET` | `/api/supplier-prices/summary` | `ApiClient.GetPriceSummaryAsync` |
|
||||
| Отправить заказ(ы) | `POST` | `/api/buyer/orders` | `DataSender.SendOrderAsync` |
|
||||
| Обновить накладные / отказы | `GET` | `/api/buyer/invoices` | `ApiClient.GetInvoicesAsync` |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Обработка ошибок
|
||||
|
||||
|
||||
| Ситуация | Поведение клиента |
|
||||
| ------------------ | ---------------------------------------------------------------------------- |
|
||||
| `401 Unauthorized` | Сообщение об ошибке авторизации; для прайса токен сбрасывается в `ApiClient` |
|
||||
| Таймаут (30 с) | `HttpRequestException` с текстом о превышении времени ожидания |
|
||||
| Прочие HTTP-ошибки | Тело `{ "error": "..." }` если сервер вернул JSON |
|
||||
| Нет `location_id` | Отправка заказов блокируется до заполнения в настройках |
|
||||
| Нет токена | Загрузка накладных и отправка заказов блокируются |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Что не реализовано
|
||||
|
||||
- `PUT`, `PATCH`, `DELETE` к API
|
||||
- Получение статуса заказа с сервера
|
||||
- Синхронизация справочников (поставщики, грузополучатели) через API
|
||||
- Автоматический re-login при истечении токена
|
||||
|
||||
---
|
||||
|
||||
## Примеры curl
|
||||
|
||||
**Логин:**
|
||||
|
||||
```bash
|
||||
curl -X POST "{ApiBaseUrl}/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"user","password":"pass"}'
|
||||
```
|
||||
|
||||
**Прайс:**
|
||||
|
||||
```bash
|
||||
curl "{ApiBaseUrl}/api/supplier-prices/summary" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
**Отправка заказа:**
|
||||
|
||||
```bash
|
||||
curl -X POST "{ApiBaseUrl}/api/buyer/orders" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"location_id":"...","comment":null,"items":[{"supplier_price_id":"...","qty":1}]}'
|
||||
```
|
||||
|
||||
**Накладные:**
|
||||
|
||||
```bash
|
||||
curl "{ApiBaseUrl}/api/buyer/invoices" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
@ -83,55 +83,6 @@ namespace Электронная_Фармация.Classes
|
||||
/// </summary>
|
||||
public bool IsAuthenticated => !string.IsNullOrEmpty(_token);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Получает сводный прайс поставщиков
|
||||
/// </summary>
|
||||
/// <param name="supplierId">ID поставщика (опционально)</param>
|
||||
/// <param name="regionId">ID региона (опционально)</param>
|
||||
/// <returns>Сводный прайс</returns>
|
||||
///
|
||||
|
||||
public async Task<PriceSummaryItem> GetPriceItem()
|
||||
{
|
||||
if (!IsAuthenticated)
|
||||
{
|
||||
throw new InvalidOperationException("Необходимо выполнить авторизацию перед запросом данных");
|
||||
}
|
||||
|
||||
//var queryParams = new List<string>();
|
||||
var url = "/api/supplier-prices/556EE09D-AD06-439E-91AF-D6CEBCD61AD3";
|
||||
|
||||
_httpClient.DefaultRequestHeaders.Authorization =
|
||||
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _token);
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var summary = await response.Content.ReadFromJsonAsync<PriceSummaryItem>();
|
||||
return summary;
|
||||
}
|
||||
else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
_token = null; // Токен истек или невалиден
|
||||
var error = await response.Content.ReadFromJsonAsync<ErrorResponse>();
|
||||
throw new UnauthorizedAccessException($"Сессия истекла: {error?.Error ?? "Необходимо войти заново"}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var error = await response.Content.ReadFromJsonAsync<ErrorResponse>();
|
||||
throw new HttpRequestException($"Ошибка HTTP {response.StatusCode}: {error?.Error ?? "Неизвестная ошибка"}");
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
throw new HttpRequestException("Превышено время ожидания ответа сервера (таймаут 30 секунд)");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<PriceSummaryResponse> GetPriceSummaryAsync(string supplierId = null, string regionId = null)
|
||||
{
|
||||
if (!IsAuthenticated)
|
||||
@ -139,7 +90,6 @@ namespace Электронная_Фармация.Classes
|
||||
throw new InvalidOperationException("Необходимо выполнить авторизацию перед запросом данных");
|
||||
}
|
||||
|
||||
// Формируем URL с параметрами
|
||||
var queryParams = new List<string>();
|
||||
if (!string.IsNullOrEmpty(supplierId))
|
||||
{
|
||||
@ -149,20 +99,6 @@ namespace Электронная_Фармация.Classes
|
||||
{
|
||||
queryParams.Add($"region_id={Uri.EscapeDataString(regionId)}");
|
||||
}
|
||||
//if (!string.IsNullOrEmpty(supplierPriceId))
|
||||
//{
|
||||
// queryParams.Add($"supplier_price_id={Uri.EscapeDataString(supplierPriceId)}");
|
||||
//}
|
||||
|
||||
//var queryParams = new List<string>();
|
||||
//if (!string.IsNullOrEmpty(supplierId))
|
||||
//{
|
||||
// queryParams.Add($"supplier_id={Uri.EscapeDataString(supplierId)}");
|
||||
//}
|
||||
//if (!string.IsNullOrEmpty(regionId))
|
||||
//{
|
||||
// queryParams.Add($"region_id={Uri.EscapeDataString(regionId)}");
|
||||
//}
|
||||
|
||||
var url = "/api/supplier-prices/summary";
|
||||
if (queryParams.Count > 0)
|
||||
|
||||
@ -60,12 +60,6 @@ namespace Электронная_Фармация.HelpForms
|
||||
regionId: string.IsNullOrWhiteSpace(Settings.Default.RegionId) ? null : Settings.Default.RegionId);
|
||||
rtxtDebug.Text += ($"✓ Получено {allSuppliersSummary.Summary.Count} позиций\n");
|
||||
|
||||
|
||||
//загружаю один товар:
|
||||
//rtxtDebug.Text += ("\nЗагружаю сводный прайс для всех поставщиков...");
|
||||
// var PriceItem = await client.GetPriceItem();
|
||||
//rtxtDebug.Text += ($"✓ Получено {allSuppliersSummary.DrugName.Count} позиций\n");
|
||||
|
||||
DataTable tablePrice = new DataTable();
|
||||
|
||||
tablePrice.Columns.Add("supplier_price_id");
|
||||
@ -134,45 +128,6 @@ namespace Электронная_Фармация.HelpForms
|
||||
//tablePrice.Columns.Add("RegistryStatus");
|
||||
#endregion
|
||||
|
||||
// Выводим первые 5 позиций
|
||||
//Console.WriteLine("Первые 500 позиций:");
|
||||
|
||||
#region загрузка одного элемента
|
||||
//if (PriceItem.DrugName != null | PriceItem.DrugName != "" | PriceItem.DrugName != string.Empty)
|
||||
//{
|
||||
|
||||
// Console.WriteLine(PriceItem.ToString());
|
||||
|
||||
// DataRow row = tablePrice.NewRow();
|
||||
|
||||
// row["supplier_price_id"] = PriceItem.SupplierPriceID.ToString();
|
||||
// row["guid_es"] = PriceItem.GuidEs.ToString();
|
||||
// row["es_code"] = PriceItem.EsCode.ToString();
|
||||
// row["supplier_id"] = PriceItem.SupplierId.ToString();
|
||||
// row["DrugName"] = PriceItem.DrugName.ToString();
|
||||
// row["SupplierName"] = "Катрен";//PriceItem.SupplierName.ToString();
|
||||
// row["Price"] = PriceItem.Price.ToString();
|
||||
// row["Quantity"] = PriceItem.Quantity.ToString();
|
||||
|
||||
// if (PriceItem.ExpiryPeriod == null)
|
||||
// { row["ExpiryPeriod"] = DBNull.Value; }
|
||||
// else { row["ExpiryPeriod"] = PriceItem.ExpiryPeriod.ToString(); }
|
||||
// ;
|
||||
|
||||
// if (PriceItem.Description == null)
|
||||
// { row["Description"] = DBNull.Value; }
|
||||
// else { row["Description"] = PriceItem.Description.ToString(); }
|
||||
// ;
|
||||
|
||||
// row["Zakaz"] = string.Empty;
|
||||
// row["SummaZakaza"] = string.Empty;
|
||||
|
||||
// tablePrice.Rows.Add(row);
|
||||
//}
|
||||
#endregion
|
||||
|
||||
|
||||
#region загрузка всего прайса РАБОЧАЯ
|
||||
if (allSuppliersSummary.Summary != null)
|
||||
{
|
||||
|
||||
@ -256,7 +211,6 @@ namespace Электронная_Фармация.HelpForms
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
string commandToDelete = "delete from [PriceList]";
|
||||
string commandTODeleteTempOrder = "delete from [TempOrderItems]";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user