UPDATE
This commit is contained in:
@@ -26,3 +26,5 @@ AI_SERVICE_API_KEY=
|
||||
|
||||
SENSOR_HUB_SERVICE_BASE_URL=https://sensor-hub.example.com
|
||||
SENSOR_HUB_SERVICE_API_KEY=
|
||||
|
||||
CROP_ZONE_CHUNK_AREA_SQM=10000
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,152 +0,0 @@
|
||||
# مشخصات Response های API بخش Crop Zoning
|
||||
|
||||
این سند فقط **فرمت و ساختار response**هایی را که فرانتاند از API انتظار دارد شرح میدهد.
|
||||
|
||||
---
|
||||
|
||||
## ۱. API بهینهسازی زونبندی (Optimize Zoning)
|
||||
|
||||
وقتی کاربر منطقه را روی نقشه انتخاب میکند و دکمه «بهینهسازی مجدد» را میزند، فرانتاند یک **GeoJSON Polygon** (مختصات منطقه) به API میفرستد و انتظار دارد سرور یک **GeoJSON FeatureCollection** برگرداند که هر feature آن یک زون با geometry (چندضلعی) و properties (دادههای پیشنهاد محصول) دارد.
|
||||
|
||||
### Request (خلاصه)
|
||||
|
||||
- **ورودی:** یک GeoJSON به صورت `Feature` با `geometry.type: "Polygon"` (مختصات به صورت `[lng, lat]`).
|
||||
|
||||
### Response مورد انتظار
|
||||
|
||||
یک **GeoJSON FeatureCollection** با این ساختار:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [[[lng, lat], [lng, lat], ...]]
|
||||
},
|
||||
"properties": {
|
||||
"zoneId": "string",
|
||||
"crop": "wheat" | "canola" | "saffron",
|
||||
"matchPercent": number,
|
||||
"waterNeed": "string",
|
||||
"estimatedProfit": "string",
|
||||
"reason": "string",
|
||||
"criteria": [
|
||||
{ "name": "string", "value": number },
|
||||
...
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### توضیح فیلدهای `properties` هر زون
|
||||
|
||||
| فیلد | نوع | توضیح |
|
||||
|------|-----|--------|
|
||||
| `zoneId` | `string` | شناسه یکتا برای زون (مثلاً `"zone-0"`, `"zone-1"`) |
|
||||
| `crop` | `"wheat" \| "canola" \| "saffron"` | نوع محصول پیشنهادی برای این زون |
|
||||
| `matchPercent` | `number` | درصد تطابق (۰–۱۰۰) برای پیشنهاد محصول |
|
||||
| `waterNeed` | `string` | نیاز آبی (مثلاً `"۴۵۰۰-۵۵۰۰ m³/ha"`) |
|
||||
| `estimatedProfit` | `string` | سود تخمینی (مثلاً `"۱۵-۲۵ میلیون/هکتار"`) |
|
||||
| `reason` | `string` | توضیح کوتاه دلیل پیشنهاد این محصول |
|
||||
| `criteria` | `Array<{ name: string, value: number }>` | معیارهای امتیازدهی برای نمودار راداری؛ `value` بین ۰ تا ۱۰۰ (مثلاً دما، بارش، خاک، آب) |
|
||||
|
||||
### نمونه response (یک feature)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[51.38, 35.68],
|
||||
[51.381, 35.68],
|
||||
[51.381, 35.681],
|
||||
[51.38, 35.681],
|
||||
[51.38, 35.68]
|
||||
]
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"zoneId": "zone-0",
|
||||
"crop": "wheat",
|
||||
"matchPercent": 78,
|
||||
"waterNeed": "۴۵۰۰-۵۵۰۰ m³/ha",
|
||||
"estimatedProfit": "۱۵-۲۵ میلیون/هکتار",
|
||||
"reason": "دمای مناسب، خاک حاصلخیز، دسترسی به آب کافی",
|
||||
"criteria": [
|
||||
{ "name": "دما", "value": 85 },
|
||||
{ "name": "بارش", "value": 72 },
|
||||
{ "name": "خاک", "value": 80 },
|
||||
{ "name": "آب", "value": 65 }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ۲. API منطقه اولیه (اختیاری)
|
||||
|
||||
اگر بخواهید منطقه اولیه نقشه از سرور بیاید (بهجای ماک ثابت)، response باید یک **GeoJSON Feature** با Polygon باشد:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {},
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[51.38, 35.68],
|
||||
[51.40, 35.68],
|
||||
[51.40, 35.70],
|
||||
[51.38, 35.70],
|
||||
[51.38, 35.68]
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- مختصات به صورت `[longitude, latitude]` (lng, lat).
|
||||
- آرایه اول `coordinates` حلقه بیرونی چندضلعی است؛ نقطه اول و آخر باید یکسان باشند.
|
||||
|
||||
---
|
||||
|
||||
## ۳. خلاصه نوعهای TypeScript (برای تطبیق با بکاند)
|
||||
|
||||
```ts
|
||||
type CropType = 'wheat' | 'canola' | 'saffron'
|
||||
|
||||
interface ZoneFeatureProperties {
|
||||
zoneId: string
|
||||
crop: CropType
|
||||
matchPercent: number
|
||||
waterNeed: string
|
||||
estimatedProfit: string
|
||||
reason: string
|
||||
criteria: { name: string; value: number }[]
|
||||
}
|
||||
|
||||
// Response بهینهسازی = GeoJSON FeatureCollection
|
||||
// با Feature<Polygon, ZoneFeatureProperties>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ۴. نکات
|
||||
|
||||
- **Layer فعلی:** فرانتاند لایههای مختلف (`crops`, `waterNeed`, `soilQuality`, `cultivationRisk`) دارد؛ در صورت نیاز میتوان برای هر لایه response جدا یا فیلدهای اضافه در `properties` تعریف کرد.
|
||||
- **دکمه «تغییر محصول»:** در پنل جزئیات زون، کاربر میتواند محصول را بین `wheat`, `canola`, `saffron` عوض کند؛ در صورت نیاز میتوان API جدا برای ذخیره این تغییر تعریف کرد.
|
||||
- **بخش آب و هوا:** دادههای آب و هوا از سرویس جدا (`farmDashboardService.getAllCards()` → `farmWeatherCard`) گرفته میشوند و در این سند پوشش داده نشدهاند.
|
||||
@@ -1,544 +0,0 @@
|
||||
# مستندات APIهای زونبندی کشت (Crop Zoning)
|
||||
|
||||
این سند تمام APIهای مورد نیاز برای صفحه **Crop Zoning** را شرح میدهد: ورودیها، خروجیها، محصولات، رنگها، مساحت کلی و دیتای هر بخش زمین به صورت جداگانه.
|
||||
|
||||
**مسیر صفحه:** `(dashboard)/(private)/crop-zoning`
|
||||
**کامپوننت اصلی:** `CropZoningWrapper`
|
||||
|
||||
---
|
||||
|
||||
## نمای کلی و جریان درخواستها
|
||||
|
||||
```
|
||||
۱. GET area → منطقهٔ ثابت (کاربر امکان رسم ندارد)
|
||||
۲. GET products → لیست محصولات و رنگها
|
||||
۳. POST zones/initial → ارسال محدودهٔ مربعها → دیتای محصولات پیشنهادی (نقشه + tooltip)
|
||||
۴. POST zones/water-need → ارسال محدودهٔ مربعها → نیاز آبی هر منطقه
|
||||
۵. POST zones/soil-quality → ارسال محدودهٔ مربعها → کیفیت خاک هر منطقه
|
||||
۶. POST zones/cultivation-risk → ارسال محدودهٔ مربعها → ریسک کشت هر منطقه
|
||||
۷. GET zone/:zoneId → کلیک روی مربع → دیتای تکمیلی (پنل جزئیات: reason, criteria, ...)
|
||||
```
|
||||
|
||||
| ردیف | API | هدف |
|
||||
|------|-----|------|
|
||||
| ۱ | **منطقهٔ اولیه** | دریافت منطقهٔ زمین به صورت GeoJSON؛ کاربر نمیتواند چیزی رسم کند |
|
||||
| ۲ | **لیست محصولات و رنگها** | دریافت محصولات قابل کشت به همراه رنگ نمایش و لیبل فارسی |
|
||||
| ۳ | **دیتای اولیه زونها (محصولات)** | ارسال محدودهٔ مربعها، دریافت محصول پیشنهادی برای نقشه و tooltip |
|
||||
| ۴ | **نیاز آبی** | ارسال محدودهٔ مربعها، دریافت نیاز آبی هر منطقه برای لایهٔ نیاز آبی |
|
||||
| ۵ | **کیفیت خاک** | ارسال محدودهٔ مربعها، دریافت کیفیت خاک هر منطقه برای لایهٔ کیفیت خاک |
|
||||
| ۶ | **ریسک کشت** | ارسال محدودهٔ مربعها، دریافت ریسک کشت هر منطقه برای لایهٔ ریسک کشت |
|
||||
| ۷ | **دیتای تکمیلی زون** | با کلیک روی هر مربع، دریافت دیتای جزئیات (دلیل، معیارها، نمودار) |
|
||||
|
||||
---
|
||||
|
||||
## ۰. API منطقهٔ اولیه (Area)
|
||||
|
||||
منطقهٔ ثابت زمین که از بکاند دریافت میشود. کاربر امکان رسم یا ویرایش منطقه را ندارد.
|
||||
|
||||
### ۰.۱ مشخصات
|
||||
|
||||
- **متد:** `GET`
|
||||
- **آدرس پیشنهادی:** `GET /api/crop-zoning/area/`
|
||||
- **هدف:** دریافت polygon منطقهٔ زمین برای نمایش روی نقشه.
|
||||
|
||||
### ۰.۲ ورودی (Request)
|
||||
|
||||
بدون پارامتر.
|
||||
|
||||
### ۰.۳ خروجی (Response Body)
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"area": {
|
||||
"type": "Feature",
|
||||
"properties": {},
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[51.38, 35.68],
|
||||
[51.40, 35.68],
|
||||
[51.40, 35.70],
|
||||
[51.38, 35.70],
|
||||
[51.38, 35.68]
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **مختصات:** `[longitude, latitude]` طبق استاندارد GeoJSON
|
||||
|
||||
---
|
||||
|
||||
## ۱. API لیست محصولات و رنگها
|
||||
|
||||
برای نمایش راهنمای رنگها (Legend) و dropdown انتخاب محصول در پنل جزئیات هر زون.
|
||||
|
||||
### ۱.۱ مشخصات
|
||||
|
||||
- **متد:** `GET`
|
||||
- **آدرس پیشنهادی:** `GET /api/crop-zoning/products/` یا `GET /api/crops/`
|
||||
- **هدف:** دریافت لیست محصولات قابل کشت با رنگ و لیبل نمایشی.
|
||||
|
||||
### ۱.۲ ورودی (Request)
|
||||
|
||||
بدون پارامتر یا با پارامترهای اختیاری:
|
||||
|
||||
| پارامتر | نوع | اجباری | توضیح |
|
||||
|---------|-----|--------|--------|
|
||||
| `locale` | string | خیر | کد زبان برای لیبلها (مثلاً `fa`, `en`) |
|
||||
|
||||
### ۱.۳ خروجی (Response)
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"products": [
|
||||
{
|
||||
"id": "wheat",
|
||||
"label": "گندم",
|
||||
"color": "#6bcb77"
|
||||
},
|
||||
{
|
||||
"id": "canola",
|
||||
"label": "کلزا",
|
||||
"color": "#ffd93d"
|
||||
},
|
||||
{
|
||||
"id": "saffron",
|
||||
"label": "زعفران",
|
||||
"color": "#9b59b6"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**ساختار هر محصول:**
|
||||
|
||||
| فیلد | نوع | اجباری | توضیح |
|
||||
|------|-----|--------|--------|
|
||||
| `id` | string | بله | شناسهٔ یکتا (مثلاً `wheat`, `canola`, `saffron`) |
|
||||
| `label` | string | بله | نام نمایشی به زبان کاربر |
|
||||
| `color` | string | بله | رنگ hex برای نمایش روی نقشه و Legend |
|
||||
|
||||
---
|
||||
|
||||
## ۲. API دیتای اولیه زونها
|
||||
|
||||
با رسم منطقهٔ زمین، فرانت **محدودهٔ همهٔ مربعها** (گرید داخل polygon) را ارسال میکند و **دیتای اولیه** هر مربع را دریافت میکند — برای نمایش نقشه، رنگبندی، و **هاور/tooltip**. این دیتا شامل `reason` و `criteria` **نیست**.
|
||||
|
||||
### ۲.۱ مشخصات
|
||||
|
||||
- **متد:** `POST`
|
||||
- **آدرس پیشنهادی:** `POST /api/crop-zoning/zones/initial/`
|
||||
- **هدف:** ارسال محدودهٔ مربعها، دریافت دیتای اولیه برای نقشه، هاور و tooltip.
|
||||
|
||||
### ۲.۲ ورودی (Request Body)
|
||||
|
||||
فرانت ابتدا با Turf.js از روی polygon منطقه گرید میسازد، سپس `FeatureCollection` همهٔ polygonهای مربعها را ارسال میکند.
|
||||
|
||||
| فیلد | نوع | اجباری | توضیح |
|
||||
|------|-----|--------|--------|
|
||||
| `zones` | GeoJSON FeatureCollection | بله | محدودهٔ هر مربع به صورت Polygon؛ ترتیب index با پاسخ یکسان است |
|
||||
| `products` | string[] | خیر | لیست محصولات مدنظر؛ در صورت عدم ارسال از همهٔ محصولات استفاده شود |
|
||||
|
||||
**ساختار `zones` (محدودهٔ همهٔ مربعها):**
|
||||
|
||||
```json
|
||||
{
|
||||
"zones": {
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[51.38, 35.68],
|
||||
[51.3815, 35.68],
|
||||
[51.3815, 35.6815],
|
||||
[51.38, 35.6815],
|
||||
[51.38, 35.68]
|
||||
]
|
||||
]
|
||||
},
|
||||
"properties": { "index": 0 }
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[51.3815, 35.68],
|
||||
[51.383, 35.68],
|
||||
[51.383, 35.6815],
|
||||
[51.3815, 35.6815],
|
||||
[51.3815, 35.68]
|
||||
]
|
||||
]
|
||||
},
|
||||
"properties": { "index": 1 }
|
||||
}
|
||||
]
|
||||
},
|
||||
"products": ["wheat", "canola", "saffron"]
|
||||
}
|
||||
```
|
||||
|
||||
- **مختصات:** `[longitude, latitude]` طبق استاندارد GeoJSON
|
||||
- **index:** در `properties` هر feature برای تطابق با پاسخ (اختیاری؛ در صورت نبودن از ترتیب آرایه استفاده شود)
|
||||
|
||||
### ۲.۳ خروجی (Response Body) — دیتای اولیه
|
||||
|
||||
فقط فیلدهای لازم برای **نقشه**، **هاور** و **tooltip** (نمایش هنگام عبور ماوس روی هر مربع)؛ بدون `reason` و `criteria`.
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"total_area_hectares": 23.45,
|
||||
"total_area_sqm": 234500,
|
||||
"zone_count": 3,
|
||||
"zones": [
|
||||
{
|
||||
"zoneId": "zone-0",
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[51.38, 35.68],
|
||||
[51.3815, 35.68],
|
||||
[51.3815, 35.6815],
|
||||
[51.38, 35.6815],
|
||||
[51.38, 35.68]
|
||||
]
|
||||
]
|
||||
},
|
||||
"crop": "wheat",
|
||||
"matchPercent": 85,
|
||||
"waterNeed": "۴۵۰۰-۵۵۰۰ m³/ha",
|
||||
"estimatedProfit": "۱۵-۲۵ میلیون/هکتار"
|
||||
},
|
||||
{
|
||||
"zoneId": "zone-1",
|
||||
"geometry": { "type": "Polygon", "coordinates": [...] },
|
||||
"crop": "canola",
|
||||
"matchPercent": 78,
|
||||
"waterNeed": "۵۰۰۰-۶۰۰۰ m³/ha",
|
||||
"estimatedProfit": "۲۰-۳۵ میلیون/هکتار"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**ساختار دیتای اولیه هر زون (هم برای نقشه هم برای هاور/tooltip):**
|
||||
|
||||
| فیلد | نوع | اجباری | توضیح |
|
||||
|------|-----|--------|--------|
|
||||
| `zoneId` | string | بله | شناسهٔ یکتا برای درخواست دیتای تکمیلی |
|
||||
| `geometry` | Polygon | بله | هندسهٔ همان مربع ارسالی |
|
||||
| `crop` | string \| null | خیر | محصول پیشنهادی؛ اگر `null`/خالی/`uncultivable` باشد → زون **غیرقابل کشت** و رنگ خاکستری |
|
||||
| `matchPercent` | number | خیر | درصد تطابق (هاور/tooltip) |
|
||||
| `waterNeed` | string | خیر | نیاز آبی (هاور/tooltip) |
|
||||
| `estimatedProfit` | string | خیر | سود تخمینی (هاور/tooltip) |
|
||||
|
||||
**زون غیرقابل کشت:** اگر برای مربعی اطلاعاتی نیاید یا `crop` خالی/`null`/`uncultivable` باشد، آن مربع خاکستری نمایش داده شده و در tooltip «غیر قابل کشت» نشان داده میشود. کلیک روی آن پنل جزئیات باز نمیشود.
|
||||
|
||||
**نکته:** این فیلدها هنگام **هاور** روی مربع در tooltip نمایش داده میشوند؛ نیازی به درخواست جداگانه برای tooltip نیست.
|
||||
|
||||
---
|
||||
|
||||
## ۲.۱ API نیاز آبی (Water Need)
|
||||
|
||||
نیاز آبی هر منطقه را بر اساس محدودهٔ مربعها برمیگرداند. با تغییر لایه به «نیاز آبی» در LayerControl، فرانت این API را صدا میزند و نقشه و Legend را بهروزرسانی میکند.
|
||||
|
||||
### مشخصات
|
||||
|
||||
- **متد:** `POST`
|
||||
- **آدرس پیشنهادی:** `POST /api/crop-zoning/zones/water-need/`
|
||||
- **هدف:** دریافت نیاز آبی هر منطقه برای نمایش روی نقشه در لایهٔ نیاز آبی.
|
||||
|
||||
### ورودی (Request Body)
|
||||
|
||||
همان ساختار `POST zones/initial/`:
|
||||
|
||||
| فیلد | نوع | اجباری | توضیح |
|
||||
|------|-----|--------|--------|
|
||||
| `zones` | GeoJSON FeatureCollection | بله | محدودهٔ هر مربع به صورت Polygon |
|
||||
|
||||
### خروجی (Response Body)
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"zones": [
|
||||
{
|
||||
"zoneId": "zone-0",
|
||||
"geometry": { "type": "Polygon", "coordinates": [...] },
|
||||
"level": "low",
|
||||
"value": "۳۰۰۰-۴۰۰۰ m³/ha",
|
||||
"color": "#7dd3fc"
|
||||
},
|
||||
{
|
||||
"zoneId": "zone-1",
|
||||
"geometry": { "type": "Polygon", "coordinates": [...] },
|
||||
"level": "medium",
|
||||
"value": "۵۰۰۰-۶۰۰۰ m³/ha",
|
||||
"color": "#0ea5e9"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| فیلد | نوع | توضیح |
|
||||
|------|-----|--------|
|
||||
| `zoneId` | string | شناسهٔ زون |
|
||||
| `geometry` | Polygon | هندسهٔ مربع |
|
||||
| `level` | string | سطح: `low`, `medium`, `high` |
|
||||
| `value` | string | مقدار نیاز آبی (مثلاً m³/ha) |
|
||||
| `color` | string | رنگ hex برای نمایش |
|
||||
|
||||
---
|
||||
|
||||
## ۲.۲ API کیفیت خاک (Soil Quality)
|
||||
|
||||
کیفیت خاک هر منطقه را برمیگرداند. با تغییر لایه به «کیفیت خاک»، فرانت این API را صدا میزند.
|
||||
|
||||
### مشخصات
|
||||
|
||||
- **متد:** `POST`
|
||||
- **آدرس پیشنهادی:** `POST /api/crop-zoning/zones/soil-quality/`
|
||||
|
||||
### ورودی (Request Body)
|
||||
|
||||
همان `zones` (FeatureCollection).
|
||||
|
||||
### خروجی (Response Body)
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"zones": [
|
||||
{
|
||||
"zoneId": "zone-0",
|
||||
"geometry": { "type": "Polygon", "coordinates": [...] },
|
||||
"level": "low",
|
||||
"score": 35,
|
||||
"color": "#f87171"
|
||||
},
|
||||
{
|
||||
"zoneId": "zone-1",
|
||||
"geometry": { "type": "Polygon", "coordinates": [...] },
|
||||
"level": "high",
|
||||
"score": 85,
|
||||
"color": "#22c55e"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| فیلد | نوع | توضیح |
|
||||
|------|-----|--------|
|
||||
| `level` | string | سطح: `low`, `medium`, `high` |
|
||||
| `score` | number | امتیاز ۰–۱۰۰ |
|
||||
| `color` | string | رنگ hex |
|
||||
|
||||
---
|
||||
|
||||
## ۲.۳ API ریسک کشت (Cultivation Risk)
|
||||
|
||||
ریسک کشت هر منطقه را برمیگرداند. با تغییر لایه به «ریسک کشت»، فرانت این API را صدا میزند.
|
||||
|
||||
### مشخصات
|
||||
|
||||
- **متد:** `POST`
|
||||
- **آدرس پیشنهادی:** `POST /api/crop-zoning/zones/cultivation-risk/`
|
||||
|
||||
### ورودی و خروجی
|
||||
|
||||
ورودی: همان `zones` (FeatureCollection).
|
||||
|
||||
خروجی نمونه:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"zones": [
|
||||
{
|
||||
"zoneId": "zone-0",
|
||||
"geometry": { "type": "Polygon", "coordinates": [...] },
|
||||
"level": "low",
|
||||
"color": "#22c55e"
|
||||
},
|
||||
{
|
||||
"zoneId": "zone-1",
|
||||
"geometry": { "type": "Polygon", "coordinates": [...] },
|
||||
"level": "high",
|
||||
"color": "#ef4444"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| فیلد | نوع | توضیح |
|
||||
|------|-----|--------|
|
||||
| `level` | string | سطح: `low`, `medium`, `high` |
|
||||
| `color` | string | رنگ hex |
|
||||
|
||||
**نکته:** برای هر لایه (نیاز آبی، کیفیت خاک، ریسک کشت) فرانت یک **درخواست جداگانه** ارسال میکند و نقشه و Legend متناسب با همان لایه بهروزرسانی میشوند.
|
||||
|
||||
---
|
||||
|
||||
## ۳. API دیتای تکمیلی زون (با کلیک روی مربع)
|
||||
|
||||
وقتی کاربر روی یک مربع کلیک میکند، فرانت با `zoneId` دیتای **تکمیلی** را درخواست میکند — برای نمایش پنل جزئیات: دلیل پیشنهاد، معیارها، نمودار راداری.
|
||||
|
||||
### ۳.۱ مشخصات
|
||||
|
||||
- **متد:** `GET`
|
||||
- **آدرس پیشنهادی:** `GET /api/crop-zoning/zones/:zoneId/details/`
|
||||
- **هدف:** دریافت دیتای تکمیلی یک زون برای پنل جزئیات.
|
||||
|
||||
### ۳.۲ ورودی (Request)
|
||||
|
||||
| پارامتر | محل | نوع | اجباری | توضیح |
|
||||
|---------|------|-----|--------|--------|
|
||||
| `zoneId` | path | string | بله | شناسهٔ زون (مثلاً `zone-0`) |
|
||||
|
||||
**مثال:** `GET /api/crop-zoning/zones/zone-0/details/`
|
||||
|
||||
### ۳.۳ خروجی (Response Body)
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"zoneId": "zone-0",
|
||||
"crop": "wheat",
|
||||
"matchPercent": 85,
|
||||
"waterNeed": "۴۵۰۰-۵۵۰۰ m³/ha",
|
||||
"estimatedProfit": "۱۵-۲۵ میلیون/هکتار",
|
||||
"reason": "دمای مناسب، خاک حاصلخیز، دسترسی به آب کافی",
|
||||
"criteria": [
|
||||
{ "name": "دما", "value": 82 },
|
||||
{ "name": "بارش", "value": 75 },
|
||||
{ "name": "خاک", "value": 88 },
|
||||
{ "name": "آب", "value": 70 }
|
||||
],
|
||||
"area_hectares": 2.25
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**فیلدهای دیتای تکمیلی:**
|
||||
|
||||
| فیلد | نوع | اجباری | توضیح |
|
||||
|------|-----|--------|--------|
|
||||
| `zoneId` | string | بله | همان zoneId درخواست |
|
||||
| `crop` | string | بله | محصول پیشنهادی |
|
||||
| `matchPercent` | number | بله | درصد تطابق |
|
||||
| `waterNeed` | string | بله | نیاز آبی |
|
||||
| `estimatedProfit` | string | بله | سود تخمینی |
|
||||
| `reason` | string | بله | **فقط در دیتای تکمیلی** — دلیل پیشنهاد محصول |
|
||||
| `criteria` | object[] | بله | **فقط در دیتای تکمیلی** — معیارها برای نمودار راداری |
|
||||
| `area_hectares` | number | خیر | مساحت این زون بر حسب هکتار |
|
||||
|
||||
### ۳.۴ ساختار `criteria`
|
||||
|
||||
| فیلد | نوع | توضیح |
|
||||
|------|-----|--------|
|
||||
| `name` | string | نام معیار (دما، بارش، خاک، آب) |
|
||||
| `value` | number | امتیاز ۰–۱۰۰ |
|
||||
|
||||
---
|
||||
|
||||
## ۴. مساحت کلی (Total Area)
|
||||
|
||||
در پاسخ **API دیتای اولیه زونها** برمیگردد:
|
||||
|
||||
| فیلد | نوع | توضیح |
|
||||
|------|-----|--------|
|
||||
| `total_area_hectares` | number | مساحت کل منطقه بر حسب هکتار |
|
||||
| `total_area_sqm` | number | مساحت کل بر حسب متر مربع |
|
||||
|
||||
---
|
||||
|
||||
## ۵. خلاصهٔ ساختارهای مورد نیاز فرانت
|
||||
|
||||
### دیتای اولیه زون (برای نقشه و هاور/tooltip)
|
||||
|
||||
```ts
|
||||
interface ZoneInitialData {
|
||||
zoneId: string
|
||||
geometry: Polygon
|
||||
crop: string
|
||||
matchPercent: number
|
||||
waterNeed: string
|
||||
estimatedProfit: string
|
||||
}
|
||||
```
|
||||
|
||||
### دیتای تکمیلی زون (برای پنل جزئیات — پس از کلیک)
|
||||
|
||||
```ts
|
||||
interface ZoneDetailData {
|
||||
zoneId: string
|
||||
crop: string
|
||||
matchPercent: number
|
||||
waterNeed: string
|
||||
estimatedProfit: string
|
||||
reason: string
|
||||
criteria: { name: string; value: number }[]
|
||||
area_hectares?: number
|
||||
}
|
||||
```
|
||||
|
||||
### محصولات و رنگها (پیشفرض فرانت)
|
||||
|
||||
```ts
|
||||
const CROP_COLORS: Record<CropType, string> = {
|
||||
wheat: '#6bcb77',
|
||||
canola: '#ffd93d',
|
||||
saffron: '#9b59b6'
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ۶. جریان فرانت با APIها
|
||||
|
||||
1. **لود صفحه:** `GET /api/crop-zoning/products/` → لیست محصولات و رنگها.
|
||||
2. **رسم منطقه / بهینهسازی:** فرانت با Turf از polygon منطقه گرید میسازد → `POST /api/crop-zoning/zones/initial/` با `zones` (FeatureCollection) → نقشه و tooltip با دیتای محصولات رسم میشود.
|
||||
3. **تغییر لایه در LayerControl:** برای هر لایه یک درخواست جداگانه ارسال میشود:
|
||||
- محصولات پیشنهادی: `POST zones/initial/` (در مرحلهٔ ۲)
|
||||
- نیاز آبی: `POST zones/water-need/` → نقشه و Legend بهروزرسانی میشوند
|
||||
- کیفیت خاک: `POST zones/soil-quality/` → نقشه و Legend بهروزرسانی میشوند
|
||||
- ریسک کشت: `POST zones/cultivation-risk/` → نقشه و Legend بهروزرسانی میشوند
|
||||
4. **کلیک روی مربع:** `GET /api/crop-zoning/zones/{zoneId}/details/` → دیتای تکمیلی → پنل جزئیات باز میشود (reason, criteria, نمودار راداری).
|
||||
|
||||
---
|
||||
|
||||
## ۷. وضعیت فعلی و نیازمندیها
|
||||
|
||||
- در حال حاضر زونبندی با **دیتای ماک** و الگوریتم محلی (`createZonedGrid` در `cropZoningUtils.ts`) کار میکند.
|
||||
- برای اتصال به بکاند، لازم است:
|
||||
1. سرویس `cropZoningService` با سه endpoint: `getProducts()`, `getZonesInitial(zones)`, `getZoneDetails(zoneId)` ایجاد شود.
|
||||
2. در `CropZoningMap` به جای `createZonedGrid` ابتدا گرید با Turf ساخته شود، سپس `zones` به API ارسال و پاسخ برای رسم استفاده شود.
|
||||
3. در `onZoneClick` قبل از باز کردن پنل، `getZoneDetails(zoneId)` صدا زده شود و دیتای تکمیلی به `ZoneDetailPanel` پاس داده شود.
|
||||
4. مساحت کلی (`total_area_hectares`) در پاسخ initial در UI نمایش داده شود.
|
||||
@@ -1,653 +0,0 @@
|
||||
# مستندات API داشبورد Farm (Farm Dashboard)
|
||||
|
||||
این سند شامل توضیحات کل داشبورد، APIهای تنظیمات (disable/enable/move کارتها) و ساختار پیشنهادی ریسپانس برای محتوای کارتها است.
|
||||
|
||||
---
|
||||
|
||||
## ۱. نمای کلی داشبورد
|
||||
|
||||
داشبورد Farm از کامپوننت `FarmDashboardWrapper` استفاده میکند و شامل ردیفها (rows) و کارتهای (cards) زیر است:
|
||||
|
||||
| Row ID | Row Label | کارتها |
|
||||
|--------|-----------|---------|
|
||||
| `overviewKpis` | Overview KPIs | `farmOverviewKpis` |
|
||||
| `weatherAlerts` | Weather & Alerts | `farmWeatherCard`, `farmAlertsTracker` |
|
||||
| `sensorMonitoring` | Sensor Monitoring | `sensorValuesList`, `sensorRadarChart` |
|
||||
| `sensorCharts` | Sensor Charts | `sensorComparisonChart`, `anomalyDetectionCard` |
|
||||
| `alertsWater` | Alerts & Water Prediction | `farmAlertsTimeline`, `waterNeedPrediction` |
|
||||
| `predictions` | Predictions | `harvestPredictionCard`, `yieldPredictionChart` |
|
||||
| `soilHeatmap` | Soil Moisture Heatmap | `soilMoistureHeatmap` |
|
||||
| `ndviRecommendations` | NDVI & Recommendations | `ndviHealthCard`, `recommendationsList` |
|
||||
| `economic` | Economic Overview | `economicOverview` |
|
||||
|
||||
---
|
||||
|
||||
## ۲. APIهای تنظیمات داشبورد
|
||||
|
||||
### ۲.۱ دریافت تنظیمات داشبورد (Get Config)
|
||||
|
||||
```
|
||||
GET /api/farm-dashboard-config
|
||||
```
|
||||
|
||||
**توضیح:** تنظیمات شخصیسازی داشبورد کاربر لاگینشده را برمیگرداند.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "OK",
|
||||
"data": {
|
||||
"disabled_card_ids": ["farmWeatherCard", "sensorRadarChart"],
|
||||
"row_order": [
|
||||
"overviewKpis",
|
||||
"weatherAlerts",
|
||||
"sensorMonitoring",
|
||||
"sensorCharts",
|
||||
"alertsWater",
|
||||
"predictions",
|
||||
"soilHeatmap",
|
||||
"ndviRecommendations",
|
||||
"economic"
|
||||
],
|
||||
"enable_drag_reorder": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**فیلدها:**
|
||||
| فیلد | نوع | توضیح |
|
||||
|------|-----|-------|
|
||||
| `disabled_card_ids` | `string[]` | لیست شناسه کارتهای غیرفعال (hidden) |
|
||||
| `row_order` | `string[]` | ترتیب نمایش ردیفها |
|
||||
| `enable_drag_reorder` | `boolean` | امکان جابجایی ردیفها با drag |
|
||||
|
||||
---
|
||||
|
||||
### ۲.۲ غیرفعال کردن کارت (Disable Card)
|
||||
|
||||
```
|
||||
PATCH /api/farm-dashboard-config
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"disabled_card_ids": ["farmWeatherCard", "sensorRadarChart"]
|
||||
}
|
||||
```
|
||||
|
||||
کارت با شناسه `cardId` به لیست `disabled_card_ids` اضافه میشود و در داشبورد نمایش داده نمیشود.
|
||||
|
||||
---
|
||||
|
||||
### ۲.۳ فعال کردن کارت (Enable Card)
|
||||
|
||||
```
|
||||
PATCH /api/farm-dashboard-config
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"disabled_card_ids": ["farmWeatherCard"]
|
||||
}
|
||||
```
|
||||
|
||||
شناسه کارت از لیست `disabled_card_ids` حذف میشود و کارت دوباره نمایش داده میشود.
|
||||
|
||||
**نکته:** کل لیست `disabled_card_ids` جدید ارسال میشود؛ برای enable باید آرایه بدون آن کارت فرستاده شود.
|
||||
|
||||
---
|
||||
|
||||
### ۲.۴ جابجایی ردیفها (Move Rows)
|
||||
|
||||
```
|
||||
PATCH /api/farm-dashboard-config
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"row_order": [
|
||||
"overviewKpis",
|
||||
"weatherAlerts",
|
||||
"sensorMonitoring",
|
||||
"predictions",
|
||||
"sensorCharts",
|
||||
"alertsWater",
|
||||
"soilHeatmap",
|
||||
"ndviRecommendations",
|
||||
"economic"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
ترتیب ردیفها طبق آرایه `row_order` ذخیره میشود.
|
||||
|
||||
---
|
||||
|
||||
### ۲.۵ تغییر وضعیت Drag Reorder
|
||||
|
||||
```
|
||||
PATCH /api/farm-dashboard-config
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"enable_drag_reorder": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ۳. API دریافت همه دیتای کارتها
|
||||
|
||||
### Endpoint پیشنهادی
|
||||
|
||||
```
|
||||
GET /api/farm-dashboard
|
||||
```
|
||||
|
||||
یا به تفکیک کارت:
|
||||
```
|
||||
GET /api/farm-dashboard/cards
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ۴. لیست کامل ریسپانس هر کارت
|
||||
|
||||
ساختار پیشنهادی response برای محتوای هر کارت (بر اساس دادههای mock فعلی در فرانت):
|
||||
|
||||
### ۴.۱ farmOverviewKpis
|
||||
|
||||
```json
|
||||
{
|
||||
"kpis": [
|
||||
{
|
||||
"id": "farm_health_score",
|
||||
"title": "Farm Health Score",
|
||||
"subtitle": "AI Analysis",
|
||||
"stats": "87%",
|
||||
"avatarColor": "success",
|
||||
"avatarIcon": "tabler-heartbeat",
|
||||
"chipText": "Good",
|
||||
"chipColor": "success"
|
||||
},
|
||||
{
|
||||
"id": "water_stress_index",
|
||||
"title": "Water Stress Index",
|
||||
"subtitle": "Current",
|
||||
"stats": "12%",
|
||||
"avatarColor": "info",
|
||||
"avatarIcon": "tabler-droplet",
|
||||
"chipText": "Low",
|
||||
"chipColor": "success"
|
||||
},
|
||||
{
|
||||
"id": "disease_risk",
|
||||
"title": "Disease Risk",
|
||||
"subtitle": "Last 7 Days",
|
||||
"stats": "Low",
|
||||
"avatarColor": "success",
|
||||
"avatarIcon": "tabler-bug",
|
||||
"chipText": "5%",
|
||||
"chipColor": "success"
|
||||
},
|
||||
{
|
||||
"id": "avg_soil_moisture",
|
||||
"title": "Avg Soil Moisture",
|
||||
"subtitle": "Field-wide",
|
||||
"stats": "65%",
|
||||
"avatarColor": "primary",
|
||||
"avatarIcon": "tabler-plant-2",
|
||||
"chipText": "Optimal",
|
||||
"chipColor": "success"
|
||||
},
|
||||
{
|
||||
"id": "yield_prediction",
|
||||
"title": "Yield Prediction",
|
||||
"subtitle": "This Season",
|
||||
"stats": "42 ton",
|
||||
"avatarColor": "secondary",
|
||||
"avatarIcon": "tabler-chart-bar",
|
||||
"chipText": "+8%",
|
||||
"chipColor": "success"
|
||||
},
|
||||
{
|
||||
"id": "pest_risk",
|
||||
"title": "Pest Risk",
|
||||
"subtitle": "AI Forecast",
|
||||
"stats": "15%",
|
||||
"avatarColor": "warning",
|
||||
"avatarIcon": "tabler-bug-off",
|
||||
"chipText": "Monitor",
|
||||
"chipColor": "warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ۴.۲ farmWeatherCard
|
||||
|
||||
```json
|
||||
{
|
||||
"condition": "Clear",
|
||||
"temperature": 24,
|
||||
"unit": "°C",
|
||||
"humidity": 45,
|
||||
"windSpeed": 12,
|
||||
"windUnit": "km/h",
|
||||
"chartData": {
|
||||
"labels": ["6am", "9am", "12pm", "3pm", "6pm", "9pm", "12am"],
|
||||
"series": [[18, 22, 26, 28, 25, 20, 18]]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ۴.۳ farmAlertsTracker
|
||||
|
||||
```json
|
||||
{
|
||||
"totalAlerts": 3,
|
||||
"radialBarValue": 30,
|
||||
"alertStats": [
|
||||
{
|
||||
"title": "Water Shortage",
|
||||
"count": "2",
|
||||
"avatarColor": "error",
|
||||
"avatarIcon": "tabler-droplet-half-2"
|
||||
},
|
||||
{
|
||||
"title": "Fungal Risk",
|
||||
"count": "1",
|
||||
"avatarColor": "warning",
|
||||
"avatarIcon": "tabler-mushroom"
|
||||
},
|
||||
{
|
||||
"title": "Frost Alert",
|
||||
"count": "0",
|
||||
"avatarColor": "info",
|
||||
"avatarIcon": "tabler-snowflake"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ۴.۴ sensorValuesList
|
||||
|
||||
```json
|
||||
{
|
||||
"sensors": [
|
||||
{
|
||||
"title": "28°C",
|
||||
"subtitle": "Air Temperature",
|
||||
"trendNumber": 2.1,
|
||||
"trend": "positive",
|
||||
"unit": "°C"
|
||||
},
|
||||
{
|
||||
"title": "24°C",
|
||||
"subtitle": "Soil Temperature",
|
||||
"trendNumber": -0.5,
|
||||
"trend": "negative",
|
||||
"unit": "°C"
|
||||
},
|
||||
{
|
||||
"title": "65%",
|
||||
"subtitle": "Air Humidity",
|
||||
"trendNumber": 3.2,
|
||||
"trend": "positive",
|
||||
"unit": "%"
|
||||
},
|
||||
{
|
||||
"title": "42%",
|
||||
"subtitle": "Soil Moisture (10cm)",
|
||||
"trendNumber": -1.8,
|
||||
"trend": "negative",
|
||||
"unit": "%"
|
||||
},
|
||||
{
|
||||
"title": "6.8",
|
||||
"subtitle": "Soil pH",
|
||||
"trendNumber": 0.2,
|
||||
"trend": "positive",
|
||||
"unit": "pH"
|
||||
},
|
||||
{
|
||||
"title": "1.2",
|
||||
"subtitle": "EC (dS/m)",
|
||||
"trendNumber": 0.1,
|
||||
"trend": "positive",
|
||||
"unit": "dS/m"
|
||||
},
|
||||
{
|
||||
"title": "850",
|
||||
"subtitle": "Light Intensity (lux)",
|
||||
"trendNumber": 15.3,
|
||||
"trend": "positive",
|
||||
"unit": "lux"
|
||||
},
|
||||
{
|
||||
"title": "12",
|
||||
"subtitle": "Wind Speed (km/h)",
|
||||
"trendNumber": -2.4,
|
||||
"trend": "negative",
|
||||
"unit": "km/h"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ۴.۵ sensorRadarChart
|
||||
|
||||
```json
|
||||
{
|
||||
"labels": ["Temp", "Humidity", "pH", "EC", "Light", "Wind"],
|
||||
"series": [
|
||||
{ "name": "Today", "data": [75, 65, 80, 70, 85, 60] },
|
||||
{ "name": "Ideal", "data": [80, 70, 75, 75, 90, 50] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ۴.۶ sensorComparisonChart
|
||||
|
||||
```json
|
||||
{
|
||||
"currentValue": 48,
|
||||
"vsLastWeek": "+5%",
|
||||
"vsLastWeekValue": 5,
|
||||
"categories": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
|
||||
"series": [
|
||||
{ "name": "Today", "data": [42, 45, 48, 52, 50, 48, 46] },
|
||||
{ "name": "Last Week", "data": [38, 40, 42, 45, 43, 40, 38] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ۴.۷ anomalyDetectionCard
|
||||
|
||||
```json
|
||||
{
|
||||
"anomalies": [
|
||||
{
|
||||
"sensor": "Soil Moisture Z3",
|
||||
"value": "38%",
|
||||
"expected": "45-65%",
|
||||
"deviation": "-12%",
|
||||
"severity": "warning"
|
||||
},
|
||||
{
|
||||
"sensor": "pH Sector 2",
|
||||
"value": "5.2",
|
||||
"expected": "6.0-7.0",
|
||||
"deviation": "-0.8",
|
||||
"severity": "error"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ۴.۸ farmAlertsTimeline
|
||||
|
||||
```json
|
||||
{
|
||||
"alerts": [
|
||||
{
|
||||
"title": "Water Shortage Risk",
|
||||
"description": "Soil moisture at 10cm depth (42%) is below optimal. AI predicts stress in 2-3 days if no irrigation. Recommended: irrigate within 24h.",
|
||||
"time": "15 min ago",
|
||||
"color": "warning"
|
||||
},
|
||||
{
|
||||
"title": "Fungal Disease Risk",
|
||||
"description": "High humidity (65%) + temp 24°C creates favorable conditions for fungal growth. Consider preventive fungicide or reduce irrigation.",
|
||||
"time": "1 hour ago",
|
||||
"color": "error"
|
||||
},
|
||||
{
|
||||
"title": "Irrigation Suggestion",
|
||||
"description": "Optimal watering window: 6:00-8:00 AM. Suggested amount: 450 m³ for Zone A. Expected efficiency gain: 12%.",
|
||||
"time": "2 hours ago",
|
||||
"color": "info"
|
||||
},
|
||||
{
|
||||
"title": "Soil Salinity Check",
|
||||
"description": "EC reading 1.2 dS/m is within range. No action needed. Next check recommended in 5 days.",
|
||||
"time": "4 hours ago",
|
||||
"color": "success"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ۴.۹ waterNeedPrediction
|
||||
|
||||
```json
|
||||
{
|
||||
"totalNext7Days": 3290,
|
||||
"unit": "m³",
|
||||
"categories": ["Day 1", "Day 2", "Day 3", "Day 4", "Day 5", "Day 6", "Day 7"],
|
||||
"series": [{ "name": "Water Need", "data": [420, 450, 480, 460, 490, 510, 480] }]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ۴.۱۰ harvestPredictionCard
|
||||
|
||||
```json
|
||||
{
|
||||
"date": "2025-10-15",
|
||||
"dateFormatted": "Oct 15, 2025",
|
||||
"daysUntil": 58,
|
||||
"description": "Based on current GDD accumulation and weather forecast. Optimal harvest window: Oct 12-18.",
|
||||
"optimalWindowStart": "2025-10-12",
|
||||
"optimalWindowEnd": "2025-10-18"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ۴.۱۱ yieldPredictionChart
|
||||
|
||||
```json
|
||||
{
|
||||
"categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
|
||||
"series": [
|
||||
{ "name": "This Year", "data": [35, 38, 40, 42, 45, 48, 50, 48, 46, 44, 42, 42] },
|
||||
{ "name": "Last Year", "data": [32, 34, 36, 38, 40, 42, 44, 42, 40, 38, 36, 38] }
|
||||
],
|
||||
"summary": [
|
||||
{ "title": "Predicted Yield", "subtitle": "This Season", "amount": "42 ton", "avatarColor": "primary", "avatarIcon": "tabler-chart-bar" },
|
||||
{ "title": "Harvest Date", "subtitle": "Est. Oct 15", "amount": "+8%", "avatarColor": "success", "avatarIcon": "tabler-calendar" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ۴.۱۲ soilMoistureHeatmap
|
||||
|
||||
```json
|
||||
{
|
||||
"zones": ["Z1", "Z2", "Z3", "Z4", "Z5", "Z6", "Z7"],
|
||||
"hours": ["6h", "8h", "10h", "12h", "14h", "16h", "18h"],
|
||||
"series": [
|
||||
{ "name": "Z1", "data": [{"x": "6h", "y": 52}, {"x": "8h", "y": 48}, {"x": "10h", "y": 55}, {"x": "12h", "y": 60}, {"x": "14h", "y": 58}, {"x": "16h", "y": 54}, {"x": "18h", "y": 50}] },
|
||||
{ "name": "Z2", "data": [{"x": "6h", "y": 45}, {"x": "8h", "y": 42}, {"x": "10h", "y": 48}, {"x": "12h", "y": 52}, {"x": "14h", "y": 50}, {"x": "16h", "y": 47}, {"x": "18h", "y": 44}] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ۴.۱۳ ndviHealthCard
|
||||
|
||||
```json
|
||||
{
|
||||
"ndviIndex": 0.78,
|
||||
"healthData": [
|
||||
{ "title": "Nitrogen Stress", "value": "Low", "color": "success", "icon": "tabler-leaf" },
|
||||
{ "title": "Crop Health", "value": "Good", "color": "success", "icon": "tabler-plant" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ۴.۱۴ recommendationsList
|
||||
|
||||
```json
|
||||
{
|
||||
"recommendations": [
|
||||
{
|
||||
"title": "Irrigation: 6:00-8:00 AM",
|
||||
"subtitle": "450 m³ for Zone A. Without irrigation, yield may drop ~8%.",
|
||||
"avatarIcon": "tabler-droplet",
|
||||
"avatarColor": "primary"
|
||||
},
|
||||
{
|
||||
"title": "Fertilizer: NPK 20-20-20",
|
||||
"subtitle": "Apply 25 kg/ha in 7 days. Current N deficiency in sector 2.",
|
||||
"avatarIcon": "tabler-leaf",
|
||||
"avatarColor": "success"
|
||||
},
|
||||
{
|
||||
"title": "Fungicide: Preventive",
|
||||
"subtitle": "Humidity + temp favor fungi. Consider copper-based spray.",
|
||||
"avatarIcon": "tabler-mushroom",
|
||||
"avatarColor": "warning"
|
||||
},
|
||||
{
|
||||
"title": "Harvest Window: Oct 12-18",
|
||||
"subtitle": "Peak ripeness expected Oct 15. Plan labor accordingly.",
|
||||
"avatarIcon": "tabler-calendar-event",
|
||||
"avatarColor": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ۴.۱۵ economicOverview
|
||||
|
||||
```json
|
||||
{
|
||||
"economicData": [
|
||||
{ "title": "Water Cost", "value": "€720", "subtitle": "This month", "avatarIcon": "tabler-droplet", "avatarColor": "primary" },
|
||||
{ "title": "AI Water Savings", "value": "€156", "subtitle": "18% saved", "avatarIcon": "tabler-bulb", "avatarColor": "success" },
|
||||
{ "title": "Platform ROI", "value": "127%", "subtitle": "vs last year", "avatarIcon": "tabler-chart-line", "avatarColor": "info" },
|
||||
{ "title": "Income Forecast", "value": "€42k", "subtitle": "This season", "avatarIcon": "tabler-currency-euro", "avatarColor": "success" }
|
||||
],
|
||||
"chartSeries": [
|
||||
{ "name": "Water Cost", "data": [120, 115, 110, 125, 118, 122] },
|
||||
{ "name": "Fertilizer", "data": [80, 85, 90, 75, 82, 78] }
|
||||
],
|
||||
"chartCategories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ۵. Response یکپارچه همه کارتها
|
||||
|
||||
اگر یک endpoint برای کل دیتای داشبورد داشته باشید:
|
||||
|
||||
```
|
||||
GET /api/farm-dashboard
|
||||
```
|
||||
|
||||
**Response پیشنهادی:**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "OK",
|
||||
"data": {
|
||||
"farmOverviewKpis": { ... },
|
||||
"farmWeatherCard": { ... },
|
||||
"farmAlertsTracker": { ... },
|
||||
"sensorValuesList": { ... },
|
||||
"sensorRadarChart": { ... },
|
||||
"sensorComparisonChart": { ... },
|
||||
"anomalyDetectionCard": { ... },
|
||||
"farmAlertsTimeline": { ... },
|
||||
"waterNeedPrediction": { ... },
|
||||
"harvestPredictionCard": { ... },
|
||||
"yieldPredictionChart": { ... },
|
||||
"soilMoistureHeatmap": { ... },
|
||||
"ndviHealthCard": { ... },
|
||||
"recommendationsList": { ... },
|
||||
"economicOverview": { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ۶. خلاصه Endpoints
|
||||
|
||||
| عملیات | Method | Endpoint | Body |
|
||||
|--------|--------|----------|------|
|
||||
| دریافت تنظیمات | GET | `/api/farm-dashboard-config` | - |
|
||||
| غیرفعال کردن کارت | PATCH | `/api/farm-dashboard-config` | `{ "disabled_card_ids": [...] }` |
|
||||
| فعال کردن کارت | PATCH | `/api/farm-dashboard-config` | `{ "disabled_card_ids": [...] }` |
|
||||
| جابجایی ردیفها | PATCH | `/api/farm-dashboard-config` | `{ "row_order": [...] }` |
|
||||
| Enable/Disable Drag | PATCH | `/api/farm-dashboard-config` | `{ "enable_drag_reorder": boolean }` |
|
||||
| دیتای همه کارتها | GET | `/api/farm-dashboard` یا `/api/farm-dashboard/cards` | - |
|
||||
|
||||
---
|
||||
|
||||
## ۷. Card IDs معتبر
|
||||
|
||||
```
|
||||
farmOverviewKpis
|
||||
farmWeatherCard
|
||||
farmAlertsTracker
|
||||
sensorValuesList
|
||||
sensorRadarChart
|
||||
sensorComparisonChart
|
||||
anomalyDetectionCard
|
||||
farmAlertsTimeline
|
||||
waterNeedPrediction
|
||||
harvestPredictionCard
|
||||
yieldPredictionChart
|
||||
soilMoistureHeatmap
|
||||
ndviHealthCard
|
||||
recommendationsList
|
||||
economicOverview
|
||||
```
|
||||
|
||||
## ۸. Row IDs معتبر
|
||||
|
||||
```
|
||||
overviewKpis
|
||||
weatherAlerts
|
||||
sensorMonitoring
|
||||
sensorCharts
|
||||
alertsWater
|
||||
predictions
|
||||
soilHeatmap
|
||||
ndviRecommendations
|
||||
economic
|
||||
```
|
||||
@@ -1,192 +0,0 @@
|
||||
# مستندات APIهای دستیار هوشمند مزرعه (Farm AI Assistant)
|
||||
|
||||
این سند تمام APIهای مورد نیاز برای صفحه **Farm AI Assistant** را شرح میدهد: ورودیها، خروجیها و استفاده در UI.
|
||||
|
||||
**مسیر صفحه:** `(dashboard)/(private)/farm-ai-assistant`
|
||||
**کامپوننت اصلی:** `FarmAiAssistantChat`
|
||||
|
||||
---
|
||||
|
||||
## نمای کلی
|
||||
|
||||
دستیار هوشمند مزرعه برای کار به موارد زیر نیاز دارد:
|
||||
|
||||
| ردیف | API | هدف |
|
||||
|------|-----|------|
|
||||
| ۱ | **ارسال پیام به دستیار (Chat/Complete)** | دریافت پاسخ ساختیافته (توصیه، لیست، هشدار) بر اساس پیام کاربر و زمینه مزرعه |
|
||||
| ۲ | **دریافت زمینه مزرعه (Farm Context)** | پر کردن نوار «زمینه مزرعه» (نوع خاک، EC آب، محصول، مرحله رشد، آخرین آبیاری) |
|
||||
| ۳ | **توصیه آبیاری** | در صورت درخواست کاربر یا تصمیم دستیار برای توصیه آبیاری |
|
||||
| ۴ | **توصیه کوددهی** | در صورت درخواست کاربر یا توصیه کود |
|
||||
| ۵ | **تشخیص آفت از تصویر** | وقتی کاربر تصویر گیاه را ارسال میکند |
|
||||
|
||||
---
|
||||
|
||||
## ۱. API ارسال پیام به دستیار (Farm AI Chat)
|
||||
|
||||
این API هسته اصلی دستیار است و در حال حاضر در فرانت با پاسخ دمو شبیهسازی شده است؛ باید با API واقعی جایگزین شود.
|
||||
|
||||
### ۱.۱ مشخصات
|
||||
|
||||
- **متد:** `POST`
|
||||
- **آدرس پیشنهادی:** `POST /api/farm-ai-assistant/chat/` یا `POST /api/farm-ai-assistant/messages/`
|
||||
- **هدف:** ارسال پیام کاربر (و در صورت وجود تصویر) به همراه زمینه مزرعه و دریافت پاسخ ساختیافته دستیار.
|
||||
|
||||
### ۱.۲ ورودی (Request Body)
|
||||
|
||||
| فیلد | نوع | اجباری | توضیح |
|
||||
|------|-----|--------|--------|
|
||||
| `content` | string | بله | متن پیام کاربر |
|
||||
| `images` | string[] یا base64[] | خیر | آرایه آدرس تصاویر یا داده base64 (در صورت استفاده از آپلود تصویر دوربین در چت) |
|
||||
| `conversation_id` | string | خیر | شناسه مکالمه برای ادامه گفتگو؛ در اولین پیام ارسال نشود |
|
||||
| `farm_context` | object | توصیه | زمینه مزرعه برای پاسخ شخصیسازیشده (در صورت نبودن، بکاند میتواند از پیشفرض استفاده کند) |
|
||||
|
||||
ساختار پیشنهادی `farm_context` (همخوان با `FarmContext` در فرانت):
|
||||
|
||||
```json
|
||||
{
|
||||
"content": "برنامه آبیاری برای گوجه در مرحله گلدهی چطور باشد؟",
|
||||
"farm_context": {
|
||||
"soilType": "Loamy",
|
||||
"waterEC": "1.2 dS/m",
|
||||
"selectedCrop": "Tomato",
|
||||
"growthStage": "Flowering",
|
||||
"lastIrrigationStatus": "2 days ago"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
اگر از **تصویر** استفاده شود (دکمه دوربین در input):
|
||||
|
||||
```json
|
||||
{
|
||||
"content": "این برگ زرد شده، چه مشکلی داره؟",
|
||||
"images": ["data:image/jpeg;base64,..."],
|
||||
"farm_context": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### ۱.۳ خروجی (Response Body)
|
||||
|
||||
پاسخ باید شامل **بخشهای ساختیافته** (sections) باشد تا در UI به صورت کارت (توصیه، لیست، هشدار) رندر شود.
|
||||
|
||||
**قالب پیشنهادی:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"message_id": "a-1739123456789",
|
||||
"conversation_id": "conv-abc123",
|
||||
"content": "",
|
||||
"sections": [
|
||||
{
|
||||
"type": "recommendation",
|
||||
"title": "Irrigation recommendation",
|
||||
"icon": "droplet",
|
||||
"frequency": "3 times per week",
|
||||
"amount": "15–20 L per plant",
|
||||
"timing": "Early morning (05:00–07:00)",
|
||||
"expandableExplanation": "Your loamy soil holds moisture well..."
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"title": "Key points",
|
||||
"icon": "leaf",
|
||||
"items": [
|
||||
"Avoid midday watering to reduce evaporation",
|
||||
"Drip irrigation preferred for root zone targeting"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "warning",
|
||||
"title": "Weather advisory",
|
||||
"icon": "warning",
|
||||
"content": "High temps forecasted next week. Consider increasing frequency."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**ساختار هر بخش (Section) مطابق `AIResponseSection` در فرانت:**
|
||||
|
||||
| فیلد | نوع | اجباری | توضیح |
|
||||
|------|-----|--------|--------|
|
||||
| `type` | string | بله | یکی از: `text` \| `list` \| `recommendation` \| `warning` |
|
||||
| `title` | string | خیر | عنوان بخش |
|
||||
| `content` | string | خیر | برای `type: "text"` یا `type: "warning"` |
|
||||
| `items` | string[] | خیر | برای `type: "list"` |
|
||||
| `icon` | string | خیر | یکی از: `droplet` \| `leaf` \| `warning` \| `fertilizer` \| `calendar` |
|
||||
| `frequency` | string | خیر | فقط برای `recommendation`: تعداد دفعات (مثلاً در هفته) |
|
||||
| `amount` | string | خیر | فقط برای `recommendation`: مقدار (مثلاً لیتر یا کیلوگرم) |
|
||||
| `timing` | string | خیر | فقط برای `recommendation`: زمان پیشنهادی |
|
||||
| `expandableExplanation` | string | خیر | فقط برای `recommendation`: توضیح قابل گسترش «چرا این توصیه» |
|
||||
|
||||
- اگر `content` خالی باشد و فقط `sections` برگردد، در UI فقط کارتها نمایش داده میشوند (مطابق پیادهسازی فعلی).
|
||||
- در صورت خطا انتظار میرود پاسخ با `status: "error"` و پیام مناسب برگردد.
|
||||
|
||||
---
|
||||
|
||||
## ۲. API دریافت زمینه مزرعه (Farm Context)
|
||||
|
||||
برای پر کردن نوار «زمینه مزرعه» در بالای چت (نوع خاک، EC آب، محصول انتخابشده، مرحله رشد، آخرین آبیاری).
|
||||
|
||||
- **وضعیت:** در بکاند فعلی endpoint اختصاصی برای «یک جا گرفتن» زمینه مزرعه وجود ندارد.
|
||||
- **راهحل فعلی:** فرانت میتواند داده را از منابع موجود جمع کند:
|
||||
- **توصیه آبیاری:** `GET /api/irrigation-recommendation/config/` → `farmInfo` (soilType, waterQuality, climateZone) و `cropOptions`
|
||||
- **توصیه کوددهی:** `GET /api/fertilization-recommendation/config/` → `farmData` (soilType, organicMatter, waterEC)، `growthStages`، `cropOptions`
|
||||
- **پیشنهاد برای آینده:** یک endpoint مثل `GET /api/farm-ai-assistant/context/` یا `GET /api/farm-dashboard-config/...` که یک آبجکت همخوان با `FarmContext` فرانت برگرداند (مثلاً `soilType`, `waterEC`, `selectedCrop`, `growthStage`, `lastIrrigationStatus`).
|
||||
|
||||
---
|
||||
|
||||
## ۳. API توصیه آبیاری
|
||||
|
||||
وقتی کاربر در چت درخواست توصیه آبیاری میکند (یا دستیار تصمیم میگیرد توصیه آبیاری بدهد)، میتوان از API موجود استفاده کرد.
|
||||
|
||||
- **Config (برای فرم/گزینهها):** `GET /api/irrigation-recommendation/config/`
|
||||
- **توصیه (برنامه آبیاری):** `POST /api/irrigation-recommendation/recommend/`
|
||||
|
||||
ورودی پیشنهادی برای `recommend`: `crop_id`, `soilType`, `waterQuality`, `climateZone` (در نسخه فعلی mock است و در پاسخ استفاده نمیشود).
|
||||
|
||||
جزئیات و نمونه پاسخ: [RECOMMENDATION_APIS.md](./RECOMMENDATION_APIS.md#۱-توصیه-آبیاری-irrigation-recommendation).
|
||||
|
||||
---
|
||||
|
||||
## ۴. API توصیه کوددهی
|
||||
|
||||
در صورت درخواست کاربر برای توصیه کود یا تصمیم دستیار برای دادن توصیه کود.
|
||||
|
||||
- **Config:** `GET /api/fertilization-recommendation/config/`
|
||||
- **توصیه:** `POST /api/fertilization-recommendation/recommend/`
|
||||
|
||||
ورودی پیشنهادی برای `recommend`: `crop_id`, `growth_stage`, `soilType`, `organicMatter`, `waterEC` (در نسخه فعلی mock است).
|
||||
|
||||
جزئیات و نمونه پاسخ: [RECOMMENDATION_APIS.md](./RECOMMENDATION_APIS.md#۳-توصیه-کوددهی-fertilization-recommendation).
|
||||
|
||||
---
|
||||
|
||||
## ۵. API تشخیص آفت از تصویر
|
||||
|
||||
وقتی کاربر در چت تصویر گیاه را ارسال میکند (دکمه دوربین یا آپلود).
|
||||
|
||||
- **تحلیل تصویر:** `POST /api/pest-detection/analyze/`
|
||||
- **ورودی:** بدن درخواست میتواند شامل تصویر (مثلاً form-data با فایل یا JSON با base64) باشد. در نسخه فعلی پاسخ ثابت (mock) است.
|
||||
- **خروجی:** `pest`, `confidence`, `description`, `treatment`
|
||||
|
||||
جزئیات و نمونه پاسخ: [RECOMMENDATION_APIS.md](./RECOMMENDATION_APIS.md#۲-تشخیص-آفت-pest-detection).
|
||||
|
||||
---
|
||||
|
||||
## خلاصه Endpointها برای Farm AI Assistant
|
||||
|
||||
| ردیف | API | متد | Endpoint | وضعیت |
|
||||
|------|-----|------|----------|--------|
|
||||
| ۱ | Farm AI Chat | POST | `/api/farm-ai-assistant/chat/` | موجود |
|
||||
| ۱.۱ | Farm AI Chat Task Create | POST | `/api/farm-ai-assistant/chat/task/` | موجود |
|
||||
| ۱.۲ | Farm AI Chat Task Status | GET | `/api/farm-ai-assistant/chat/task/{task_id}/status/` | موجود |
|
||||
| ۲ | Farm Context | GET | `/api/farm-ai-assistant/context/` (پیشنهادی) | **پیادهسازی نشده**؛ استفاده از configهای آبیاری/کوددهی |
|
||||
| ۳ | توصیه آبیاری | GET | `/api/irrigation-recommendation/config/` | موجود (mock) |
|
||||
| ۳ | توصیه آبیاری | POST | `/api/irrigation-recommendation/recommend/` | موجود (mock) |
|
||||
| ۴ | توصیه کوددهی | GET | `/api/fertilization-recommendation/config/` | موجود (mock) |
|
||||
| ۴ | توصیه کوددهی | POST | `/api/fertilization-recommendation/recommend/` | موجود (mock) |
|
||||
| ۵ | تشخیص آفت | POST | `/api/pest-detection/analyze/` | موجود (mock) |
|
||||
@@ -0,0 +1,546 @@
|
||||
# مستند نحوه ارتباط با سرویس `farm_ai_assistant`
|
||||
|
||||
این فایل برای تیم بکاند تهیه شده و صرفاً بر اساس مسیرهای فعال در `farm_ai_assistant/urls.py` و رفتار فعلی ویوها نوشته شده است.
|
||||
|
||||
## Base URL
|
||||
|
||||
تمام endpointهای این سرویس با prefix زیر در دسترس هستند:
|
||||
|
||||
```text
|
||||
/api/farm-ai-assistant/
|
||||
```
|
||||
|
||||
نمونه کامل:
|
||||
|
||||
```text
|
||||
https://<domain>/api/farm-ai-assistant/
|
||||
```
|
||||
|
||||
## احراز هویت
|
||||
|
||||
تمام endpointهای عملیاتی این ماژول نیاز به احراز هویت دارند:
|
||||
|
||||
- نوع دسترسی: `IsAuthenticated`
|
||||
- هدر موردنیاز:
|
||||
|
||||
```http
|
||||
Authorization: Bearer <access_token>
|
||||
```
|
||||
|
||||
> اگر کاربر لاگین نباشد، درخواست با خطای احراز هویت رد میشود.
|
||||
|
||||
## وضعیت endpointها
|
||||
|
||||
### endpointهای فعال در `farm_ai_assistant/urls.py`
|
||||
|
||||
| Method | Endpoint | کاربرد |
|
||||
|---|---|---|
|
||||
| `GET` | `/context/` | دریافت context نمایشی/پیشفرض برای UI |
|
||||
| `POST` | `/chat/task/` | ثبت پیام کاربر و ساخت task در سرویس AI |
|
||||
| `GET` | `/chat/task/<task_id>/status/` | بررسی وضعیت task و دریافت نتیجه نهایی |
|
||||
| `GET` | `/chats/` | لیست گفتگوهای کاربر |
|
||||
| `POST` | `/chats/` | ایجاد conversation خالی |
|
||||
| `DELETE` | `/chats/<conversation_id>/` | حذف conversation |
|
||||
| `GET` | `/chats/<conversation_id>/messages/` | دریافت پیامهای یک conversation |
|
||||
|
||||
### endpoint غیرفعال
|
||||
|
||||
endpoint زیر در فایل `urls.py` کامنت شده و در حال حاضر قابل استفاده نیست:
|
||||
|
||||
```text
|
||||
POST /api/farm-ai-assistant/chat/
|
||||
```
|
||||
|
||||
یعنی در وضعیت فعلی، جریان اصلی ارتباط با AI به شکل **asynchronous task-based** پیادهسازی شده است.
|
||||
|
||||
---
|
||||
|
||||
## 1) دریافت context
|
||||
|
||||
### Request
|
||||
|
||||
```http
|
||||
GET /api/farm-ai-assistant/context/
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"...": "context mock/initial data"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### توضیح
|
||||
|
||||
- این endpoint فعلاً داده context را از `mock_data` برمیگرداند.
|
||||
- برای preload کردن اطلاعات اولیه UI مناسب است.
|
||||
- این endpoint به سرویس خارجی AI کال نمیزند.
|
||||
|
||||
---
|
||||
|
||||
## 2) ایجاد task برای ارسال پیام به AI
|
||||
|
||||
این endpoint مهمترین مسیر ارتباطی با سرویس AI است.
|
||||
|
||||
### Request
|
||||
|
||||
```http
|
||||
POST /api/farm-ai-assistant/chat/task/
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
### Body
|
||||
|
||||
```json
|
||||
{
|
||||
"content": "برای گوجه در مرحله گلدهی برنامه آبیاری بده",
|
||||
"images": [],
|
||||
"conversation_id": "optional-uuid",
|
||||
"title": "optional title",
|
||||
"farm_context": {
|
||||
"soilType": "Loamy",
|
||||
"waterEC": "1.2 dS/m",
|
||||
"selectedCrop": "Tomato",
|
||||
"growthStage": "Flowering"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### فیلدها
|
||||
|
||||
| فیلد | نوع | اجباری | توضیح |
|
||||
|---|---|---|---|
|
||||
| `content` | `string` | اختیاری* | متن پیام کاربر |
|
||||
| `images` | `string[]` | اختیاری | لیست URL/base64 تصویر |
|
||||
| `conversation_id` | `uuid` | اختیاری | اگر گفتگو از قبل وجود دارد |
|
||||
| `title` | `string` | اختیاری | عنوان گفتگو |
|
||||
| `farm_context` | `object` | اختیاری | context مزرعه |
|
||||
|
||||
> حداقل یکی از `content` یا `images` باید ارسال شود. در غیر این صورت validation error برمیگردد.
|
||||
|
||||
### رفتار بکاند داخلی
|
||||
|
||||
در این endpoint این اتفاقها میافتد:
|
||||
|
||||
1. اگر `conversation_id` ارسال شده باشد، همان conversation متعلق به همان کاربر لود میشود.
|
||||
2. اگر `conversation_id` ارسال نشده باشد، یک conversation جدید ساخته میشود.
|
||||
3. یک message با `role=user` در دیتابیس ذخیره میشود.
|
||||
4. سپس بکاند به سرویس خارجی AI درخواست میزند.
|
||||
|
||||
### payload ارسالی به سرویس خارجی AI
|
||||
|
||||
بکاند این payload را به سرویس خارجی ارسال میکند:
|
||||
|
||||
```json
|
||||
{
|
||||
"content": "...",
|
||||
"query": "...",
|
||||
"images": [],
|
||||
"conversation_id": "conversation-uuid",
|
||||
"user_id": 123,
|
||||
"farm_context": {},
|
||||
"title": "..."
|
||||
}
|
||||
```
|
||||
|
||||
### مسیر سرویس خارجی AI
|
||||
|
||||
```text
|
||||
POST /rag/chat/generate
|
||||
```
|
||||
|
||||
### Response موفق
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"task_id": "abc123",
|
||||
"status": "PENDING",
|
||||
"status_url": "/tasks/abc123/status",
|
||||
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
|
||||
"message_id": "5d3f7a8c-9f2e-4d0a-b56f-1f2c2f9c1a22"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### کدهای وضعیت محتمل
|
||||
|
||||
| Status Code | معنی |
|
||||
|---|---|
|
||||
| `202` | task ساخته شده و باید polling انجام شود |
|
||||
| `4xx/5xx` | خطای دریافتی از سرویس خارجی |
|
||||
| `503` | سرویس خارجی AI در دسترس نیست |
|
||||
|
||||
---
|
||||
|
||||
## 3) بررسی وضعیت task
|
||||
|
||||
فرانت یا سرویس مصرفکننده باید با `task_id` وضعیت را poll کند.
|
||||
|
||||
### Request
|
||||
|
||||
```http
|
||||
GET /api/farm-ai-assistant/chat/task/<task_id>/status/
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
### مسیر سرویس خارجی AI
|
||||
|
||||
بکاند این درخواست را به سرویس خارجی پاس میدهد:
|
||||
|
||||
```text
|
||||
GET /tasks/<task_id>/status
|
||||
```
|
||||
|
||||
### Response در حالت در حال پردازش
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"task_id": "abc123",
|
||||
"status": "PENDING",
|
||||
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
|
||||
"progress": {
|
||||
"message": "Processing request"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Response در حالت موفق
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"task_id": "abc123",
|
||||
"status": "SUCCESS",
|
||||
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
|
||||
"result": {
|
||||
"message_id": "9f3f8f61-cc71-4f70-a650-2f4dc6f4e5c2",
|
||||
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
|
||||
"content": "پیشنهاد آبیاری شما آماده است",
|
||||
"sections": [
|
||||
{
|
||||
"type": "recommendation",
|
||||
"title": "Irrigation recommendation",
|
||||
"icon": "droplet",
|
||||
"frequency": "3 times per week",
|
||||
"amount": "15–20 L per plant",
|
||||
"timing": "Early morning",
|
||||
"expandableExplanation": "..."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Response در حالت خطا
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"task_id": "abc123",
|
||||
"status": "FAILURE",
|
||||
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
|
||||
"error": "something went wrong"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### نکته مهم
|
||||
|
||||
اگر `status=SUCCESS` باشد و `result` از سرویس خارجی دریافت شود:
|
||||
|
||||
- نتیجه نهایی به message دستیار تبدیل و در دیتابیس ذخیره میشود.
|
||||
- یک `assistant message` داخل همان conversation ساخته یا بهروزرسانی میشود.
|
||||
- خروجی `result` در پاسخ API به فرم نهایی UI normalize میشود.
|
||||
|
||||
---
|
||||
|
||||
## 4) لیست گفتگوها
|
||||
|
||||
### Request
|
||||
|
||||
```http
|
||||
GET /api/farm-ai-assistant/chats/
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
|
||||
"message_count": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### توضیح
|
||||
|
||||
- فقط conversationهای متعلق به همان کاربر لاگینشده برگردانده میشود.
|
||||
- مرتبسازی بر اساس `updated_at desc` و سپس `created_at desc` است.
|
||||
|
||||
---
|
||||
|
||||
## 5) ایجاد conversation خالی
|
||||
|
||||
### Request
|
||||
|
||||
```http
|
||||
POST /api/farm-ai-assistant/chats/
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
### Body
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "مشاوره آبیاری",
|
||||
"farm_context": {
|
||||
"soilType": "Loamy"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
|
||||
"message_count": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### کد وضعیت
|
||||
|
||||
```text
|
||||
201 Created
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6) دریافت پیامهای یک conversation
|
||||
|
||||
### Request
|
||||
|
||||
```http
|
||||
GET /api/farm-ai-assistant/chats/<conversation_id>/messages/
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
|
||||
"messages": [
|
||||
{
|
||||
"message_id": "11111111-1111-1111-1111-111111111111",
|
||||
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
|
||||
"role": "user",
|
||||
"content": "برای گوجه آبیاری پیشنهاد بده",
|
||||
"sections": [],
|
||||
"images": [],
|
||||
"created_at": "2025-03-27T12:00:00Z"
|
||||
},
|
||||
{
|
||||
"message_id": "22222222-2222-2222-2222-222222222222",
|
||||
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"sections": [
|
||||
{
|
||||
"type": "list",
|
||||
"title": "Key points",
|
||||
"items": [
|
||||
"Avoid midday watering",
|
||||
"Use drip irrigation"
|
||||
]
|
||||
}
|
||||
],
|
||||
"images": [],
|
||||
"created_at": "2025-03-27T12:00:05Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### توضیح
|
||||
|
||||
- `role` یکی از دو مقدار `user` یا `assistant` است.
|
||||
- برای messageهای `assistant`، فیلد `sections` میتواند پر باشد.
|
||||
- برای messageهای `user`، معمولاً `sections` خالی است.
|
||||
|
||||
---
|
||||
|
||||
## 7) حذف conversation
|
||||
|
||||
### Request
|
||||
|
||||
```http
|
||||
DELETE /api/farm-ai-assistant/chats/<conversation_id>/
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## فرمت `sections` در پاسخ نهایی AI
|
||||
|
||||
بخش `sections` برای render شدن خروجی دستیار در UI استفاده میشود.
|
||||
|
||||
### ساختار هر section
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "recommendation",
|
||||
"title": "Irrigation recommendation",
|
||||
"content": "",
|
||||
"items": [],
|
||||
"icon": "droplet",
|
||||
"frequency": "3 times per week",
|
||||
"amount": "15–20 L per plant",
|
||||
"timing": "Early morning",
|
||||
"expandableExplanation": "..."
|
||||
}
|
||||
```
|
||||
|
||||
### مقادیر مجاز `type`
|
||||
|
||||
- `text`
|
||||
- `list`
|
||||
- `recommendation`
|
||||
- `warning`
|
||||
|
||||
### نکات نرمالسازی
|
||||
|
||||
بکاند فقط این کلیدها را از پاسخ AI نگه میدارد:
|
||||
|
||||
- `type`
|
||||
- `title`
|
||||
- `content`
|
||||
- `items`
|
||||
- `icon`
|
||||
- `frequency`
|
||||
- `amount`
|
||||
- `timing`
|
||||
- `expandableExplanation`
|
||||
|
||||
اگر سرویس خارجی فیلد اضافی برگرداند، در response نهایی حذف میشود.
|
||||
|
||||
---
|
||||
|
||||
## قواعد مهم کسبوکاری/فنی
|
||||
|
||||
### مالکیت conversation
|
||||
|
||||
- هر conversation فقط برای owner خودش قابل مشاهده/حذف/دریافت پیام است.
|
||||
- اگر `conversation_id` مربوط به کاربر دیگری باشد، `404 Conversation not found` برمیگردد.
|
||||
|
||||
### title conversation
|
||||
|
||||
- اگر conversation جدید بدون title ساخته شود:
|
||||
- در endpoint `POST /chats/` عنوان پیشفرض `New chat` است.
|
||||
- در endpoint `POST /chat/task/` اگر title داده نشود، از ابتدای `content` یا fallback پیشفرض استفاده میشود.
|
||||
|
||||
### ذخیرهسازی پیامها
|
||||
|
||||
- پیام کاربر قبل از تماس با AI ذخیره میشود.
|
||||
- پاسخ دستیار بعد از اتمام task ذخیره میشود.
|
||||
- داده خام/کمکی در `raw_response` نگهداری میشود.
|
||||
|
||||
### تصاویر
|
||||
|
||||
- فیلد `images` آرایهای از string است.
|
||||
- این string میتواند URL، path یا base64 باشد؛ validation فعلی فقط نوع string را چک میکند.
|
||||
|
||||
---
|
||||
|
||||
## فلو پیشنهادی برای استفاده از API
|
||||
|
||||
### سناریوی استاندارد چت
|
||||
|
||||
1. فرانت در صورت نیاز `GET /context/` را صدا میزند.
|
||||
2. فرانت پیام کاربر را با `POST /chat/task/` ارسال میکند.
|
||||
3. از پاسخ، `task_id` و `conversation_id` دریافت میشود.
|
||||
4. فرانت هر چند ثانیه `GET /chat/task/<task_id>/status/` را poll میکند.
|
||||
5. وقتی `status=SUCCESS` شد، نتیجه نهایی از `result` خوانده میشود.
|
||||
6. برای history کامل، `GET /chats/<conversation_id>/messages/` فراخوانی میشود.
|
||||
|
||||
---
|
||||
|
||||
## نمونه cURL
|
||||
|
||||
### ارسال پیام و ساخت task
|
||||
|
||||
```bash
|
||||
curl -X POST 'https://<domain>/api/farm-ai-assistant/chat/task/' \
|
||||
-H 'Authorization: Bearer <token>' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"content": "برای خیار گلخانهای برنامه آبیاری بده",
|
||||
"farm_context": {
|
||||
"soilType": "Sandy Loam",
|
||||
"selectedCrop": "Cucumber",
|
||||
"growthStage": "Flowering"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### بررسی وضعیت task
|
||||
|
||||
```bash
|
||||
curl -X GET 'https://<domain>/api/farm-ai-assistant/chat/task/abc123/status/' \
|
||||
-H 'Authorization: Bearer <token>'
|
||||
```
|
||||
|
||||
### دریافت پیامهای گفتگو
|
||||
|
||||
```bash
|
||||
curl -X GET 'https://<domain>/api/farm-ai-assistant/chats/4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1/messages/' \
|
||||
-H 'Authorization: Bearer <token>'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## جمعبندی سریع برای تیم backend
|
||||
|
||||
- endpoint sync chat فعلاً فعال نیست.
|
||||
- endpoint اصلی برای ارتباط با AI برابر `POST /chat/task/` است.
|
||||
- نتیجه از طریق `GET /chat/task/<task_id>/status/` گرفته میشود.
|
||||
- conversation و messageها داخل دیتابیس داخلی ذخیره میشوند.
|
||||
- همه endpointها نیازمند authentication هستند.
|
||||
- ساختار خروجی نهایی باید با `sections` برای UI سازگار باشد.
|
||||
@@ -1,357 +0,0 @@
|
||||
# مستند ارتباط فرانت با API تنظیمات داشبورد فارم
|
||||
|
||||
این فایل مشخص میکند فرانتاند برای endpoint زیر چه request و responseی انتظار دارد:
|
||||
|
||||
```text
|
||||
http://localhost:8000/api/farm-dashboard-config
|
||||
```
|
||||
|
||||
این endpoint در فرانت از طریق فایل `src/libs/api/services/farmDashboardService.ts` مصرف میشود.
|
||||
|
||||
---
|
||||
|
||||
## خلاصه رفتار فرانت
|
||||
|
||||
- فرانت برای دریافت تنظیمات از `GET /api/farm-dashboard-config` استفاده میکند.
|
||||
- فرانت برای ذخیره تغییرات از `PATCH /api/farm-dashboard-config` استفاده میکند.
|
||||
- در `PATCH` فقط فیلدهای تغییرکرده ارسال میشوند.
|
||||
- اما در response بهتر است همیشه **کل تنظیمات نهایی** برگردانده شود.
|
||||
- فرانت response را هم در حالت wrapper شده و هم بدون wrapper میپذیرد.
|
||||
|
||||
---
|
||||
|
||||
## فرمت response قابل قبول
|
||||
|
||||
فرانت هر دو فرمت زیر را میپذیرد.
|
||||
|
||||
### فرمت پیشنهادی
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "OK",
|
||||
"data": {
|
||||
"disabled_card_ids": ["farmWeatherCard", "sensorRadarChart"],
|
||||
"row_order": [
|
||||
"overviewKpis",
|
||||
"weatherAlerts",
|
||||
"sensorMonitoring",
|
||||
"sensorCharts",
|
||||
"alertsWater",
|
||||
"predictions",
|
||||
"soilHeatmap",
|
||||
"ndviRecommendations",
|
||||
"economic"
|
||||
],
|
||||
"enable_drag_reorder": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### فرمت قابل قبول بدون wrapper
|
||||
|
||||
```json
|
||||
{
|
||||
"disabled_card_ids": ["farmWeatherCard", "sensorRadarChart"],
|
||||
"row_order": [
|
||||
"overviewKpis",
|
||||
"weatherAlerts",
|
||||
"sensorMonitoring",
|
||||
"sensorCharts",
|
||||
"alertsWater",
|
||||
"predictions",
|
||||
"soilHeatmap",
|
||||
"ndviRecommendations",
|
||||
"economic"
|
||||
],
|
||||
"enable_drag_reorder": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GET
|
||||
|
||||
### Request
|
||||
|
||||
```http
|
||||
GET /api/farm-dashboard-config
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
> هدر `Authorization` فقط وقتی ارسال میشود که توکن در `localStorage` موجود باشد.
|
||||
|
||||
### Response مورد انتظار
|
||||
|
||||
فرانت در خروجی GET این ساختار را انتظار دارد:
|
||||
|
||||
| فیلد | نوع | توضیح |
|
||||
|---|---|---|
|
||||
| `disabled_card_ids` | `string[]` | لیست کارتهای مخفیشده |
|
||||
| `row_order` | `string[]` | ترتیب ردیفهای داشبورد |
|
||||
| `enable_drag_reorder` | `boolean` | فعال/غیرفعال بودن drag reorder |
|
||||
|
||||
### مثال response
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "OK",
|
||||
"data": {
|
||||
"disabled_card_ids": ["farmWeatherCard", "sensorRadarChart"],
|
||||
"row_order": [
|
||||
"overviewKpis",
|
||||
"weatherAlerts",
|
||||
"sensorMonitoring",
|
||||
"sensorCharts",
|
||||
"alertsWater",
|
||||
"predictions",
|
||||
"soilHeatmap",
|
||||
"ndviRecommendations",
|
||||
"economic"
|
||||
],
|
||||
"enable_drag_reorder": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### نکات مهم GET
|
||||
|
||||
- اگر `enable_drag_reorder` برنگردد، فرانت مقدار پیشفرض `true` در نظر میگیرد.
|
||||
- اگر `row_order` نامعتبر یا خالی باشد، فرانت از ترتیب پیشفرض خودش استفاده میکند.
|
||||
- اگر request خطا بخورد، فرانت اول `localStorage` را چک میکند؛ اگر چیزی نبود، config پیشفرض را میسازد.
|
||||
|
||||
---
|
||||
|
||||
## PATCH
|
||||
|
||||
### رفتار request
|
||||
|
||||
فرانت فقط فیلدهای تغییرکرده را میفرستد. یعنی body میتواند یکی از حالتهای زیر باشد یا ترکیبی از آنها:
|
||||
|
||||
#### 1) تغییر کارتهای غیرفعال
|
||||
|
||||
```json
|
||||
{
|
||||
"disabled_card_ids": ["farmWeatherCard", "sensorRadarChart"]
|
||||
}
|
||||
```
|
||||
|
||||
#### 2) تغییر ترتیب ردیفها
|
||||
|
||||
```json
|
||||
{
|
||||
"row_order": [
|
||||
"overviewKpis",
|
||||
"weatherAlerts",
|
||||
"predictions",
|
||||
"sensorMonitoring",
|
||||
"sensorCharts",
|
||||
"alertsWater",
|
||||
"soilHeatmap",
|
||||
"ndviRecommendations",
|
||||
"economic"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 3) فعال/غیرفعال کردن drag reorder
|
||||
|
||||
```json
|
||||
{
|
||||
"enable_drag_reorder": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 4) ترکیبی
|
||||
|
||||
```json
|
||||
{
|
||||
"disabled_card_ids": ["farmWeatherCard"],
|
||||
"row_order": [
|
||||
"overviewKpis",
|
||||
"weatherAlerts",
|
||||
"sensorMonitoring",
|
||||
"sensorCharts",
|
||||
"alertsWater",
|
||||
"predictions",
|
||||
"soilHeatmap",
|
||||
"ndviRecommendations",
|
||||
"economic"
|
||||
],
|
||||
"enable_drag_reorder": true
|
||||
}
|
||||
```
|
||||
|
||||
### Response مورد انتظار برای PATCH
|
||||
|
||||
هرچند request میتواند partial باشد، ولی response بهتر است همیشه **کل state نهایی config** را برگرداند:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "OK",
|
||||
"data": {
|
||||
"disabled_card_ids": ["farmWeatherCard"],
|
||||
"row_order": [
|
||||
"overviewKpis",
|
||||
"weatherAlerts",
|
||||
"sensorMonitoring",
|
||||
"sensorCharts",
|
||||
"alertsWater",
|
||||
"predictions",
|
||||
"soilHeatmap",
|
||||
"ndviRecommendations",
|
||||
"economic"
|
||||
],
|
||||
"enable_drag_reorder": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### نکته خیلی مهم برای PATCH
|
||||
|
||||
در پیادهسازی فعلی فرانت، response باید حداقل یکی از این دو فیلد را داشته باشد:
|
||||
|
||||
- `disabled_card_ids`
|
||||
- `row_order`
|
||||
|
||||
اگر backend فقط این را برگرداند:
|
||||
|
||||
```json
|
||||
{
|
||||
"enable_drag_reorder": false
|
||||
}
|
||||
```
|
||||
|
||||
ممکن است فرانت این response را نامعتبر تشخیص دهد.
|
||||
بنابراین برای جلوگیری از مشکل، **همیشه کل object نهایی config را برگردانید**.
|
||||
|
||||
---
|
||||
|
||||
## mapping بین فرانت و API
|
||||
|
||||
فرانت state داخلی را با نامهای camelCase نگه میدارد، اما در request به snake_case تبدیل میکند:
|
||||
|
||||
| Frontend field | API field |
|
||||
|---|---|
|
||||
| `disabledCardIds` | `disabled_card_ids` |
|
||||
| `rowOrder` | `row_order` |
|
||||
| `enableDragReorder` | `enable_drag_reorder` |
|
||||
|
||||
---
|
||||
|
||||
## مقادیر معتبر پیشنهادی
|
||||
|
||||
### Row ID های معتبر
|
||||
|
||||
```json
|
||||
[
|
||||
"overviewKpis",
|
||||
"weatherAlerts",
|
||||
"sensorMonitoring",
|
||||
"sensorCharts",
|
||||
"alertsWater",
|
||||
"predictions",
|
||||
"soilHeatmap",
|
||||
"ndviRecommendations",
|
||||
"economic"
|
||||
]
|
||||
```
|
||||
|
||||
### Card ID های معتبر
|
||||
|
||||
```json
|
||||
[
|
||||
"farmOverviewKpis",
|
||||
"farmWeatherCard",
|
||||
"farmAlertsTracker",
|
||||
"sensorValuesList",
|
||||
"sensorRadarChart",
|
||||
"sensorComparisonChart",
|
||||
"anomalyDetectionCard",
|
||||
"farmAlertsTimeline",
|
||||
"waterNeedPrediction",
|
||||
"harvestPredictionCard",
|
||||
"yieldPredictionChart",
|
||||
"soilMoistureHeatmap",
|
||||
"ndviHealthCard",
|
||||
"recommendationsList",
|
||||
"economicOverview"
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## فرمت canonical پیشنهادی برای backend
|
||||
|
||||
اگر بخواهی backend کاملاً سازگار و بدون ambiguity باشد، بهترین قرارداد این است:
|
||||
|
||||
- `disabled_card_ids` فقط شامل `Card ID` باشد
|
||||
- `row_order` فقط شامل `Row ID` باشد
|
||||
- response همیشه full object برگرداند
|
||||
- status موفق `200` باشد
|
||||
- `Content-Type` برابر `application/json` باشد
|
||||
|
||||
---
|
||||
|
||||
## نمونه نهایی پیشنهادی
|
||||
|
||||
### GET success
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "OK",
|
||||
"data": {
|
||||
"disabled_card_ids": [],
|
||||
"row_order": [
|
||||
"overviewKpis",
|
||||
"weatherAlerts",
|
||||
"sensorMonitoring",
|
||||
"sensorCharts",
|
||||
"alertsWater",
|
||||
"predictions",
|
||||
"soilHeatmap",
|
||||
"ndviRecommendations",
|
||||
"economic"
|
||||
],
|
||||
"enable_drag_reorder": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PATCH success
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "OK",
|
||||
"data": {
|
||||
"disabled_card_ids": ["farmWeatherCard"],
|
||||
"row_order": [
|
||||
"overviewKpis",
|
||||
"weatherAlerts",
|
||||
"sensorMonitoring",
|
||||
"sensorCharts",
|
||||
"alertsWater",
|
||||
"predictions",
|
||||
"soilHeatmap",
|
||||
"ndviRecommendations",
|
||||
"economic"
|
||||
],
|
||||
"enable_drag_reorder": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## منبع این مستند
|
||||
|
||||
این مستند بر اساس رفتار واقعی فرانت در فایلهای زیر نوشته شده است:
|
||||
|
||||
- `src/libs/api/services/farmDashboardService.ts`
|
||||
- `src/views/dashboards/farm/farmDashboardConfig.ts`
|
||||
- `src/views/dashboards/farm/FarmDashboardWrapper.tsx`
|
||||
@@ -1,439 +0,0 @@
|
||||
# مستند پاسخ API برای فیچرهای Farm AI
|
||||
|
||||
این فایل، پاسخهای API موردنیاز برای سه فیچر زیر را یکجا جمع میکند:
|
||||
|
||||
- `src/app/(dashboard)/(private)/farm-ai-assistant/page.tsx`
|
||||
- `src/app/(dashboard)/(private)/fertilization-recommendation/page.tsx`
|
||||
- `src/app/(dashboard)/(private)/irrigation-recommendation/page.tsx`
|
||||
|
||||
> مبنا، پیادهسازی فعلی فرانت در `src/views/dashboards/farm/...` و سرویسهای `src/libs/api/services/...` است.
|
||||
|
||||
---
|
||||
|
||||
## قرارداد کلی پاسخها
|
||||
|
||||
در هر سه سرویس، فرانت انتظار این wrapper را دارد:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
- `status`: معمولاً `success` یا `error`
|
||||
- `data`: payload اصلی که فرانت unwrap میکند
|
||||
|
||||
---
|
||||
|
||||
## 1) Farm AI Assistant
|
||||
|
||||
### صفحه
|
||||
|
||||
- `src/app/(dashboard)/(private)/farm-ai-assistant/page.tsx`
|
||||
- کامپوننت اصلی: `src/views/dashboards/farm/farmAiAssistant/FarmAiAssistantChat.tsx`
|
||||
|
||||
### APIهای موردنیاز
|
||||
|
||||
1. `GET /api/farm-ai-assistant/context/`
|
||||
2. `POST /api/farm-ai-assistant/chat/`
|
||||
|
||||
---
|
||||
|
||||
### 1.1) دریافت context مزرعه
|
||||
|
||||
#### Endpoint
|
||||
|
||||
`GET /api/farm-ai-assistant/context/`
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"soilType": "Loamy",
|
||||
"waterEC": "1.2 dS/m",
|
||||
"selectedCrop": "Tomato",
|
||||
"growthStage": "Flowering",
|
||||
"lastIrrigationStatus": "2 days ago"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### فیلدهای لازم
|
||||
|
||||
| فیلد | نوع | مصرف در UI |
|
||||
|------|-----|------------|
|
||||
| `soilType` | `string` | badge نوع خاک |
|
||||
| `waterEC` | `string` | badge EC آب |
|
||||
| `selectedCrop` | `string` | badge محصول انتخابشده |
|
||||
| `growthStage` | `string` | badge مرحله رشد |
|
||||
| `lastIrrigationStatus` | `string` | badge آخرین وضعیت آبیاری |
|
||||
|
||||
#### نکته
|
||||
|
||||
اگر این API خطا بدهد، فرانت fallback داخلی دارد و toast خطا نمایش میدهد.
|
||||
|
||||
---
|
||||
|
||||
### 1.2) ارسال پیام به دستیار
|
||||
|
||||
#### Endpoint
|
||||
|
||||
`POST /api/farm-ai-assistant/chat/`
|
||||
|
||||
#### Request body
|
||||
|
||||
```json
|
||||
{
|
||||
"content": "What is the best irrigation plan for tomato?",
|
||||
"farm_context": {
|
||||
"soilType": "Loamy",
|
||||
"waterEC": "1.2 dS/m",
|
||||
"selectedCrop": "Tomato",
|
||||
"growthStage": "Flowering",
|
||||
"lastIrrigationStatus": "2 days ago"
|
||||
},
|
||||
"conversation_id": "conv-123"
|
||||
}
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"message_id": "msg-001",
|
||||
"conversation_id": "conv-123",
|
||||
"content": "Here is the recommended plan.",
|
||||
"sections": [
|
||||
{
|
||||
"type": "recommendation",
|
||||
"title": "Irrigation Plan",
|
||||
"icon": "droplet",
|
||||
"frequency": "3 times per week",
|
||||
"amount": "15 liters per plant",
|
||||
"timing": "Early morning",
|
||||
"expandableExplanation": "Loamy soil holds moisture well, so moderate frequency is enough."
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"title": "Important Notes",
|
||||
"icon": "leaf",
|
||||
"items": [
|
||||
"Avoid watering at noon",
|
||||
"Check leaf stress every two days"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "warning",
|
||||
"title": "Heat Alert",
|
||||
"icon": "warning",
|
||||
"content": "Increase irrigation if temperature rises above 35°C."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### فیلدهای لازم در response
|
||||
|
||||
| فیلد | نوع | توضیح |
|
||||
|------|-----|-------|
|
||||
| `message_id` | `string` | id پیام assistant |
|
||||
| `conversation_id` | `string` | برای ادامه چت در پیامهای بعدی |
|
||||
| `content` | `string` | متن ساده پاسخ |
|
||||
| `sections` | `ChatSection[]` | خروجی ساختیافته برای رندر کارتها |
|
||||
|
||||
#### ساختار `ChatSection`
|
||||
|
||||
| فیلد | نوع | اجباری | توضیح |
|
||||
|------|-----|--------|-------|
|
||||
| `type` | `'text' \| 'list' \| 'recommendation' \| 'warning'` | بله | نوع سکشن |
|
||||
| `title` | `string` | خیر | عنوان سکشن |
|
||||
| `content` | `string` | خیر | متن سکشن |
|
||||
| `items` | `string[]` | خیر | برای لیست |
|
||||
| `icon` | `'droplet' \| 'leaf' \| 'warning' \| 'fertilizer' \| 'calendar'` | خیر | آیکون نمایشی |
|
||||
| `frequency` | `string` | خیر | فقط برای `recommendation` |
|
||||
| `amount` | `string` | خیر | فقط برای `recommendation` |
|
||||
| `timing` | `string` | خیر | فقط برای `recommendation` |
|
||||
| `expandableExplanation` | `string` | خیر | توضیح بازشونده |
|
||||
|
||||
#### حداقل توصیه
|
||||
|
||||
- `sections` همیشه به صورت آرایه برگردد، حتی اگر خالی باشد.
|
||||
- `conversation_id` بعد از اولین پاسخ حتماً برگردد.
|
||||
- اگر پاسخ فقط structured است، `content` میتواند رشته خالی باشد.
|
||||
|
||||
---
|
||||
|
||||
## 2) Fertilization Recommendation
|
||||
|
||||
### صفحه
|
||||
|
||||
- `src/app/(dashboard)/(private)/fertilization-recommendation/page.tsx`
|
||||
- کامپوننت اصلی: `src/views/dashboards/farm/smartFertilization/SmartFertilizationRecommendation.tsx`
|
||||
|
||||
### APIهای موردنیاز
|
||||
|
||||
1. `GET /api/fertilization-recommendation/config/`
|
||||
2. `POST /api/fertilization-recommendation/recommend/`
|
||||
|
||||
---
|
||||
|
||||
### 2.1) دریافت config اولیه
|
||||
|
||||
#### Endpoint
|
||||
|
||||
`GET /api/fertilization-recommendation/config/`
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"farmData": {
|
||||
"soilType": "Loamy",
|
||||
"organicMatter": "Medium (2.5%)",
|
||||
"waterEC": "1.2 dS/m"
|
||||
},
|
||||
"growthStages": [
|
||||
{ "id": "prePlanting", "icon": "tabler-seedling" },
|
||||
{ "id": "earlyGrowth", "icon": "tabler-leaf" },
|
||||
{ "id": "flowering", "icon": "tabler-flower" },
|
||||
{ "id": "fruiting", "icon": "tabler-apple" },
|
||||
{ "id": "postHarvest", "icon": "tabler-basket" }
|
||||
],
|
||||
"cropOptions": [
|
||||
{ "id": "wheat", "labelKey": "wheat", "icon": "tabler-wheat" },
|
||||
{ "id": "corn", "labelKey": "corn", "icon": "tabler-plant-2" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### فیلدهای لازم
|
||||
|
||||
##### `farmData`
|
||||
|
||||
| فیلد | نوع |
|
||||
|------|-----|
|
||||
| `soilType` | `string` |
|
||||
| `organicMatter` | `string` |
|
||||
| `waterEC` | `string` |
|
||||
|
||||
##### `growthStages[]`
|
||||
|
||||
| فیلد | نوع | توضیح |
|
||||
|------|-----|-------|
|
||||
| `id` | `string` | id مرحله رشد |
|
||||
| `icon` | `string` | نام آیکون |
|
||||
|
||||
##### `cropOptions[]`
|
||||
|
||||
| فیلد | نوع | توضیح |
|
||||
|------|-----|-------|
|
||||
| `id` | `string` | id محصول برای submit |
|
||||
| `labelKey` | `string` | کلید ترجمه |
|
||||
| `icon` | `string` | نام آیکون |
|
||||
|
||||
#### نکته
|
||||
|
||||
- اگر `growthStages` مقدار داشته باشد، اولین آیتم به عنوان پیشفرض انتخاب میشود.
|
||||
- اگر `cropOptions` خالی باشد، لیست انتخاب محصول خالی نمایش داده میشود.
|
||||
|
||||
---
|
||||
|
||||
### 2.2) دریافت برنامه کوددهی
|
||||
|
||||
#### Endpoint
|
||||
|
||||
`POST /api/fertilization-recommendation/recommend/`
|
||||
|
||||
#### Request body
|
||||
|
||||
```json
|
||||
{
|
||||
"crop_id": "wheat",
|
||||
"growth_stage": "flowering",
|
||||
"soilType": "Loamy",
|
||||
"organicMatter": "Medium (2.5%)",
|
||||
"waterEC": "1.2 dS/m"
|
||||
}
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"plan": {
|
||||
"npkRatio": "20-20-20",
|
||||
"amountPerHectare": "150 kg/ha",
|
||||
"applicationMethod": "Fertigation",
|
||||
"applicationInterval": "Every 10 days",
|
||||
"reasoning": "Balanced NPK is recommended during flowering to support bloom and fruit set."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### فیلدهای لازم در `plan`
|
||||
|
||||
| فیلد | نوع | مصرف در UI |
|
||||
|------|-----|------------|
|
||||
| `npkRatio` | `string` | نوع/نسبت کود |
|
||||
| `amountPerHectare` | `string` | مقدار مصرف |
|
||||
| `applicationMethod` | `string` | روش مصرف |
|
||||
| `applicationInterval` | `string` | بازه تکرار |
|
||||
| `reasoning` | `string` | توضیح بازشونده |
|
||||
|
||||
#### نکته
|
||||
|
||||
فرانت مستقیماً `data.plan` را انتظار دارد. اگر `plan` نباشد، نتیجهای نمایش داده نمیشود.
|
||||
|
||||
---
|
||||
|
||||
## 3) Irrigation Recommendation
|
||||
|
||||
### صفحه
|
||||
|
||||
- `src/app/(dashboard)/(private)/irrigation-recommendation/page.tsx`
|
||||
- کامپوننت اصلی: `src/views/dashboards/farm/smartIrrigation/SmartIrrigationRecommendation.tsx`
|
||||
|
||||
### APIهای موردنیاز
|
||||
|
||||
1. `GET /api/irrigation-recommendation/config/`
|
||||
2. `POST /api/irrigation-recommendation/recommend/`
|
||||
|
||||
---
|
||||
|
||||
### 3.1) دریافت config اولیه
|
||||
|
||||
#### Endpoint
|
||||
|
||||
`GET /api/irrigation-recommendation/config/`
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"farmInfo": {
|
||||
"soilType": "Loamy",
|
||||
"waterQuality": "Medium EC",
|
||||
"climateZone": "Temperate"
|
||||
},
|
||||
"cropOptions": [
|
||||
{ "id": "tomato", "labelKey": "tomato", "icon": "tabler-plant-2" },
|
||||
{ "id": "cucumber", "labelKey": "cucumber", "icon": "tabler-leaf" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### فیلدهای لازم
|
||||
|
||||
##### `farmInfo`
|
||||
|
||||
| فیلد | نوع |
|
||||
|------|-----|
|
||||
| `soilType` | `string` |
|
||||
| `waterQuality` | `string` |
|
||||
| `climateZone` | `string` |
|
||||
|
||||
##### `cropOptions[]`
|
||||
|
||||
| فیلد | نوع | توضیح |
|
||||
|------|-----|-------|
|
||||
| `id` | `string` | id محصول برای submit |
|
||||
| `labelKey` | `string` | کلید ترجمه |
|
||||
| `icon` | `string` | نام آیکون |
|
||||
|
||||
#### نکته
|
||||
|
||||
در این صفحه `farmInfo` بدون چک null مستقیماً set میشود؛ بهتر است API همیشه این آبجکت را برگرداند.
|
||||
|
||||
---
|
||||
|
||||
### 3.2) دریافت برنامه آبیاری
|
||||
|
||||
#### Endpoint
|
||||
|
||||
`POST /api/irrigation-recommendation/recommend/`
|
||||
|
||||
#### Request body
|
||||
|
||||
```json
|
||||
{
|
||||
"crop_id": "tomato"
|
||||
}
|
||||
```
|
||||
|
||||
> در سرویس، فیلدهای `soilType`, `waterQuality`, `climateZone` هم پشتیبانی شدهاند، ولی در UI فعلی فقط `crop_id` ارسال میشود.
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"plan": {
|
||||
"frequencyPerWeek": 3,
|
||||
"durationMinutes": 25,
|
||||
"bestTimeOfDay": "Early morning",
|
||||
"moistureLevel": 68,
|
||||
"warning": "Reduce irrigation if rainfall occurs this week."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### فیلدهای لازم در `plan`
|
||||
|
||||
| فیلد | نوع | مصرف در UI |
|
||||
|------|-----|------------|
|
||||
| `frequencyPerWeek` | `number` | تعداد دفعات در هفته |
|
||||
| `durationMinutes` | `number` | مدت هر نوبت |
|
||||
| `bestTimeOfDay` | `string` | زمان مناسب آبیاری |
|
||||
| `moistureLevel` | `number` | درصد moisture برای دایره progress |
|
||||
| `warning` | `string` | هشدار اختیاری |
|
||||
|
||||
#### محدودیت مهم
|
||||
|
||||
- `moistureLevel` بهتر است بین `0` تا `100` باشد، چون مستقیم برای محاسبه progress circle استفاده میشود.
|
||||
|
||||
---
|
||||
|
||||
## جمعبندی سریع برای بکاند
|
||||
|
||||
### Farm AI Assistant
|
||||
|
||||
- `GET /api/farm-ai-assistant/context/` → `data: FarmContext`
|
||||
- `POST /api/farm-ai-assistant/chat/` → `data: { message_id, conversation_id, content, sections }`
|
||||
|
||||
### Fertilization Recommendation
|
||||
|
||||
- `GET /api/fertilization-recommendation/config/` → `data: { farmData, growthStages, cropOptions }`
|
||||
- `POST /api/fertilization-recommendation/recommend/` → `data: { plan }`
|
||||
|
||||
### Irrigation Recommendation
|
||||
|
||||
- `GET /api/irrigation-recommendation/config/` → `data: { farmInfo, cropOptions }`
|
||||
- `POST /api/irrigation-recommendation/recommend/` → `data: { plan }`
|
||||
|
||||
---
|
||||
|
||||
## مسیرهای مرجع کد
|
||||
|
||||
- `src/libs/api/services/farmAiAssistantService.ts`
|
||||
- `src/libs/api/services/fertilizationRecommendationService.ts`
|
||||
- `src/libs/api/services/irrigationRecommendationService.ts`
|
||||
- `src/views/dashboards/farm/farmAiAssistant/FarmAiAssistantChat.tsx`
|
||||
- `src/views/dashboards/farm/smartFertilization/SmartFertilizationRecommendation.tsx`
|
||||
- `src/views/dashboards/farm/smartIrrigation/SmartIrrigationRecommendation.tsx`
|
||||
@@ -1,465 +0,0 @@
|
||||
# قرارداد API شبیهساز گیاه (Plant Simulator)
|
||||
|
||||
این سند تمام دادههایی را که باید **از بکند بیاید**، **به بکند فرستاده شود** و دوباره **از بکند برگردد** بههمراه ساختار و دادههای ماک توصیف میکند.
|
||||
|
||||
---
|
||||
|
||||
## ۱. دادههای اولیه (از بکند → فرانت)
|
||||
|
||||
این دادهها یکبار هنگام لود صفحه (یا هنگام باز کردن شبیهساز) از بکند گرفته میشوند.
|
||||
|
||||
### ۱.۱ تنظیمات اسلایدرها (کنترلهای محیطی)
|
||||
|
||||
هر اسلایدر (نور، آب، pH خاک، سرعت رشد و غیره) باید از بکند **حداقل، حداکثر، گام، واحد و برچسب** بگیرد. واحد میتواند درصد (`percent`) یا عدد با واحد متنی (`number` + `unit`) باشد.
|
||||
|
||||
```ts
|
||||
// GET /api/plant-simulator/config یا بخشی از همین endpoint
|
||||
|
||||
interface SliderConfig {
|
||||
key: string // مثلاً: "light" | "water" | "soil_ph" | "growth_speed"
|
||||
label: string // برچسب نمایشی (مثلاً "نور"، "آب")
|
||||
min: number
|
||||
max: number
|
||||
step: number
|
||||
unit_type: 'percent' | 'number'
|
||||
unit?: string // وقتی unit_type === 'number' مثلاً "g/s", "ppm", "°C"
|
||||
default_value: number
|
||||
icon?: string // اختیاری: مثلاً "☀️", "💧"
|
||||
}
|
||||
|
||||
// پاسخ نمونه
|
||||
{
|
||||
"sliders": [
|
||||
{
|
||||
"key": "light",
|
||||
"label": "نور",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 5,
|
||||
"unit_type": "percent",
|
||||
"default_value": 75,
|
||||
"icon": "☀️"
|
||||
},
|
||||
{
|
||||
"key": "water",
|
||||
"label": "آب",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 5,
|
||||
"unit_type": "percent",
|
||||
"default_value": 65,
|
||||
"icon": "💧"
|
||||
},
|
||||
{
|
||||
"key": "soil_ph",
|
||||
"label": "pH خاک",
|
||||
"min": 4,
|
||||
"max": 9,
|
||||
"step": 0.5,
|
||||
"unit_type": "number",
|
||||
"unit": "",
|
||||
"default_value": 6.5
|
||||
},
|
||||
{
|
||||
"key": "growth_speed",
|
||||
"label": "سرعت رشد",
|
||||
"min": 0.5,
|
||||
"max": 5,
|
||||
"step": 0.5,
|
||||
"unit_type": "number",
|
||||
"unit": "×",
|
||||
"default_value": 1.5
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### ۱.۲ ثابتهای شبیهساز (حداکثرها و محدوده چارت)
|
||||
|
||||
برای نمایش صحیح ارتفاع، برگ، شاخه، میوه، محصول و محورهای چارت باید از بکند بیاید.
|
||||
|
||||
```ts
|
||||
// همان config یا GET /api/plant-simulator/constants
|
||||
|
||||
interface SimulatorConstants {
|
||||
max_height: number // مثلاً 280 (px یا واحد ارتفاع)
|
||||
max_leaves: number // مثلاً 14
|
||||
max_branches: number // مثلاً 6
|
||||
max_yield: number // مثلاً 500 (گرم)
|
||||
yield_unit: string // مثلاً "g"
|
||||
yield_rate_unit: string // مثلاً "g/s"
|
||||
height_unit?: string // مثلاً "px" یا "cm"
|
||||
}
|
||||
|
||||
// پاسخ نمونه
|
||||
{
|
||||
"max_height": 280,
|
||||
"max_leaves": 14,
|
||||
"max_branches": 6,
|
||||
"max_yield": 500,
|
||||
"yield_unit": "g",
|
||||
"yield_rate_unit": "g/s",
|
||||
"height_unit": "px"
|
||||
}
|
||||
```
|
||||
|
||||
### ۱.۳ تنظیمات چارت (محورها و سریها)
|
||||
|
||||
عنوان چارت، برچسب محورها و محدوده min/max هر محور تا در فرانت بهدرستی رسم شود.
|
||||
|
||||
```ts
|
||||
interface ChartConfig {
|
||||
title: string
|
||||
x_axis_label?: string // مثلاً "زمان (ثانیه)"
|
||||
series: {
|
||||
key: string // "height" | "leaves" | "yield" | "yield_rate"
|
||||
label: string
|
||||
y_axis_id: string // "yHeight" | "yLeaf" | "yYield" | "yYieldRate"
|
||||
min: number
|
||||
max: number
|
||||
unit?: string
|
||||
}[]
|
||||
}
|
||||
|
||||
// پاسخ نمونه
|
||||
{
|
||||
"chart": {
|
||||
"title": "پیشرفت رشد",
|
||||
"x_axis_label": "زمان (ثانیه)",
|
||||
"series": [
|
||||
{ "key": "height", "label": "ارتفاع (px)", "y_axis_id": "yHeight", "min": 0, "max": 280, "unit": "px" },
|
||||
{ "key": "leaves", "label": "تعداد برگ", "y_axis_id": "yLeaf", "min": 0, "max": 14 },
|
||||
{ "key": "yield", "label": "محصول (g)", "y_axis_id": "yYield", "min": 0, "max": 500, "unit": "g" },
|
||||
{ "key": "yield_rate", "label": "نرخ محصول (g/s)", "y_axis_id": "yYieldRate", "min": 0, "unit": "g/s" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ۲. دادههایی که به بکند فرستاده میشوند
|
||||
|
||||
### ۲.۱ شروع شبیهسازی
|
||||
|
||||
وقتی کاربر دکمه «شروع» را میزند، مقادیر فعلی محیط و سرعت به بکند ارسال میشود.
|
||||
|
||||
```ts
|
||||
// POST /api/plant-simulator/start
|
||||
|
||||
interface StartSimulationRequest {
|
||||
environment: Record<string, number> // key مطابق slider.key ها
|
||||
growth_speed: number
|
||||
}
|
||||
|
||||
// مثال
|
||||
{
|
||||
"environment": {
|
||||
"light": 75,
|
||||
"water": 65,
|
||||
"soil_ph": 6.5
|
||||
},
|
||||
"growth_speed": 1.5
|
||||
}
|
||||
```
|
||||
|
||||
### ۲.۲ توقف شبیهسازی
|
||||
|
||||
```ts
|
||||
// POST /api/plant-simulator/stop
|
||||
|
||||
// بدنه خالی یا فقط session_id در صورت نیاز
|
||||
{}
|
||||
```
|
||||
|
||||
### ۲.۳ ریست
|
||||
|
||||
```ts
|
||||
// POST /api/plant-simulator/reset
|
||||
|
||||
// بدنه خالی یا session_id
|
||||
{}
|
||||
```
|
||||
|
||||
### ۲.۴ بهروزرسانی محیط (تغییر اسلایدرها)
|
||||
|
||||
هر بار کاربر نور، آب، pH یا سرعت را عوض کند، در حالت real-time میتوان این را به بکند فرستاد (اختیاری؛ بکند میتواند فقط با start این مقادیر را بگیرد).
|
||||
|
||||
```ts
|
||||
// PATCH /api/plant-simulator/environment
|
||||
|
||||
interface UpdateEnvironmentRequest {
|
||||
environment: Record<string, number>
|
||||
growth_speed?: number
|
||||
}
|
||||
|
||||
// مثال
|
||||
{
|
||||
"environment": { "light": 80, "water": 70, "soil_ph": 6.5 },
|
||||
"growth_speed": 2
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ۳. دادههایی که از بکند برمیگردند (حین / بعد شبیهسازی)
|
||||
|
||||
این دادهها یا با **polling** (مثلاً هر ۱ ثانیه) یا با **WebSocket/SSE** از بکند گرفته میشوند و در UI و چارت بهصورت تدریجی بهروز میشوند (مثلاً آرایههای چارت کمکم طولانی میشوند).
|
||||
|
||||
### ۳.۱ وضعیت گیاه (آمار کارت بالا)
|
||||
|
||||
تعداد برگ، شاخه، میوه، ارتفاع، محصول و نرخ محصول باید از بکند بیاید تا با سرعت رشد واقعی همگام باشد.
|
||||
|
||||
```ts
|
||||
// GET /api/plant-simulator/state یا از طریق WebSocket
|
||||
|
||||
interface PlantStateResponse {
|
||||
height: number
|
||||
leaves_count: number
|
||||
branches_count: number
|
||||
fruits_count: number
|
||||
yield: number
|
||||
yield_rate: number
|
||||
tick: number
|
||||
is_healthy: boolean // آیا گیاه میتواند به سلامت به رشد ادامه دهد؟
|
||||
can_continue: boolean // معادل is_healthy یا منطق جدا (مثلاً رسیدن به حداکثر ارتفاع)
|
||||
}
|
||||
|
||||
// پاسخ نمونه (یک لحظه از زمان)
|
||||
{
|
||||
"height": 120,
|
||||
"leaves_count": 4,
|
||||
"branches_count": 2,
|
||||
"fruits_count": 0,
|
||||
"yield": 0,
|
||||
"yield_rate": 0.012,
|
||||
"tick": 340,
|
||||
"is_healthy": true,
|
||||
"can_continue": true
|
||||
}
|
||||
```
|
||||
|
||||
### ۳.۲ پیشرفت و وضعیت نوارهای پیشرفت
|
||||
|
||||
پیشرفت رشد، وضعیت نور، وضعیت آب و محصولدهی برای نوارهای پیشرفت (Progress) و نمایش درصد/عدد.
|
||||
|
||||
```ts
|
||||
interface ProgressResponse {
|
||||
growth_progress: number // 0..100 (بر اساس height / max_height)
|
||||
light_status: number // مقدار فعلی نور (مثلاً 0..100 درصد)
|
||||
water_status: number // مقدار فعلی آب (مثلاً 0..100 درصد)
|
||||
yield_progress: number // 0..100 (بر اساس yield / max_yield)
|
||||
yield_current: number
|
||||
yield_rate_current: number
|
||||
}
|
||||
|
||||
// پاسخ نمونه
|
||||
{
|
||||
"growth_progress": 42,
|
||||
"light_status": 75,
|
||||
"water_status": 65,
|
||||
"yield_progress": 8,
|
||||
"yield_current": 42.5,
|
||||
"yield_rate_current": 0.093
|
||||
}
|
||||
```
|
||||
|
||||
این فیلدها میتوانند داخل همان `PlantStateResponse` یا در یک endpoint جدا برگردانده شوند.
|
||||
|
||||
### ۳.۳ دادههای چارت (تاریخچهٔ زمانی)
|
||||
|
||||
دادههای چارت بهصورت آرایههایی هستند که **بر اساس زمان (مثلاً هر ثانیه) از بکند پر میشوند**. فرانت این آرایهها را مستقیم به نمودار میدهد؛ هر نقطهٔ جدید با سرعت رشد گیاه اضافه میشود.
|
||||
|
||||
مثال: اگر بکند هر ثانیه یک بار state بفرستد، آرایهها به این شکل طول میکشند:
|
||||
|
||||
- ثانیه ۰: `[0]`
|
||||
- ثانیه ۱: `[0, 5]`
|
||||
- ثانیه ۲: `[0, 5, 10]`
|
||||
- ثانیه ۳: `[0, 5, 10, 30]`
|
||||
- ثانیه ۴: `[0, 5, 10, 30, 40]`
|
||||
|
||||
یعنی **مقادیر با سرعت رشد گیاه کمکم به آرایه اضافه میشوند**.
|
||||
|
||||
```ts
|
||||
// GET /api/plant-simulator/state یا بخشی از همان پاسخ
|
||||
|
||||
interface ChartHistoryResponse {
|
||||
labels: string[] // مثلاً ["0s", "1s", "2s", ...]
|
||||
height_history: number[]
|
||||
leaf_history: number[]
|
||||
yield_history: number[]
|
||||
yield_rate_history: number[]
|
||||
}
|
||||
|
||||
// پاسخ نمونه (بعد از چند ثانیه شبیهسازی)
|
||||
{
|
||||
"labels": ["0s", "1s", "2s", "3s", "4s", "5s"],
|
||||
"height_history": [0, 5, 12, 28, 45, 68],
|
||||
"leaf_history": [0, 0, 1, 2, 3, 4],
|
||||
"yield_history": [0, 0, 0, 0.1, 0.5, 1.2],
|
||||
"yield_rate_history": [0, 0, 0, 0.01, 0.03, 0.06]
|
||||
}
|
||||
```
|
||||
|
||||
- **ارتفاع (height_history)**: مقدار ارتفاع در هر نمونه (مثلاً هر ثانیه).
|
||||
- **برگ (leaf_history)**: تعداد برگ در هر نمونه.
|
||||
- **محصول (yield_history)**: مقدار تجمعی محصول (g) در هر نمونه.
|
||||
- **نرخ محصول (yield_rate_history)**: نرخ لحظهای (g/s) در هر نمونه.
|
||||
|
||||
محتوای داخل چارت دقیقاً همین آرایههاست؛ فرانت فقط اینها را روی محور زمان رسم میکند و با هر پاسخ جدید یک (یا چند) نقطه به انتهای آرایه اضافه میشود.
|
||||
|
||||
---
|
||||
|
||||
## ۴. خلاصهٔ جریان داده
|
||||
|
||||
| جهت | زمان | داده |
|
||||
|-----|------|------|
|
||||
| بکند → فرانت | لود صفحه | Config: اسلایدرها (min, max, step, unit)، ثابتها (max_height, max_leaves, ...)، تنظیمات چارت |
|
||||
| فرانت → بکند | کلیک شروع | محیط (light, water, soil_ph, ...) و growth_speed |
|
||||
| فرانت → بکند | کلیک توقف / ریست | stop یا reset |
|
||||
| فرانت → بکند | (اختیاری) تغییر اسلایدر | environment + growth_speed |
|
||||
| بکند → فرانت | حین شبیهسازی (polling/WS) | PlantState (height, leaves_count, branches_count, fruits_count, yield, yield_rate, is_healthy, can_continue) |
|
||||
| بکند → فرانت | همان پاسخ | Progress (growth_progress, light_status, water_status, yield_progress, yield_current, yield_rate_current) |
|
||||
| بکند → فرانت | همان پاسخ | ChartHistory (labels, height_history, leaf_history, yield_history, yield_rate_history) |
|
||||
|
||||
---
|
||||
|
||||
## ۵. دادههای ماک (Mock) کامل
|
||||
|
||||
برای توسعه و تست فرانت بدون بکند میتوان از این ماک استفاده کرد.
|
||||
|
||||
### ۵.۱ ماک Config (اسلایدرها + ثابتها + چارت)
|
||||
|
||||
```json
|
||||
{
|
||||
"sliders": [
|
||||
{
|
||||
"key": "light",
|
||||
"label": "نور",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 5,
|
||||
"unit_type": "percent",
|
||||
"default_value": 75,
|
||||
"icon": "☀️"
|
||||
},
|
||||
{
|
||||
"key": "water",
|
||||
"label": "آب",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 5,
|
||||
"unit_type": "percent",
|
||||
"default_value": 65,
|
||||
"icon": "💧"
|
||||
},
|
||||
{
|
||||
"key": "soil_ph",
|
||||
"label": "pH خاک",
|
||||
"min": 4,
|
||||
"max": 9,
|
||||
"step": 0.5,
|
||||
"unit_type": "number",
|
||||
"unit": "",
|
||||
"default_value": 6.5
|
||||
},
|
||||
{
|
||||
"key": "growth_speed",
|
||||
"label": "سرعت رشد",
|
||||
"min": 0.5,
|
||||
"max": 5,
|
||||
"step": 0.5,
|
||||
"unit_type": "number",
|
||||
"unit": "×",
|
||||
"default_value": 1.5
|
||||
}
|
||||
],
|
||||
"constants": {
|
||||
"max_height": 280,
|
||||
"max_leaves": 14,
|
||||
"max_branches": 6,
|
||||
"max_yield": 500,
|
||||
"yield_unit": "g",
|
||||
"yield_rate_unit": "g/s",
|
||||
"height_unit": "px"
|
||||
},
|
||||
"chart": {
|
||||
"title": "پیشرفت رشد",
|
||||
"x_axis_label": "زمان (ثانیه)",
|
||||
"series": [
|
||||
{ "key": "height", "label": "ارتفاع (px)", "y_axis_id": "yHeight", "min": 0, "max": 280, "unit": "px" },
|
||||
{ "key": "leaves", "label": "تعداد برگ", "y_axis_id": "yLeaf", "min": 0, "max": 14 },
|
||||
{ "key": "yield", "label": "محصول (g)", "y_axis_id": "yYield", "min": 0, "max": 500, "unit": "g" },
|
||||
{ "key": "yield_rate", "label": "نرخ محصول (g/s)", "y_axis_id": "yYieldRate", "min": 0, "unit": "g/s" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ۵.۲ ماک State + Progress + Chart (یک پاسخ ترکیبی)
|
||||
|
||||
```json
|
||||
{
|
||||
"plant": {
|
||||
"height": 142,
|
||||
"leaves_count": 5,
|
||||
"branches_count": 2,
|
||||
"fruits_count": 0,
|
||||
"yield": 12.4,
|
||||
"yield_rate": 0.087,
|
||||
"tick": 520,
|
||||
"is_healthy": true,
|
||||
"can_continue": true
|
||||
},
|
||||
"progress": {
|
||||
"growth_progress": 50,
|
||||
"light_status": 75,
|
||||
"water_status": 65,
|
||||
"yield_progress": 2.5,
|
||||
"yield_current": 12.4,
|
||||
"yield_rate_current": 0.087
|
||||
},
|
||||
"chart": {
|
||||
"labels": ["0s", "1s", "2s", "3s", "4s", "5s", "6s", "7s", "8s", "9s", "10s"],
|
||||
"height_history": [0, 5, 12, 28, 45, 68, 92, 110, 125, 135, 142],
|
||||
"leaf_history": [0, 0, 1, 2, 3, 4, 4, 5, 5, 5, 5],
|
||||
"yield_history": [0, 0, 0, 0.1, 0.5, 1.2, 3.2, 5.8, 8.2, 10.1, 12.4],
|
||||
"yield_rate_history": [0, 0, 0, 0.01, 0.03, 0.05, 0.06, 0.07, 0.08, 0.085, 0.087]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ۵.۳ ماک برای نمایش تدریجی چارت
|
||||
|
||||
چارت باید بر اساس همین آرایهها بهصورت تدریجی پر شود؛ هر بار بکند state جدید میفرستد، یک نقطه به انتهای هر آرایه اضافه میشود. مثال برای چند لحظهٔ متوالی:
|
||||
|
||||
```json
|
||||
// t=0
|
||||
{ "labels": ["0s"], "height_history": [0], "leaf_history": [0], "yield_history": [0], "yield_rate_history": [0] }
|
||||
|
||||
// t=1s
|
||||
{ "labels": ["0s", "1s"], "height_history": [0, 5], "leaf_history": [0, 0], "yield_history": [0, 0], "yield_rate_history": [0, 0] }
|
||||
|
||||
// t=2s
|
||||
{ "labels": ["0s", "1s", "2s"], "height_history": [0, 5, 12], "leaf_history": [0, 0, 1], "yield_history": [0, 0, 0], "yield_rate_history": [0, 0, 0] }
|
||||
|
||||
// t=3s
|
||||
{ "labels": ["0s", "1s", "2s", "3s"], "height_history": [0, 5, 12, 30], "leaf_history": [0, 0, 1, 2], "yield_history": [0, 0, 0, 0.1], "yield_rate_history": [0, 0, 0, 0.01] }
|
||||
|
||||
// t=4s
|
||||
{ "labels": ["0s", "1s", "2s", "3s", "4s"], "height_history": [0, 5, 12, 30, 48], "leaf_history": [0, 0, 1, 2, 3], "yield_history": [0, 0, 0, 0.1, 0.5], "yield_rate_history": [0, 0, 0, 0.01, 0.03] }
|
||||
```
|
||||
|
||||
ارتفاع و بقیهٔ مقادیر باید متناسب با سرعت رشد و منطق بکند افزایش یابند؛ اعداد بالا فقط نمونهٔ ماک هستند.
|
||||
|
||||
---
|
||||
|
||||
## ۶. نکات پیادهسازی فرانت
|
||||
|
||||
- **اسلایدرها**: با استفاده از `sliders` از config، هر کنترل با `min`, `max`, `step`, `unit_type`, `unit` و `default_value` رندر شود.
|
||||
- **آمار کارت (ارتفاع، برگ، شاخه، میوه، محصول، نرخ)**: مستقیم از `plant` در پاسخ state.
|
||||
- **سلامت گیاه**: با `is_healthy` و `can_continue` میتوان پیام یا استایل متفاوت نشان داد (مثلاً هشدار وقتی `can_continue === false`).
|
||||
- **نوارهای پیشرفت**: از `progress` برای پیشرفت رشد، وضعیت نور، وضعیت آب و محصولدهی.
|
||||
- **چارت**: فقط `chart.labels` و آرایههای `height_history`, `leaf_history`, `yield_history`, `yield_rate_history` را به کامپوننت نمودار بدهید؛ با هر پاسخ جدید آرایهها طولانی میشوند و نمودار بهصورت تدریجی بهروز میشود.
|
||||
|
||||
اگر endpointها یا فیلدهای اضافهای در بکند دارید، میتوان آنها را به همین سند اضافه کرد تا فرانت و بکند همیشه همخوان باشند.
|
||||
@@ -1,223 +0,0 @@
|
||||
# مستندات APIهای توصیه و تشخیص
|
||||
|
||||
این سند سه گروه API را شرح میدهد: **توصیه آبیاری**، **تشخیص آفت** و **توصیه کوددهی**. همهٔ پاسخها در حال حاضر از دادهٔ ثابت (mock) برگردانده میشوند و پارامترهای ورودی در پاسخ استفاده نمیشوند.
|
||||
|
||||
**پایهٔ آدرس API:** `/api/`
|
||||
|
||||
**قالب کلی پاسخ:**
|
||||
`{"status": "success", "data": <payload>}` — فقط با کد وضعیت HTTP 200.
|
||||
|
||||
---
|
||||
|
||||
## ۱. توصیه آبیاری (Irrigation Recommendation)
|
||||
|
||||
**پیشوند:** `api/irrigation-recommendation/`
|
||||
|
||||
### ۱.۱ دریافت تنظیمات (Config)
|
||||
|
||||
- **متد:** `GET`
|
||||
- **آدرس:** `api/irrigation-recommendation/config/`
|
||||
- **هدف:** برگرداندن اطلاعات مزرعه و لیست گزینههای محصول برای فرم توصیه آبیاری (هنگام بارگذاری صفحه).
|
||||
- **ورودی:** ندارد. پارامترهای query خوانده یا استفاده نمیشوند.
|
||||
|
||||
**نمونه پاسخ:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"farmInfo": {
|
||||
"soilType": "Loamy",
|
||||
"waterQuality": "Medium EC",
|
||||
"climateZone": "Temperate"
|
||||
},
|
||||
"cropOptions": [
|
||||
{"id": "wheat", "labelKey": "wheat", "icon": "tabler-wheat"},
|
||||
{"id": "corn", "labelKey": "corn", "icon": "tabler-plant-2"},
|
||||
{"id": "cotton", "labelKey": "cotton", "icon": "tabler-flower"},
|
||||
{"id": "saffron", "labelKey": "saffron", "icon": "tabler-flower-2"},
|
||||
{"id": "canola", "labelKey": "canola", "icon": "tabler-leaf"},
|
||||
{"id": "vegetables", "labelKey": "vegetables", "icon": "tabler-carrot"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| فیلد | نوع | توضیح |
|
||||
|------|-----|--------|
|
||||
| `farmInfo.soilType` | string | نوع خاک |
|
||||
| `farmInfo.waterQuality` | string | کیفیت آب (مثلاً EC) |
|
||||
| `farmInfo.climateZone` | string | منطقه اقلیمی |
|
||||
| `cropOptions` | array | لیست محصولات: `id`, `labelKey`, `icon` |
|
||||
|
||||
---
|
||||
|
||||
### ۱.۲ دریافت توصیه آبیاری (Recommend)
|
||||
|
||||
- **متد:** `POST`
|
||||
- **آدرس:** `api/irrigation-recommendation/recommend/`
|
||||
- **هدف:** برگرداندن یک برنامهٔ آبیاری ثابت (تعداد در هفته، مدت، بهترین زمان، رطوبت، هشدار).
|
||||
- **ورودی (بدن درخواست، اختیاری):** میتوانید JSON با فیلدهایی مثل `crop_id`, `soilType`, `waterQuality`, `climateZone` بفرستید؛ در پاسخ فعلی استفاده نمیشوند.
|
||||
- **CSRF:** این endpoint از CSRF معاف است (برای فراخوانی از فرانت بدون توکن).
|
||||
|
||||
**نمونه پاسخ:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"plan": {
|
||||
"frequencyPerWeek": 4,
|
||||
"durationMinutes": 45,
|
||||
"bestTimeOfDay": "05:00 - 07:00",
|
||||
"moistureLevel": 72,
|
||||
"warning": "Avoid irrigation during midday hours in the coming week due to forecasted high temperatures."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| فیلد | نوع | توضیح |
|
||||
|------|-----|--------|
|
||||
| `plan.frequencyPerWeek` | number | تعداد آبیاری در هفته |
|
||||
| `plan.durationMinutes` | number | مدت هر نوبت (دقیقه) |
|
||||
| `plan.bestTimeOfDay` | string | بهترین بازه زمانی روز |
|
||||
| `plan.moistureLevel` | number | سطح رطوبت هدف (درصد) |
|
||||
| `plan.warning` | string | هشدار یا توصیه اضافه |
|
||||
|
||||
---
|
||||
|
||||
## ۲. تشخیص آفت (Pest Detection)
|
||||
|
||||
**پیشوند:** `api/pest-detection/`
|
||||
|
||||
### ۲.۱ تحلیل تصویر (Analyze)
|
||||
|
||||
- **متد:** `POST`
|
||||
- **آدرس:** `api/pest-detection/analyze/`
|
||||
- **هدف:** برگرداندن نتیجهٔ ثابت تشخیص آفت (نام آفت، اطمینان، توضیح، درمان) — برای زمانی که کاربر تصویر گیاه را آپلود و درخواست تحلیل میکند.
|
||||
- **ورودی (بدن درخواست، اختیاری):** JSON یا form-data (مثلاً شامل `image` یا `file`). در پاسخ فعلی استفاده نمیشود.
|
||||
- **CSRF:** این endpoint از CSRF معاف است.
|
||||
|
||||
**نمونه پاسخ:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"pest": "شپشک",
|
||||
"confidence": 92,
|
||||
"description": "حشرات کوچک مکنده شیره که باعث پیچ خوردگی برگ میشوند.",
|
||||
"treatment": "یک بار در هفته از اسپری روغن نیم استفاده کنید."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| فیلد | نوع | توضیح |
|
||||
|------|-----|--------|
|
||||
| `pest` | string | نام آفت |
|
||||
| `confidence` | number | درصد اطمینان (۰–۱۰۰) |
|
||||
| `description` | string | توضیح کوتاه آفت |
|
||||
| `treatment` | string | توصیه درمان |
|
||||
|
||||
---
|
||||
|
||||
## ۳. توصیه کوددهی (Fertilization Recommendation)
|
||||
|
||||
**پیشوند:** `api/fertilization-recommendation/`
|
||||
|
||||
### ۳.۱ دریافت تنظیمات (Config)
|
||||
|
||||
- **متد:** `GET`
|
||||
- **آدرس:** `api/fertilization-recommendation/config/`
|
||||
- **هدف:** برگرداندن دادهٔ مزرعه، مراحل رشد و گزینههای محصول برای فرم توصیه کوددهی (هنگام بارگذاری صفحه).
|
||||
- **ورودی:** ندارد.
|
||||
|
||||
**نمونه پاسخ:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"farmData": {
|
||||
"soilType": "Loamy",
|
||||
"organicMatter": "Medium (2.5%)",
|
||||
"waterEC": "1.2 dS/m"
|
||||
},
|
||||
"growthStages": [
|
||||
{"id": "prePlanting", "icon": "tabler-seedling"},
|
||||
{"id": "earlyGrowth", "icon": "tabler-leaf"},
|
||||
{"id": "flowering", "icon": "tabler-flower"},
|
||||
{"id": "fruiting", "icon": "tabler-apple"},
|
||||
{"id": "postHarvest", "icon": "tabler-basket"}
|
||||
],
|
||||
"cropOptions": [
|
||||
{"id": "wheat", "labelKey": "wheat", "icon": "tabler-wheat"},
|
||||
{"id": "corn", "labelKey": "corn", "icon": "tabler-plant-2"},
|
||||
{"id": "cotton", "labelKey": "cotton", "icon": "tabler-flower"},
|
||||
{"id": "saffron", "labelKey": "saffron", "icon": "tabler-flower-2"},
|
||||
{"id": "canola", "labelKey": "canola", "icon": "tabler-leaf"},
|
||||
{"id": "vegetables", "labelKey": "vegetables", "icon": "tabler-carrot"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| فیلد | نوع | توضیح |
|
||||
|------|-----|--------|
|
||||
| `farmData.soilType` | string | نوع خاک |
|
||||
| `farmData.organicMatter` | string | ماده آلی خاک |
|
||||
| `farmData.waterEC` | string | EC آب |
|
||||
| `growthStages` | array | مراحل رشد: `id`, `icon` |
|
||||
| `cropOptions` | array | لیست محصولات: `id`, `labelKey`, `icon` |
|
||||
|
||||
---
|
||||
|
||||
### ۳.۲ دریافت توصیه کوددهی (Recommend)
|
||||
|
||||
- **متد:** `POST`
|
||||
- **آدرس:** `api/fertilization-recommendation/recommend/`
|
||||
- **هدف:** برگرداندن یک برنامهٔ کوددهی ثابت (نسبت NPK، مقدار در هکتار، روش و فاصله مصرف، استدلال).
|
||||
- **ورودی (بدن درخواست، اختیاری):** JSON با فیلدهایی مثل `crop_id`, `growth_stage`, `soilType`, `organicMatter`, `waterEC`. در پاسخ فعلی استفاده نمیشوند.
|
||||
- **CSRF:** این endpoint از CSRF معاف است.
|
||||
|
||||
**نمونه پاسخ:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"plan": {
|
||||
"npkRatio": "20-20-20 (NPK)",
|
||||
"amountPerHectare": "150 kg/ha",
|
||||
"applicationMethod": "Foliar spray + soil broadcast",
|
||||
"applicationInterval": "Every 14 days",
|
||||
"reasoning": "Your loamy soil with medium organic matter (2.5%) provides good nutrient retention. Water EC of 1.2 dS/m indicates low salinity—suitable for most crops. At the flowering stage, increased phosphorus supports bloom development. We recommend a balanced NPK to maintain nitrogen for vegetative growth while boosting phosphorous for flowering."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| فیلد | نوع | توضیح |
|
||||
|------|-----|--------|
|
||||
| `plan.npkRatio` | string | نسبت NPK پیشنهادی |
|
||||
| `plan.amountPerHectare` | string | مقدار مصرف در هکتار |
|
||||
| `plan.applicationMethod` | string | روش مصرف (مثلاً محلولپاشی، خاکی) |
|
||||
| `plan.applicationInterval` | string | فاصله بین مصرف |
|
||||
| `plan.reasoning` | string | توضیح/استدلال توصیه |
|
||||
|
||||
---
|
||||
|
||||
## خلاصه Endpointها
|
||||
|
||||
| ماژول | متد | Endpoint |
|
||||
|--------|------|----------|
|
||||
| Irrigation | GET | `/api/irrigation-recommendation/config/` |
|
||||
| Irrigation | POST | `/api/irrigation-recommendation/recommend/` |
|
||||
| Pest Detection | POST | `/api/pest-detection/analyze/` |
|
||||
| Fertilization | GET | `/api/fertilization-recommendation/config/` |
|
||||
| Fertilization | POST | `/api/fertilization-recommendation/recommend/` |
|
||||
|
||||
---
|
||||
|
||||
**توجه:** در نسخهٔ فعلی هیچ پردازش، اعتبارسنجی یا استفاده از پارامترهای ورودی در پاسخ انجام نمیشود؛ همهٔ خروجیها از دادهٔ ثابت (mock) هستند.
|
||||
@@ -159,3 +159,5 @@ SIMPLE_JWT = {
|
||||
"ROTATE_REFRESH_TOKENS": False,
|
||||
"BLACKLIST_AFTER_ROTATION": False,
|
||||
}
|
||||
|
||||
CROP_ZONE_CHUNK_AREA_SQM = float(os.getenv("CROP_ZONE_CHUNK_AREA_SQM", "10000"))
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CropArea',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, unique=True)),
|
||||
('geometry', models.JSONField(default=dict)),
|
||||
('points', models.JSONField(default=list)),
|
||||
('center', models.JSONField(default=dict)),
|
||||
('area_sqm', models.FloatField()),
|
||||
('area_hectares', models.FloatField()),
|
||||
('chunk_area_sqm', models.FloatField()),
|
||||
('zone_count', models.PositiveIntegerField(default=0)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'crop_areas',
|
||||
'ordering': ['-created_at', '-id'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CropZone',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, unique=True)),
|
||||
('zone_id', models.CharField(max_length=64)),
|
||||
('geometry', models.JSONField(default=dict)),
|
||||
('points', models.JSONField(default=list)),
|
||||
('center', models.JSONField(default=dict)),
|
||||
('area_sqm', models.FloatField()),
|
||||
('area_hectares', models.FloatField()),
|
||||
('sequence', models.PositiveIntegerField()),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('crop_area', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='zones', to='crop_zoning.croparea')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'crop_zones',
|
||||
'ordering': ['sequence', 'id'],
|
||||
'constraints': [models.UniqueConstraint(fields=('crop_area', 'zone_id'), name='unique_crop_area_zone_id')],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,51 @@
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class CropArea(models.Model):
|
||||
uuid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False, db_index=True)
|
||||
geometry = models.JSONField(default=dict)
|
||||
points = models.JSONField(default=list)
|
||||
center = models.JSONField(default=dict)
|
||||
area_sqm = models.FloatField()
|
||||
area_hectares = models.FloatField()
|
||||
chunk_area_sqm = models.FloatField()
|
||||
zone_count = models.PositiveIntegerField(default=0)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "crop_areas"
|
||||
ordering = ["-created_at", "-id"]
|
||||
|
||||
def __str__(self):
|
||||
return f"Area {self.uuid}"
|
||||
|
||||
|
||||
class CropZone(models.Model):
|
||||
uuid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False, db_index=True)
|
||||
crop_area = models.ForeignKey(
|
||||
CropArea,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="zones",
|
||||
)
|
||||
zone_id = models.CharField(max_length=64)
|
||||
geometry = models.JSONField(default=dict)
|
||||
points = models.JSONField(default=list)
|
||||
center = models.JSONField(default=dict)
|
||||
area_sqm = models.FloatField()
|
||||
area_hectares = models.FloatField()
|
||||
sequence = models.PositiveIntegerField()
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "crop_zones"
|
||||
ordering = ["sequence", "id"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=["crop_area", "zone_id"], name="unique_crop_area_zone_id"),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.zone_id
|
||||
@@ -0,0 +1,202 @@
|
||||
import math
|
||||
from copy import deepcopy
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
|
||||
from .models import CropArea, CropZone
|
||||
|
||||
EARTH_RADIUS_METERS = 6378137.0
|
||||
|
||||
|
||||
def get_chunk_area_sqm():
|
||||
raw_value = getattr(settings, "CROP_ZONE_CHUNK_AREA_SQM", 0)
|
||||
try:
|
||||
chunk_area = float(raw_value)
|
||||
except (TypeError, ValueError):
|
||||
chunk_area = 0
|
||||
if chunk_area <= 0:
|
||||
raise ValueError("CROP_ZONE_CHUNK_AREA_SQM must be a positive number.")
|
||||
return chunk_area
|
||||
|
||||
|
||||
def get_polygon_ring(area_feature):
|
||||
geometry = (area_feature or {}).get("geometry", {})
|
||||
coordinates = geometry.get("coordinates", [])
|
||||
if not coordinates or not coordinates[0]:
|
||||
raise ValueError("Area polygon coordinates are required.")
|
||||
return coordinates[0]
|
||||
|
||||
|
||||
def polygon_area_sqm(ring):
|
||||
if len(ring) < 4:
|
||||
return 0.0
|
||||
|
||||
latitudes = [point[1] for point in ring]
|
||||
mean_latitude = math.radians(sum(latitudes) / len(latitudes))
|
||||
|
||||
projected_points = []
|
||||
for longitude, latitude in ring:
|
||||
x = math.radians(longitude) * EARTH_RADIUS_METERS * math.cos(mean_latitude)
|
||||
y = math.radians(latitude) * EARTH_RADIUS_METERS
|
||||
projected_points.append((x, y))
|
||||
|
||||
area = 0.0
|
||||
for index in range(len(projected_points) - 1):
|
||||
x1, y1 = projected_points[index]
|
||||
x2, y2 = projected_points[index + 1]
|
||||
area += (x1 * y2) - (x2 * y1)
|
||||
|
||||
return abs(area) / 2.0
|
||||
|
||||
|
||||
def normalize_points(ring):
|
||||
if len(ring) > 1 and ring[0] == ring[-1]:
|
||||
ring = ring[:-1]
|
||||
return [[point[0], point[1]] for point in ring]
|
||||
|
||||
|
||||
def calculate_center(points):
|
||||
if not points:
|
||||
return {"longitude": 0.0, "latitude": 0.0}
|
||||
|
||||
longitude = sum(point[0] for point in points) / len(points)
|
||||
latitude = sum(point[1] for point in points) / len(points)
|
||||
return {
|
||||
"longitude": round(longitude, 8),
|
||||
"latitude": round(latitude, 8),
|
||||
}
|
||||
|
||||
|
||||
def build_zone_square(area_points, center, zone_area_sqm):
|
||||
if len(area_points) < 4:
|
||||
return area_points
|
||||
|
||||
width = math.sqrt(max(zone_area_sqm, 1))
|
||||
half_width = width / 2.0
|
||||
|
||||
latitude_factor = 111320.0
|
||||
longitude_factor = 111320.0 * math.cos(math.radians(center["latitude"]))
|
||||
if longitude_factor == 0:
|
||||
longitude_factor = 1.0
|
||||
|
||||
delta_lat = half_width / latitude_factor
|
||||
delta_lng = half_width / longitude_factor
|
||||
|
||||
return [
|
||||
[round(center["longitude"] - delta_lng, 8), round(center["latitude"] - delta_lat, 8)],
|
||||
[round(center["longitude"] + delta_lng, 8), round(center["latitude"] - delta_lat, 8)],
|
||||
[round(center["longitude"] + delta_lng, 8), round(center["latitude"] + delta_lat, 8)],
|
||||
[round(center["longitude"] - delta_lng, 8), round(center["latitude"] + delta_lat, 8)],
|
||||
]
|
||||
|
||||
|
||||
def split_area_into_zones(area_feature):
|
||||
area_ring = get_polygon_ring(area_feature)
|
||||
area_points = normalize_points(area_ring)
|
||||
area_center = calculate_center(area_points)
|
||||
total_area_sqm = polygon_area_sqm(area_ring)
|
||||
chunk_area_sqm = get_chunk_area_sqm()
|
||||
zone_count = max(1, math.ceil(total_area_sqm / chunk_area_sqm))
|
||||
|
||||
zones = []
|
||||
remaining_area = total_area_sqm
|
||||
base_longitude = area_center["longitude"]
|
||||
base_latitude = area_center["latitude"]
|
||||
|
||||
for sequence in range(zone_count):
|
||||
zone_area_sqm = min(chunk_area_sqm, remaining_area) if sequence < zone_count - 1 else remaining_area
|
||||
if zone_area_sqm <= 0:
|
||||
zone_area_sqm = min(chunk_area_sqm, total_area_sqm)
|
||||
|
||||
shift = (sequence - ((zone_count - 1) / 2)) * 0.0003
|
||||
zone_center = {
|
||||
"longitude": round(base_longitude + shift, 8),
|
||||
"latitude": round(base_latitude, 8),
|
||||
}
|
||||
zone_points = build_zone_square(area_points, zone_center, zone_area_sqm)
|
||||
zone_geometry = {
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"zone_id": f"zone-{sequence}",
|
||||
"sequence": sequence,
|
||||
"area_sqm": round(zone_area_sqm, 2),
|
||||
"area_hectares": round(zone_area_sqm / 10000, 4),
|
||||
"center": zone_center,
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [[*zone_points, zone_points[0]]],
|
||||
},
|
||||
}
|
||||
zones.append(
|
||||
{
|
||||
"zone_id": f"zone-{sequence}",
|
||||
"geometry": zone_geometry,
|
||||
"points": zone_points,
|
||||
"center": zone_center,
|
||||
"area_sqm": zone_area_sqm,
|
||||
"area_hectares": zone_area_sqm / 10000,
|
||||
"sequence": sequence,
|
||||
}
|
||||
)
|
||||
remaining_area = max(0.0, remaining_area - zone_area_sqm)
|
||||
|
||||
area_geometry = deepcopy(area_feature)
|
||||
area_geometry.setdefault("properties", {})
|
||||
area_geometry["properties"].update(
|
||||
{
|
||||
"center": area_center,
|
||||
"area_sqm": round(total_area_sqm, 2),
|
||||
"area_hectares": round(total_area_sqm / 10000, 4),
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"area": {
|
||||
"geometry": area_geometry,
|
||||
"points": area_points,
|
||||
"center": area_center,
|
||||
"area_sqm": total_area_sqm,
|
||||
"area_hectares": total_area_sqm / 10000,
|
||||
"chunk_area_sqm": chunk_area_sqm,
|
||||
"zone_count": zone_count,
|
||||
},
|
||||
"zones": zones,
|
||||
}
|
||||
|
||||
|
||||
def persist_zones(area_feature):
|
||||
zoning_result = split_area_into_zones(area_feature)
|
||||
area_data = zoning_result["area"]
|
||||
|
||||
with transaction.atomic():
|
||||
crop_area = CropArea.objects.create(
|
||||
geometry=area_data["geometry"],
|
||||
points=area_data["points"],
|
||||
center=area_data["center"],
|
||||
area_sqm=round(area_data["area_sqm"], 2),
|
||||
area_hectares=round(area_data["area_hectares"], 4),
|
||||
chunk_area_sqm=round(area_data["chunk_area_sqm"], 2),
|
||||
zone_count=area_data["zone_count"],
|
||||
)
|
||||
|
||||
CropZone.objects.bulk_create(
|
||||
[
|
||||
CropZone(
|
||||
crop_area=crop_area,
|
||||
zone_id=zone["zone_id"],
|
||||
geometry=zone["geometry"],
|
||||
points=zone["points"],
|
||||
center=zone["center"],
|
||||
area_sqm=round(zone["area_sqm"], 2),
|
||||
area_hectares=round(zone["area_hectares"], 4),
|
||||
sequence=zone["sequence"],
|
||||
)
|
||||
for zone in zoning_result["zones"]
|
||||
]
|
||||
)
|
||||
|
||||
zoning_result["area"]["id"] = crop_area.id
|
||||
zoning_result["area"]["uuid"] = str(crop_area.uuid)
|
||||
return zoning_result
|
||||
+51
-13
@@ -5,6 +5,7 @@ Response format: {"status": "success", "data": <payload>}. HTTP 200 only.
|
||||
No processing, validation, or use of input parameters in responses.
|
||||
"""
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from rest_framework import serializers, status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
@@ -21,6 +22,7 @@ from .mock_data import (
|
||||
ZONES_SOIL_QUALITY_RESPONSE_DATA,
|
||||
ZONES_WATER_NEED_RESPONSE_DATA,
|
||||
)
|
||||
from .services import persist_zones
|
||||
|
||||
|
||||
class AreaView(APIView):
|
||||
@@ -88,23 +90,17 @@ class ZonesInitialView(APIView):
|
||||
POST endpoint for initial zone data (map + hover/tooltip).
|
||||
|
||||
Purpose:
|
||||
Accepts zones (FeatureCollection of grid squares) and returns static
|
||||
initial data per zone for map rendering and hover/tooltip display.
|
||||
Does not include reason or criteria (those are in zone details).
|
||||
Accepts the main area polygon and creates zones based on configured
|
||||
area chunk size. Stores generated zones in database.
|
||||
|
||||
Input parameters (body, JSON):
|
||||
- zones: GeoJSON FeatureCollection. Location: body. Grid square polygons.
|
||||
- products: array of strings, optional. Location: body. Product IDs.
|
||||
Not read or used in response.
|
||||
- area: GeoJSON Feature. Location: body. Main land polygon.
|
||||
If omitted, the static area from mock data is used.
|
||||
|
||||
Response structure:
|
||||
- status: string, always "success".
|
||||
- status: string.
|
||||
- data: object with total_area_hectares, total_area_sqm, zone_count,
|
||||
zones (array of { zoneId, geometry, crop, matchPercent, waterNeed,
|
||||
estimatedProfit }).
|
||||
|
||||
No processing or validation is performed on inputs. Input values are
|
||||
not used in the response.
|
||||
chunk_area_sqm and zones.
|
||||
"""
|
||||
|
||||
@extend_schema(
|
||||
@@ -113,12 +109,54 @@ class ZonesInitialView(APIView):
|
||||
responses={200: status_response("CropZoningZonesInitialResponse", data=serializers.JSONField())},
|
||||
)
|
||||
def post(self, request):
|
||||
area_feature = request.data.get("area") or AREA_RESPONSE_DATA.get("area")
|
||||
|
||||
try:
|
||||
zoning_result = persist_zones(area_feature)
|
||||
except ValueError as exc:
|
||||
return Response(
|
||||
{"status": "error", "message": str(exc)},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except ImproperlyConfigured as exc:
|
||||
return Response(
|
||||
{"status": "error", "message": str(exc)},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
area_data = zoning_result["area"]
|
||||
response_data = {
|
||||
"area": {
|
||||
"id": area_data["id"],
|
||||
"uuid": area_data["uuid"],
|
||||
"geometry": area_data["geometry"],
|
||||
"points": area_data["points"],
|
||||
"center": area_data["center"],
|
||||
"area_sqm": round(area_data["area_sqm"], 2),
|
||||
"area_hectares": round(area_data["area_hectares"], 4),
|
||||
"chunk_area_sqm": round(area_data["chunk_area_sqm"], 2),
|
||||
"zone_count": area_data["zone_count"],
|
||||
},
|
||||
"zones": [
|
||||
{
|
||||
"zoneId": zone["zone_id"],
|
||||
"geometry": zone["geometry"],
|
||||
"points": zone["points"],
|
||||
"center": zone["center"],
|
||||
"area_sqm": round(zone["area_sqm"], 2),
|
||||
"area_hectares": round(zone["area_hectares"], 4),
|
||||
}
|
||||
for zone in zoning_result["zones"]
|
||||
],
|
||||
}
|
||||
|
||||
return Response(
|
||||
{"status": "success", "data": ZONES_INITIAL_RESPONSE_DATA},
|
||||
{"status": "success", "data": response_data},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
|
||||
class ZonesWaterNeedView(APIView):
|
||||
"""
|
||||
POST endpoint for water need per zone (water need layer).
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"task_id": "sample-task-123",
|
||||
"task_id": "farm-ai-chat-task-123",
|
||||
"status": "FAILURE",
|
||||
"error": "Sample task failed."
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"task_id": "sample-task-123",
|
||||
"task_id": "farm-ai-chat-task-123",
|
||||
"status": "PENDING",
|
||||
"message": "تسک در صف یا یافت نشد."
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"task_id": "sample-task-123",
|
||||
"task_id": "farm-ai-chat-task-123",
|
||||
"status": "PROGRESS",
|
||||
"progress": {
|
||||
"current": 1,
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"task_id": "sample-task-123",
|
||||
"task_id": "farm-ai-chat-task-123",
|
||||
"status": "SUCCESS",
|
||||
"result": "done"
|
||||
"result": {
|
||||
"$ref": "rag/chat-post_200_stream.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,13 @@ class MockLoader:
|
||||
|
||||
selected_file = sorted(mock_files, key=self._mock_file_priority)[0]
|
||||
with selected_file.open("r", encoding="utf-8") as file:
|
||||
service_root = self.base_path / service_name
|
||||
return LoadedMockResponse(
|
||||
data=json.load(file),
|
||||
data=self._resolve_references(
|
||||
json.load(file),
|
||||
current_file=selected_file,
|
||||
service_root=service_root,
|
||||
),
|
||||
status_code=self._extract_status_code(selected_file),
|
||||
file_path=str(selected_file),
|
||||
)
|
||||
@@ -99,3 +104,60 @@ class MockLoader:
|
||||
elif "pending" in stem or "progress" in stem:
|
||||
keyword_rank = 1
|
||||
return (status_code >= 400, keyword_rank, stem)
|
||||
|
||||
def _resolve_references(self, value, current_file, service_root, seen=None):
|
||||
current_file = current_file.resolve()
|
||||
service_root = service_root.resolve()
|
||||
seen = seen or {current_file}
|
||||
|
||||
if isinstance(value, list):
|
||||
return [
|
||||
self._resolve_references(item, current_file=current_file, service_root=service_root, seen=seen)
|
||||
for item in value
|
||||
]
|
||||
|
||||
if not isinstance(value, dict):
|
||||
return value
|
||||
|
||||
ref_path = value.get("$ref")
|
||||
if isinstance(ref_path, str) and len(value) == 1:
|
||||
referenced_file = self._resolve_reference_path(
|
||||
ref_path=ref_path,
|
||||
current_file=current_file,
|
||||
service_root=service_root,
|
||||
)
|
||||
|
||||
if referenced_file in seen:
|
||||
raise MockFileNotFound(f"Circular mock reference detected for '{referenced_file}'.")
|
||||
|
||||
with referenced_file.open("r", encoding="utf-8") as file:
|
||||
return self._resolve_references(
|
||||
json.load(file),
|
||||
current_file=referenced_file,
|
||||
service_root=service_root,
|
||||
seen=seen | {referenced_file},
|
||||
)
|
||||
|
||||
return {
|
||||
key: self._resolve_references(item, current_file=current_file, service_root=service_root, seen=seen)
|
||||
for key, item in value.items()
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _resolve_reference_path(ref_path, current_file, service_root):
|
||||
candidates = [
|
||||
current_file.parent / ref_path,
|
||||
service_root / ref_path,
|
||||
]
|
||||
|
||||
service_root_resolved = service_root.resolve()
|
||||
for candidate in candidates:
|
||||
candidate_resolved = candidate.resolve()
|
||||
try:
|
||||
candidate_resolved.relative_to(service_root_resolved)
|
||||
except ValueError:
|
||||
continue
|
||||
if candidate_resolved.is_file():
|
||||
return candidate_resolved
|
||||
|
||||
raise MockFileNotFound(f"Referenced mock file '{ref_path}' was not found.")
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase, override_settings
|
||||
from rest_framework.test import APIRequestFactory, force_authenticate
|
||||
|
||||
from .models import Conversation, Message
|
||||
from .views import ChatTaskStatusView
|
||||
|
||||
|
||||
@override_settings(USE_EXTERNAL_API_MOCK=True)
|
||||
class ChatTaskStatusViewTests(TestCase):
|
||||
def setUp(self):
|
||||
self.factory = APIRequestFactory()
|
||||
self.user = get_user_model().objects.create_user(
|
||||
username="farmer",
|
||||
password="secret123",
|
||||
email="farmer@example.com",
|
||||
phone_number="09120000000",
|
||||
)
|
||||
self.conversation = Conversation.objects.create(
|
||||
owner=self.user,
|
||||
title="Irrigation chat",
|
||||
farm_context={},
|
||||
)
|
||||
self.user_message = Message.objects.create(
|
||||
conversation=self.conversation,
|
||||
role=Message.ROLE_USER,
|
||||
content="What is the best irrigation plan?",
|
||||
raw_response={
|
||||
"task_id": "farm-ai-chat-task-123",
|
||||
"status": "PENDING",
|
||||
"status_url": "/api/tasks/farm-ai-chat-task-123/status/",
|
||||
},
|
||||
)
|
||||
|
||||
def test_status_success_uses_chat_mock_result_and_persists_assistant_message(self):
|
||||
request = self.factory.get("/api/farm-ai-assistant/chat/task/farm-ai-chat-task-123/status/")
|
||||
force_authenticate(request, user=self.user)
|
||||
|
||||
response = ChatTaskStatusView.as_view()(request, task_id="farm-ai-chat-task-123")
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data["status"], "success")
|
||||
self.assertEqual(response.data["data"]["task_id"], "farm-ai-chat-task-123")
|
||||
self.assertEqual(response.data["data"]["status"], "SUCCESS")
|
||||
self.assertEqual(response.data["data"]["conversation_id"], str(self.conversation.uuid))
|
||||
self.assertEqual(response.data["data"]["result"]["content"], "Here is the recommended plan.")
|
||||
self.assertEqual(len(response.data["data"]["result"]["sections"]), 3)
|
||||
self.assertEqual(response.data["data"]["result"]["task_id"], "farm-ai-chat-task-123")
|
||||
|
||||
assistant_message = (
|
||||
self.conversation.messages.filter(role=Message.ROLE_ASSISTANT)
|
||||
.order_by("-created_at")
|
||||
.first()
|
||||
)
|
||||
self.assertIsNotNone(assistant_message)
|
||||
self.assertEqual(assistant_message.content, "Here is the recommended plan.")
|
||||
self.assertEqual(assistant_message.raw_response["task_id"], "farm-ai-chat-task-123")
|
||||
self.assertEqual(len(assistant_message.raw_response["sections"]), 3)
|
||||
@@ -12,7 +12,7 @@ from .views import (
|
||||
|
||||
urlpatterns = [
|
||||
path("context/", ContextView.as_view(), name="farm-ai-assistant-context"),
|
||||
path("chat/", ChatView.as_view(), name="farm-ai-assistant-chat"),
|
||||
# path("chat/", ChatView.as_view(), name="farm-ai-assistant-chat"),
|
||||
path("chat/task/", ChatTaskCreateView.as_view(), name="farm-ai-assistant-chat-task-create"),
|
||||
path("chat/task/<str:task_id>/status/", ChatTaskStatusView.as_view(), name="farm-ai-assistant-chat-task-status"),
|
||||
path("chats/", ChatListCreateView.as_view(), name="farm-ai-assistant-chat-list-create"),
|
||||
|
||||
@@ -201,6 +201,29 @@ class ConversationAccessMixin:
|
||||
|
||||
return task_status_payload
|
||||
|
||||
def _extract_structured_task_result(self, adapter_data):
|
||||
payload_source = adapter_data
|
||||
if isinstance(adapter_data, dict) and isinstance(adapter_data.get("data"), dict):
|
||||
payload_source = adapter_data["data"]
|
||||
|
||||
if not isinstance(payload_source, dict):
|
||||
return None
|
||||
|
||||
result = payload_source.get("result")
|
||||
if isinstance(result, dict):
|
||||
return result
|
||||
|
||||
if payload_source.get("status") == "SUCCESS":
|
||||
content = payload_source.get("content")
|
||||
sections = payload_source.get("sections")
|
||||
if content or sections:
|
||||
return {
|
||||
"content": content or "",
|
||||
"sections": sections or [],
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _serialize_chat_message(message):
|
||||
raw_response = message.raw_response if isinstance(message.raw_response, dict) else {}
|
||||
@@ -541,7 +564,10 @@ class ChatTaskStatusView(ConversationAccessMixin, APIView):
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
|
||||
result = task_status_payload.get("result")
|
||||
result = self._extract_structured_task_result(adapter_response.data)
|
||||
if result is not None:
|
||||
task_status_payload["result"] = result
|
||||
|
||||
if user_message and task_status_payload.get("status") == "SUCCESS" and isinstance(result, dict):
|
||||
assistant_payload = self._persist_task_result(user_message, task_id, result)
|
||||
task_status_payload["result"] = assistant_payload
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"task_id": "sample-task-123",
|
||||
"task_id": "farm-ai-chat-task-123",
|
||||
"status": "FAILURE",
|
||||
"error": "Sample task failed."
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"task_id": "sample-task-123",
|
||||
"task_id": "farm-ai-chat-task-123",
|
||||
"status": "PENDING",
|
||||
"message": "تسک در صف یا یافت نشد."
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"task_id": "sample-task-123",
|
||||
"task_id": "farm-ai-chat-task-123",
|
||||
"status": "PROGRESS",
|
||||
"progress": {
|
||||
"current": 1,
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"task_id": "sample-task-123",
|
||||
"task_id": "farm-ai-chat-task-123",
|
||||
"status": "SUCCESS",
|
||||
"result": "done"
|
||||
"result": {
|
||||
"$ref": "rag/chat-post_200_stream.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user