CI/CD
This commit is contained in:
@@ -10,6 +10,7 @@ DB_USER=croplogic
|
|||||||
DB_PASSWORD=changeme
|
DB_PASSWORD=changeme
|
||||||
DB_HOST=db
|
DB_HOST=db
|
||||||
DB_PORT=3306
|
DB_PORT=3306
|
||||||
|
DB_ROOT_PASSWORD=root
|
||||||
|
|
||||||
# Optional: for running manage.py from host (local DB)
|
# Optional: for running manage.py from host (local DB)
|
||||||
# DB_HOST=127.0.0.1
|
# DB_HOST=127.0.0.1
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
name: Backend Service CI/CD
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [production]
|
||||||
|
paths:
|
||||||
|
- '**'
|
||||||
|
- '.gitea/workflows/backend.yml'
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
branches: [production]
|
||||||
|
paths:
|
||||||
|
- '**'
|
||||||
|
- '.gitea/workflows/backend.yml'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Lint & Test
|
||||||
|
runs-on: self-hosted
|
||||||
|
container:
|
||||||
|
image: mirror2.chabokan.net/ubuntu:22.04
|
||||||
|
options: --add-host gitea:172.17.0.1
|
||||||
|
|
||||||
|
steps:
|
||||||
|
|
||||||
|
|
||||||
|
- name: Setup Ubuntu apt mirrors
|
||||||
|
run: |
|
||||||
|
tee /etc/apt/sources.list > /dev/null <<'EOF'
|
||||||
|
deb https://mirror-linux.runflare.com/ubuntu/ noble main restricted universe multiverse
|
||||||
|
deb https://mirror-linux.runflare.com/ubuntu/ noble-updates main restricted universe multiverse
|
||||||
|
deb https://mirror-linux.runflare.com/ubuntu/ noble-backports main restricted universe multiverse
|
||||||
|
deb https://mirror-linux.runflare.com/ubuntu/ noble-security main restricted universe multiverse
|
||||||
|
|
||||||
|
deb [trusted=yes] https://mirror2.chabokan.net/ubuntu focal main universe
|
||||||
|
deb [trusted=yes] https://mirror2.chabokan.net/ubuntu focal-updates main universe
|
||||||
|
deb [trusted=yes] https://mirror2.chabokan.net/ubuntu focal-security main universe
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
deb http://mirror.iranserver.com/ubuntu/ jammy main restricted
|
||||||
|
deb-src http://mirror.iranserver.com/ubuntu/ jammy main restricted
|
||||||
|
|
||||||
|
|
||||||
|
deb http://mirror.iranserver.com/ubuntu/ jammy-updates main restricted
|
||||||
|
deb-src http://mirror.iranserver.com/ubuntu/ jammy-updates main restricted
|
||||||
|
|
||||||
|
|
||||||
|
deb http://mirror.iranserver.com/ubuntu/ jammy universe
|
||||||
|
deb-src http://mirror.iranserver.com/ubuntu/ jammy universe
|
||||||
|
deb http://mirror.iranserver.com/ubuntu/ jammy-updates universe
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
deb http://mirror.iranserver.com/ubuntu/ jammy multiverse
|
||||||
|
deb-src http://mirror.iranserver.com/ubuntu/ jammy multiverse
|
||||||
|
deb http://mirror.iranserver.com/ubuntu/ jammy-updates multiverse
|
||||||
|
deb-src http://mirror.iranserver.com/ubuntu/ jammy-updates multiverse
|
||||||
|
|
||||||
|
|
||||||
|
deb http://mirror.iranserver.com/ubuntu/ jammy-backports main restricted universe multiverse
|
||||||
|
deb-src http://mirror.iranserver.com/ubuntu/ jammy-backports main restricted universe multiverse
|
||||||
|
|
||||||
|
|
||||||
|
EOF
|
||||||
|
apt-get update
|
||||||
|
- name: Install git
|
||||||
|
run: |
|
||||||
|
apt-get install -y git
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
run: |
|
||||||
|
git clone http://gitea:3000/sajad-dev/Backend.git .
|
||||||
|
|
||||||
|
- name: Install Python
|
||||||
|
run: |
|
||||||
|
apt-get install -y python3 python3-pip python3-venv git
|
||||||
|
|
||||||
|
- name: Setup Python pip mirrors
|
||||||
|
run: |
|
||||||
|
pip3 config --user set global.index-url https://package-mirror.liara.ir/repository/pypi/simple
|
||||||
|
pip3 config --user set global.extra-index-url https://mirror.cdn.ir/repository/pypi/simple
|
||||||
|
pip3 config --user set global.trusted-host "package-mirror.liara.ir mirror.cdn.ir mirror2.chabokan.net"
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: |
|
||||||
|
apt-get install -y \
|
||||||
|
python3 \
|
||||||
|
python3-pip \
|
||||||
|
python3-venv \
|
||||||
|
pkg-config \
|
||||||
|
build-essential \
|
||||||
|
default-libmysqlclient-dev
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python3 -m pip install --upgrade pip
|
||||||
|
pip3 install -r requirements.txt
|
||||||
|
pip3 install pytest flake8
|
||||||
|
|
||||||
|
- name: Run lint
|
||||||
|
run: |
|
||||||
|
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||||
|
|
||||||
|
|
||||||
|
- name: Setup SSH key
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
|
||||||
|
chmod 600 ~/.ssh/id_ed25519
|
||||||
|
ssh-keyscan -p ${{secrets.SERVER_SSH_PORT}} -H ${{ secrets.SERVER_HOST }} >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
- name: Deploy
|
||||||
|
run: |
|
||||||
|
ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} -p ${{secrets.SERVER_SSH_PORT}}<< 'EOF'
|
||||||
|
cd application/Backend
|
||||||
|
git pull origin production
|
||||||
|
docker-compose -f docker-compose-prod.yaml down
|
||||||
|
docker-compose -f docker-compose-prod.yaml up -d
|
||||||
|
EOF
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
name: Backend Service CI/CD
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'backend/**'
|
||||||
|
- 'backend/.github/workflows/backend.yml'
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'backend/**'
|
||||||
|
- 'backend/.github/workflows/backend.yml'
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: backend
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Lint & Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
python-version: ['3.11']
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Cache pip dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.cache/pip
|
||||||
|
key: ${{ runner.os }}-pip-backend-${{ hashFiles('backend/requirements.txt') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pip-backend-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install pytest flake8
|
||||||
|
|
||||||
|
- name: Run lint
|
||||||
|
run: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: pytest --tb=short -q
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
name: Deploy Backend Service
|
||||||
|
needs: test
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Deploy via SSH
|
||||||
|
uses: appleboy/ssh-action@v1
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_USER }}
|
||||||
|
port: ${{ secrets.SSH_PORT }}
|
||||||
|
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||||
|
script: |
|
||||||
|
cd /opt/myproject/backend
|
||||||
|
git pull origin main
|
||||||
|
sudo systemctl restart backend
|
||||||
+162
@@ -0,0 +1,162 @@
|
|||||||
|
## Backend Architecture – CropLogic Platform
|
||||||
|
|
||||||
|
پلتفرم **CropLogic** از سه سرویس کاملاً مستقل تشکیل شده است تا مقیاسپذیری، پایداری و امنیت دادهها در سطح بالایی حفظ شود. این سرویسها عبارتاند از:
|
||||||
|
|
||||||
|
- Backend Service
|
||||||
|
- AI Service
|
||||||
|
- SensorHub Service
|
||||||
|
|
||||||
|
جدا بودن این سرویسها باعث میشود هر بخش بتواند بهصورت مستقل توسعه داده شود، در صورت افزایش بار بهصورت جداگانه مقیاسپذیر باشد و در صورت بروز مشکل در یک سرویس، سایر سرویسها دچار اختلال نشوند.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Backend Service
|
||||||
|
|
||||||
|
سرویس **Backend** هستهی اصلی منطق سیستم محسوب میشود و نقش **Gateway** بین کلاینت و سایر سرویسهای داخلی را ایفا میکند.
|
||||||
|
|
||||||
|
### وظایف اصلی
|
||||||
|
- دریافت درخواستها از سمت کلاینت
|
||||||
|
- ارتباط با سرویسهای **AI** و **SensorHub**
|
||||||
|
- پردازش و مدیریت خروجی سرویسها
|
||||||
|
- آمادهسازی و ارسال پاسخ نهایی به کلاینت
|
||||||
|
|
||||||
|
به عبارت دیگر، Backend لایهی هماهنگکننده بین بخشهای مختلف سیستم است و تمام ارتباطات خارجی از طریق این سرویس انجام میشود.
|
||||||
|
|
||||||
|
### تکنولوژیها
|
||||||
|
- **Framework:** Django
|
||||||
|
- **Database:** MySQL
|
||||||
|
|
||||||
|
پروژه با استفاده از **Clean Architecture** طراحی شده است؛ بنابراین لایهی دیتابیس از منطق اصلی جدا بوده و در صورت نیاز امکان تغییر دیتابیس در آینده بهسادگی وجود دارد.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. AI Service
|
||||||
|
|
||||||
|
سرویس **AI** به دلیل حجم بالای محاسبات و احتمال ایجاد بار سنگین روی سیستم، بهصورت کاملاً جدا از Backend پیادهسازی شده است. این سرویس مسئول انجام تمام پردازشهای مرتبط با **هوش مصنوعی و تحلیل دادهها** است.
|
||||||
|
|
||||||
|
### بخشهای اصلی این سرویس
|
||||||
|
|
||||||
|
#### 2.1 سیستم RAG (Retrieval-Augmented Generation)
|
||||||
|
|
||||||
|
این سیستم با استفاده از **Embedding مدلهای LLM** پیادهسازی شده و نقش ساخت **دستیار هوشمند مزرعه** را بر عهده دارد.
|
||||||
|
|
||||||
|
از این دستیار برای:
|
||||||
|
- پاسخ به سوالات کشاورزان
|
||||||
|
- ارائه راهنماییهای تخصصی کشاورزی
|
||||||
|
- تحلیل شرایط مزرعه
|
||||||
|
|
||||||
|
استفاده میشود.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2.2 دریافت اطلاعات خاک بر اساس مختصات جغرافیایی
|
||||||
|
|
||||||
|
برای دریافت دادههای علمی مربوط به خاک از API زیر استفاده میشود:
|
||||||
|
|
||||||
|
`https://rest.isric.org/soilgrids`
|
||||||
|
|
||||||
|
این سرویس اطلاعات خاک را بر اساس مختصات جغرافیایی ارائه میدهد.
|
||||||
|
|
||||||
|
پارامترهای اصلی دریافت شده شامل موارد زیر است:
|
||||||
|
|
||||||
|
- depth_label
|
||||||
|
- bdod
|
||||||
|
- cec
|
||||||
|
- cfvo
|
||||||
|
- clay
|
||||||
|
- nitrogen
|
||||||
|
- ocd
|
||||||
|
- ocs
|
||||||
|
- phh2o
|
||||||
|
- sand
|
||||||
|
- silt
|
||||||
|
- soc
|
||||||
|
- wv0010
|
||||||
|
- wv0033
|
||||||
|
- wv1500
|
||||||
|
|
||||||
|
علاوه بر این، اطلاعات خاک برای عمقهای مختلف نیز دریافت میشود، از جمله:
|
||||||
|
|
||||||
|
- 0–5 سانتیمتر
|
||||||
|
- 5–10 سانتیمتر
|
||||||
|
- 10–15 سانتیمتر
|
||||||
|
|
||||||
|
این تفکیک عمق خاک اهمیت زیادی دارد، زیرا برخی گیاهان نسبت به ویژگیهای خاک در عمقهای خاص حساستر هستند.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2.3 ترکیب دادههای خاک با دادههای سنسور
|
||||||
|
|
||||||
|
اطلاعات دریافتی از SoilGrids با دادههای **سنسورهای مزرعه** ترکیب میشوند.
|
||||||
|
|
||||||
|
هدف از این مرحله:
|
||||||
|
- تکمیل دادههای ناقص سنسورها
|
||||||
|
- افزایش دقت مدلهای تحلیلی
|
||||||
|
- ایجاد یک دیتاست کامل از شرایط واقعی مزرعه
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2.4 محاسبات تکمیلی ژئوفیزیک و خاک
|
||||||
|
|
||||||
|
در این مرحله، دادههایی که نه از طریق سنسورها و نه از طریق APIهای خارجی قابل دریافت نیستند، با استفاده از **فرمولهای علمی ژئوفیزیک و خاکشناسی کشاورزی** محاسبه میشوند.
|
||||||
|
|
||||||
|
این کار باعث میشود مدلهای تحلیلی تصویر کاملتری از شرایط مزرعه داشته باشند.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2.5 تحلیل و مدلهای هوش مصنوعی
|
||||||
|
|
||||||
|
در مرحله نهایی، دادههای جمعآوریشده و پردازششده وارد سیستمهای تحلیلی میشوند که شامل موارد زیر است:
|
||||||
|
|
||||||
|
- استفاده از **سیستم RAG** برای تحلیل و تولید پاسخ
|
||||||
|
- استفاده از **Machine Learning** در سناریوهای تخصصیتر
|
||||||
|
- استفاده از **مدلهای از پیش آموزشدیده (Pretrained Models)**
|
||||||
|
|
||||||
|
خروجی این تحلیلها شامل مواردی مانند:
|
||||||
|
|
||||||
|
- توصیه زمان و میزان **آبیاری**
|
||||||
|
- توصیه **کوددهی**
|
||||||
|
- تحلیل وضعیت خاک و رشد گیاه
|
||||||
|
- پیشنهاد اقدامات بهینه برای مدیریت مزرعه
|
||||||
|
|
||||||
|
### دیتابیس
|
||||||
|
- **MySQL**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. SensorHub Service
|
||||||
|
|
||||||
|
سرویس **SensorHub** مسئول دریافت، ذخیره و مدیریت دادههای سنسورهای مزرعه است.
|
||||||
|
|
||||||
|
به دلیل حساسیت بالای این دادهها و اهمیت **عدم از دست رفتن اطلاعات سنسورها**، این سرویس بهصورت مستقل از سایر بخشها پیادهسازی شده است.
|
||||||
|
|
||||||
|
### ویژگیهای این سرویس
|
||||||
|
- دریافت دادههای سنسورها در زمان واقعی
|
||||||
|
- ذخیرهسازی امن و پایدار دادهها
|
||||||
|
- ارائه دادهها به سرویس Backend برای تحلیل
|
||||||
|
|
||||||
|
### دیتابیس
|
||||||
|
|
||||||
|
برای این سرویس از **Apache Cassandra** استفاده شده است.
|
||||||
|
|
||||||
|
دلایل انتخاب Cassandra:
|
||||||
|
|
||||||
|
- دیتابیس **Distributed** و مناسب برای دادههای حجیم
|
||||||
|
- **Fault Tolerance بالا**
|
||||||
|
- جلوگیری از از دست رفتن دادهها
|
||||||
|
- مناسب برای **Time-Series Data** مانند دادههای سنسورها
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## جمعبندی معماری
|
||||||
|
|
||||||
|
معماری CropLogic بر پایه **Microservice Architecture** طراحی شده است که مزایای زیر را فراهم میکند:
|
||||||
|
|
||||||
|
- مقیاسپذیری مستقل هر سرویس
|
||||||
|
- افزایش پایداری سیستم
|
||||||
|
- جداسازی مسئولیتها
|
||||||
|
- مدیریت بهتر بار پردازشی
|
||||||
|
- امنیت بیشتر برای دادههای حساس
|
||||||
|
|
||||||
|
این معماری باعث میشود پلتفرم بتواند در مقیاسهای بزرگ مزرعه و تعداد زیاد سنسورها نیز عملکرد پایدار و قابل اعتمادی داشته باشد.
|
||||||
|
:::
|
||||||
+162
@@ -0,0 +1,162 @@
|
|||||||
|
## Backend Architecture – CropLogic Platform
|
||||||
|
|
||||||
|
پلتفرم **CropLogic** از سه سرویس کاملاً مستقل تشکیل شده است تا مقیاسپذیری، پایداری و امنیت دادهها در سطح بالایی حفظ شود. این سرویسها عبارتاند از:
|
||||||
|
|
||||||
|
- Backend Service
|
||||||
|
- AI Service
|
||||||
|
- SensorHub Service
|
||||||
|
|
||||||
|
جدا بودن این سرویسها باعث میشود هر بخش بتواند بهصورت مستقل توسعه داده شود، در صورت افزایش بار بهصورت جداگانه مقیاسپذیر باشد و در صورت بروز مشکل در یک سرویس، سایر سرویسها دچار اختلال نشوند.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Backend Service
|
||||||
|
|
||||||
|
سرویس **Backend** هستهی اصلی منطق سیستم محسوب میشود و نقش **Gateway** بین کلاینت و سایر سرویسهای داخلی را ایفا میکند.
|
||||||
|
|
||||||
|
### وظایف اصلی
|
||||||
|
- دریافت درخواستها از سمت کلاینت
|
||||||
|
- ارتباط با سرویسهای **AI** و **SensorHub**
|
||||||
|
- پردازش و مدیریت خروجی سرویسها
|
||||||
|
- آمادهسازی و ارسال پاسخ نهایی به کلاینت
|
||||||
|
|
||||||
|
به عبارت دیگر، Backend لایهی هماهنگکننده بین بخشهای مختلف سیستم است و تمام ارتباطات خارجی از طریق این سرویس انجام میشود.
|
||||||
|
|
||||||
|
### تکنولوژیها
|
||||||
|
- **Framework:** Django
|
||||||
|
- **Database:** MySQL
|
||||||
|
|
||||||
|
پروژه با استفاده از **Clean Architecture** طراحی شده است؛ بنابراین لایهی دیتابیس از منطق اصلی جدا بوده و در صورت نیاز امکان تغییر دیتابیس در آینده بهسادگی وجود دارد.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. AI Service
|
||||||
|
|
||||||
|
سرویس **AI** به دلیل حجم بالای محاسبات و احتمال ایجاد بار سنگین روی سیستم، بهصورت کاملاً جدا از Backend پیادهسازی شده است. این سرویس مسئول انجام تمام پردازشهای مرتبط با **هوش مصنوعی و تحلیل دادهها** است.
|
||||||
|
|
||||||
|
### بخشهای اصلی این سرویس
|
||||||
|
|
||||||
|
#### 2.1 سیستم RAG (Retrieval-Augmented Generation)
|
||||||
|
|
||||||
|
این سیستم با استفاده از **Embedding مدلهای LLM** پیادهسازی شده و نقش ساخت **دستیار هوشمند مزرعه** را بر عهده دارد.
|
||||||
|
|
||||||
|
از این دستیار برای:
|
||||||
|
- پاسخ به سوالات کشاورزان
|
||||||
|
- ارائه راهنماییهای تخصصی کشاورزی
|
||||||
|
- تحلیل شرایط مزرعه
|
||||||
|
|
||||||
|
استفاده میشود.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2.2 دریافت اطلاعات خاک بر اساس مختصات جغرافیایی
|
||||||
|
|
||||||
|
برای دریافت دادههای علمی مربوط به خاک از API زیر استفاده میشود:
|
||||||
|
|
||||||
|
`https://rest.isric.org/soilgrids`
|
||||||
|
|
||||||
|
این سرویس اطلاعات خاک را بر اساس مختصات جغرافیایی ارائه میدهد.
|
||||||
|
|
||||||
|
پارامترهای اصلی دریافت شده شامل موارد زیر است:
|
||||||
|
|
||||||
|
- depth_label
|
||||||
|
- bdod
|
||||||
|
- cec
|
||||||
|
- cfvo
|
||||||
|
- clay
|
||||||
|
- nitrogen
|
||||||
|
- ocd
|
||||||
|
- ocs
|
||||||
|
- phh2o
|
||||||
|
- sand
|
||||||
|
- silt
|
||||||
|
- soc
|
||||||
|
- wv0010
|
||||||
|
- wv0033
|
||||||
|
- wv1500
|
||||||
|
|
||||||
|
علاوه بر این، اطلاعات خاک برای عمقهای مختلف نیز دریافت میشود، از جمله:
|
||||||
|
|
||||||
|
- 0–5 سانتیمتر
|
||||||
|
- 5–10 سانتیمتر
|
||||||
|
- 10–15 سانتیمتر
|
||||||
|
|
||||||
|
این تفکیک عمق خاک اهمیت زیادی دارد، زیرا برخی گیاهان نسبت به ویژگیهای خاک در عمقهای خاص حساستر هستند.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2.3 ترکیب دادههای خاک با دادههای سنسور
|
||||||
|
|
||||||
|
اطلاعات دریافتی از SoilGrids با دادههای **سنسورهای مزرعه** ترکیب میشوند.
|
||||||
|
|
||||||
|
هدف از این مرحله:
|
||||||
|
- تکمیل دادههای ناقص سنسورها
|
||||||
|
- افزایش دقت مدلهای تحلیلی
|
||||||
|
- ایجاد یک دیتاست کامل از شرایط واقعی مزرعه
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2.4 محاسبات تکمیلی ژئوفیزیک و خاک
|
||||||
|
|
||||||
|
در این مرحله، دادههایی که نه از طریق سنسورها و نه از طریق APIهای خارجی قابل دریافت نیستند، با استفاده از **فرمولهای علمی ژئوفیزیک و خاکشناسی کشاورزی** محاسبه میشوند.
|
||||||
|
|
||||||
|
این کار باعث میشود مدلهای تحلیلی تصویر کاملتری از شرایط مزرعه داشته باشند.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2.5 تحلیل و مدلهای هوش مصنوعی
|
||||||
|
|
||||||
|
در مرحله نهایی، دادههای جمعآوریشده و پردازششده وارد سیستمهای تحلیلی میشوند که شامل موارد زیر است:
|
||||||
|
|
||||||
|
- استفاده از **سیستم RAG** برای تحلیل و تولید پاسخ
|
||||||
|
- استفاده از **Machine Learning** در سناریوهای تخصصیتر
|
||||||
|
- استفاده از **مدلهای از پیش آموزشدیده (Pretrained Models)**
|
||||||
|
|
||||||
|
خروجی این تحلیلها شامل مواردی مانند:
|
||||||
|
|
||||||
|
- توصیه زمان و میزان **آبیاری**
|
||||||
|
- توصیه **کوددهی**
|
||||||
|
- تحلیل وضعیت خاک و رشد گیاه
|
||||||
|
- پیشنهاد اقدامات بهینه برای مدیریت مزرعه
|
||||||
|
|
||||||
|
### دیتابیس
|
||||||
|
- **MySQL**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. SensorHub Service
|
||||||
|
|
||||||
|
سرویس **SensorHub** مسئول دریافت، ذخیره و مدیریت دادههای سنسورهای مزرعه است.
|
||||||
|
|
||||||
|
به دلیل حساسیت بالای این دادهها و اهمیت **عدم از دست رفتن اطلاعات سنسورها**، این سرویس بهصورت مستقل از سایر بخشها پیادهسازی شده است.
|
||||||
|
|
||||||
|
### ویژگیهای این سرویس
|
||||||
|
- دریافت دادههای سنسورها در زمان واقعی
|
||||||
|
- ذخیرهسازی امن و پایدار دادهها
|
||||||
|
- ارائه دادهها به سرویس Backend برای تحلیل
|
||||||
|
|
||||||
|
### دیتابیس
|
||||||
|
|
||||||
|
برای این سرویس از **Apache Cassandra** استفاده شده است.
|
||||||
|
|
||||||
|
دلایل انتخاب Cassandra:
|
||||||
|
|
||||||
|
- دیتابیس **Distributed** و مناسب برای دادههای حجیم
|
||||||
|
- **Fault Tolerance بالا**
|
||||||
|
- جلوگیری از از دست رفتن دادهها
|
||||||
|
- مناسب برای **Time-Series Data** مانند دادههای سنسورها
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## جمعبندی معماری
|
||||||
|
|
||||||
|
معماری CropLogic بر پایه **Microservice Architecture** طراحی شده است که مزایای زیر را فراهم میکند:
|
||||||
|
|
||||||
|
- مقیاسپذیری مستقل هر سرویس
|
||||||
|
- افزایش پایداری سیستم
|
||||||
|
- جداسازی مسئولیتها
|
||||||
|
- مدیریت بهتر بار پردازشی
|
||||||
|
- امنیت بیشتر برای دادههای حساس
|
||||||
|
|
||||||
|
این معماری باعث میشود پلتفرم بتواند در مقیاسهای بزرگ مزرعه و تعداد زیاد سنسورها نیز عملکرد پایدار و قابل اعتمادی داشته باشد.
|
||||||
|
:::
|
||||||
+25
-2
@@ -1,10 +1,24 @@
|
|||||||
FROM python:3.12-slim
|
FROM docker.iranserver.com/python:3.10
|
||||||
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Debian/debian mirrors for apt
|
||||||
|
RUN rm -f /etc/apt/sources.list /etc/apt/sources.list.d/* && \
|
||||||
|
printf '%s\n' \
|
||||||
|
'deb https://mirror-linux.runflare.com/debian/ bookworm main contrib non-free non-free-firmware' \
|
||||||
|
'deb https://mirror-linux.runflare.com/debian/ bookworm-updates main contrib non-free non-free-firmware' \
|
||||||
|
'deb https://mirror-linux.runflare.com/debian-security/ bookworm-security main contrib non-free non-free-firmware' \
|
||||||
|
'' \
|
||||||
|
'deb [trusted=yes] https://mirror2.chabokan.net/debian bookworm main contrib non-free non-free-firmware' \
|
||||||
|
'deb [trusted=yes] https://mirror2.chabokan.net/debian-security bookworm-security main contrib non-free non-free-firmware' \
|
||||||
|
'' \
|
||||||
|
'deb http://mirror.iranserver.com/debian/ bookworm main contrib non-free non-free-firmware' \
|
||||||
|
'deb-src http://mirror.iranserver.com/debian/ bookworm main contrib non-free non-free-firmware' \
|
||||||
|
> /etc/apt/sources.list
|
||||||
|
|
||||||
# System deps for MySQL client (pkg-config required by mysqlclient to find libs)
|
# System deps for MySQL client (pkg-config required by mysqlclient to find libs)
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
default-libmysqlclient-dev \
|
default-libmysqlclient-dev \
|
||||||
@@ -13,7 +27,16 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
# Python mirrors
|
||||||
|
RUN pip config --user set global.index-url https://package-mirror.liara.ir/repository/pypi/simple && \
|
||||||
|
pip config --user set global.extra-index-url https://mirror.cdn.ir/repository/pypi/simple && \
|
||||||
|
pip config --user set global.extra-index-url https://mirror2.chabokan.net/pypi/simple && \
|
||||||
|
pip config --user set global.trusted-host package-mirror.liara.ir && \
|
||||||
|
pip config --user set global.trusted-host mirror.cdn.ir && \
|
||||||
|
pip config --user set global.trusted-host mirror-pypi.runflare.com
|
||||||
|
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AccountConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "account"
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
# Generated by Django 5.2.11 on 2026-03-18 14:09
|
||||||
|
|
||||||
|
import django.contrib.auth.models
|
||||||
|
import django.contrib.auth.validators
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("auth", "0012_alter_user_first_name_max_length"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="User",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("password", models.CharField(max_length=128, verbose_name="password")),
|
||||||
|
(
|
||||||
|
"last_login",
|
||||||
|
models.DateTimeField(
|
||||||
|
blank=True, null=True, verbose_name="last login"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"is_superuser",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Designates that this user has all permissions without explicitly assigning them.",
|
||||||
|
verbose_name="superuser status",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"username",
|
||||||
|
models.CharField(
|
||||||
|
error_messages={
|
||||||
|
"unique": "A user with that username already exists."
|
||||||
|
},
|
||||||
|
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
|
||||||
|
max_length=150,
|
||||||
|
unique=True,
|
||||||
|
validators=[
|
||||||
|
django.contrib.auth.validators.UnicodeUsernameValidator()
|
||||||
|
],
|
||||||
|
verbose_name="username",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"first_name",
|
||||||
|
models.CharField(
|
||||||
|
blank=True, max_length=150, verbose_name="first name"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"last_name",
|
||||||
|
models.CharField(
|
||||||
|
blank=True, max_length=150, verbose_name="last name"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"email",
|
||||||
|
models.EmailField(
|
||||||
|
blank=True, max_length=254, verbose_name="email address"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"is_staff",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Designates whether the user can log into this admin site.",
|
||||||
|
verbose_name="staff status",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"is_active",
|
||||||
|
models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
|
||||||
|
verbose_name="active",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"date_joined",
|
||||||
|
models.DateTimeField(
|
||||||
|
default=django.utils.timezone.now, verbose_name="date joined"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"phone_number",
|
||||||
|
models.CharField(db_index=True, max_length=32, unique=True),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"groups",
|
||||||
|
models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
||||||
|
related_name="user_set",
|
||||||
|
related_query_name="user",
|
||||||
|
to="auth.group",
|
||||||
|
verbose_name="groups",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user_permissions",
|
||||||
|
models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Specific permissions for this user.",
|
||||||
|
related_name="user_set",
|
||||||
|
related_query_name="user",
|
||||||
|
to="auth.permission",
|
||||||
|
verbose_name="user permissions",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"db_table": "users",
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
("objects", django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class User(AbstractUser):
|
||||||
|
phone_number = models.CharField(
|
||||||
|
max_length=32,
|
||||||
|
unique=True,
|
||||||
|
db_index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
USERNAME_FIELD = "phone_number"
|
||||||
|
REQUIRED_FIELDS = ["username"]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "users"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.phone_number
|
||||||
+16
-17
@@ -1,7 +1,6 @@
|
|||||||
"""
|
"""
|
||||||
Account API module.
|
Account API module.
|
||||||
CRUD endpoints for user account profile (first name, last name, phone numbers).
|
CRUD endpoints for user account profile.
|
||||||
Profile update endpoint returns UpdateProfileResponse (code, msg, data: AuthUser).
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@@ -16,21 +15,13 @@ def _auth_user_to_data(user):
|
|||||||
"""Build AuthUser-shaped dict from Django User."""
|
"""Build AuthUser-shaped dict from Django User."""
|
||||||
if user is None or not getattr(user, "pk", None):
|
if user is None or not getattr(user, "pk", None):
|
||||||
return None
|
return None
|
||||||
# return {
|
|
||||||
# "id": user.id,
|
|
||||||
# "username": getattr(user, "username", "") or "",
|
|
||||||
# "email": getattr(user, "email", "") or "",
|
|
||||||
# "first_name": getattr(user, "first_name", "") or "",
|
|
||||||
# "last_name": getattr(user, "last_name", "") or "",
|
|
||||||
# "phone_number": getattr(user, "phone_number", "") or "",
|
|
||||||
# }
|
|
||||||
return {
|
return {
|
||||||
"id": 1,
|
"id": user.id,
|
||||||
"username": "testuser",
|
"username": getattr(user, "username", "") or "",
|
||||||
"email": "testuser@example.com",
|
"email": getattr(user, "email", "") or "",
|
||||||
"first_name": "Test",
|
"first_name": getattr(user, "first_name", "") or "",
|
||||||
"last_name": "User",
|
"last_name": getattr(user, "last_name", "") or "",
|
||||||
"phone_number": "09123456789",
|
"phone_number": getattr(user, "phone_number", "") or "",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -46,8 +37,16 @@ class ProfileView(APIView):
|
|||||||
def patch(self, request):
|
def patch(self, request):
|
||||||
serializer = UpdateProfileSerializer(data=request.data, partial=True)
|
serializer = UpdateProfileSerializer(data=request.data, partial=True)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
# TODO: persist first_name, last_name, email via service layer
|
|
||||||
user = request.user
|
user = request.user
|
||||||
|
for field in ("first_name", "last_name", "email"):
|
||||||
|
if field in serializer.validated_data:
|
||||||
|
setattr(user, field, serializer.validated_data[field])
|
||||||
|
user.save(update_fields=[
|
||||||
|
f for f in ("first_name", "last_name", "email")
|
||||||
|
if f in serializer.validated_data
|
||||||
|
])
|
||||||
|
|
||||||
data = _auth_user_to_data(user)
|
data = _auth_user_to_data(user)
|
||||||
if data is None:
|
if data is None:
|
||||||
data = {
|
data = {
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import http.client
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def send_otp_sms(phone_number: str, otp_code: str) -> bool:
|
||||||
|
"""Send OTP code via SMS.ir bulk API.
|
||||||
|
|
||||||
|
Returns True on success, False on failure.
|
||||||
|
"""
|
||||||
|
api_key = getattr(settings, "SMS_IR_API_KEY", "")
|
||||||
|
line_number = getattr(settings, "SMS_IR_LINE_NUMBER", 300000000000)
|
||||||
|
|
||||||
|
if not api_key:
|
||||||
|
logger.error("SMS_IR_API_KEY is not configured.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
message_text = f"کد تایید شما: {otp_code}"
|
||||||
|
|
||||||
|
payload = json.dumps({
|
||||||
|
"lineNumber": line_number,
|
||||||
|
"messageText": message_text,
|
||||||
|
"mobiles": [phone_number],
|
||||||
|
"sendDateTime": None,
|
||||||
|
})
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"X-API-KEY": api_key,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = http.client.HTTPSConnection("api.sms.ir")
|
||||||
|
conn.request("POST", "/v1/send/bulk", payload, headers)
|
||||||
|
res = conn.getresponse()
|
||||||
|
data = res.read().decode("utf-8")
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
response = json.loads(data)
|
||||||
|
status_code = response.get("status")
|
||||||
|
|
||||||
|
if res.status == 200 and status_code == 1:
|
||||||
|
logger.info("SMS sent successfully to %s", phone_number)
|
||||||
|
return True
|
||||||
|
|
||||||
|
logger.warning(
|
||||||
|
"SMS.ir returned unexpected response: HTTP %s, body: %s",
|
||||||
|
res.status,
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to send SMS to %s", phone_number)
|
||||||
|
return False
|
||||||
+52
-33
@@ -2,12 +2,15 @@ import secrets
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.signing import TimestampSigner
|
from django.core.signing import BadSignature, SignatureExpired, TimestampSigner
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
|
||||||
|
from account.models import User
|
||||||
from .serializers import RequestOTPSerializer, VerifyOTPSerializer
|
from .serializers import RequestOTPSerializer, VerifyOTPSerializer
|
||||||
|
from .sms_service import send_otp_sms
|
||||||
|
|
||||||
|
|
||||||
OTP_TTL_SECONDS = 300
|
OTP_TTL_SECONDS = 300
|
||||||
@@ -15,24 +18,16 @@ OTP_SIGNER = TimestampSigner(salt="auth.otp")
|
|||||||
|
|
||||||
|
|
||||||
def _auth_user_to_data(user):
|
def _auth_user_to_data(user):
|
||||||
"""Build AuthUser-shaped dict from Django User (or mock)."""
|
"""Build AuthUser-shaped dict from Django User."""
|
||||||
# if user is None or not getattr(user, "pk", None):
|
if user is None or not getattr(user, "pk", None):
|
||||||
# return None
|
return None
|
||||||
# return {
|
|
||||||
# "id": user.id,
|
|
||||||
# "username": getattr(user, "username", "") or "",
|
|
||||||
# "email": getattr(user, "email", "") or "",
|
|
||||||
# "first_name": getattr(user, "first_name", "") or "",
|
|
||||||
# "last_name": getattr(user, "last_name", "") or "",
|
|
||||||
# "phone_number": getattr(user, "phone_number", "") or "",
|
|
||||||
# }
|
|
||||||
return {
|
return {
|
||||||
"id": 1,
|
"id": user.id,
|
||||||
"username": "testuser",
|
"username": getattr(user, "username", "") or "",
|
||||||
"email": "testuser@example.com",
|
"email": getattr(user, "email", "") or "",
|
||||||
"first_name": "Test",
|
"first_name": getattr(user, "first_name", "") or "",
|
||||||
"last_name": "User",
|
"last_name": getattr(user, "last_name", "") or "",
|
||||||
"phone_number": "09123456789",
|
"phone_number": getattr(user, "phone_number", "") or "",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -58,9 +53,13 @@ class AuthenticationView(APIView):
|
|||||||
cache.set(f"otp_code:{phone_number}", otp_code, timeout=OTP_TTL_SECONDS)
|
cache.set(f"otp_code:{phone_number}", otp_code, timeout=OTP_TTL_SECONDS)
|
||||||
otp_token = OTP_SIGNER.sign(phone_number)
|
otp_token = OTP_SIGNER.sign(phone_number)
|
||||||
|
|
||||||
|
sms_sent = send_otp_sms(phone_number, otp_code)
|
||||||
|
|
||||||
payload = {"code": 200, "msg": "success", "token": otp_token}
|
payload = {"code": 200, "msg": "success", "token": otp_token}
|
||||||
|
if not sms_sent:
|
||||||
|
payload["sms_warning"] = "SMS delivery failed; OTP stored server-side."
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
payload["debug_note"] = "OTP code is returned only when DEBUG=1."
|
payload["debug_otp"] = otp_code
|
||||||
|
|
||||||
return Response(payload, status=status.HTTP_200_OK)
|
return Response(payload, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@@ -68,26 +67,46 @@ class AuthenticationView(APIView):
|
|||||||
serializer = VerifyOTPSerializer(data=request.data)
|
serializer = VerifyOTPSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
# TODO: validate token + otp_code, load or create user, issue JWT/session token
|
token = serializer.validated_data["token"]
|
||||||
auth_token = "1234567890"
|
otp_code = serializer.validated_data["otp_code"].strip()
|
||||||
user_data = _auth_user_to_data(getattr(request, "user", None))
|
|
||||||
if user_data is None:
|
try:
|
||||||
user_data = {
|
phone_number = OTP_SIGNER.unsign(
|
||||||
"id": 0,
|
token, max_age=OTP_TTL_SECONDS
|
||||||
"username": "",
|
)
|
||||||
"email": "",
|
except (BadSignature, SignatureExpired):
|
||||||
"first_name": "",
|
return Response(
|
||||||
"last_name": "",
|
{"code": 400, "msg": "Token is invalid or expired."},
|
||||||
"phone_number": "",
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
}
|
)
|
||||||
|
|
||||||
|
cached_otp = cache.get(f"otp_code:{phone_number}")
|
||||||
|
if cached_otp is None or cached_otp != otp_code:
|
||||||
|
return Response(
|
||||||
|
{"code": 400, "msg": "OTP code is invalid or expired."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
cache.delete(f"otp_code:{phone_number}")
|
||||||
|
|
||||||
|
user, created = User.objects.get_or_create(
|
||||||
|
phone_number=phone_number,
|
||||||
|
defaults={"username": phone_number},
|
||||||
|
)
|
||||||
|
|
||||||
|
refresh = RefreshToken.for_user(user)
|
||||||
|
|
||||||
|
user_data = _auth_user_to_data(user)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"code": 200,
|
"code": 200,
|
||||||
"msg": "success",
|
"msg": "success",
|
||||||
"data": user_data,
|
"data": user_data,
|
||||||
"token": auth_token,
|
"token": {
|
||||||
|
"access": str(refresh.access_token),
|
||||||
|
"refresh": str(refresh),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -11,6 +11,8 @@ SECRET_KEY = os.environ.get("SECRET_KEY", "django-insecure-dev-only")
|
|||||||
DEBUG = os.environ.get("DEBUG", "0") == "1"
|
DEBUG = os.environ.get("DEBUG", "0") == "1"
|
||||||
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")
|
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")
|
||||||
|
|
||||||
|
AUTH_USER_MODEL = "account.User"
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
"django.contrib.admin",
|
"django.contrib.admin",
|
||||||
"django.contrib.auth",
|
"django.contrib.auth",
|
||||||
@@ -19,7 +21,7 @@ INSTALLED_APPS = [
|
|||||||
"django.contrib.messages",
|
"django.contrib.messages",
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
"auth.apps.AuthConfig",
|
"auth.apps.AuthConfig",
|
||||||
"account",
|
"account.apps.AccountConfig",
|
||||||
"sensor_hub",
|
"sensor_hub",
|
||||||
"dashboard",
|
"dashboard",
|
||||||
"crop_zoning",
|
"crop_zoning",
|
||||||
@@ -109,4 +111,8 @@ REST_FRAMEWORK = {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SMS_IR_API_KEY = os.environ.get("SMS_IR_API_KEY", "")
|
||||||
|
SMS_IR_LINE_NUMBER = int(os.environ.get("SMS_IR_LINE_NUMBER", "300000000000"))
|
||||||
|
|
||||||
CORS_ALLOW_ALL_ORIGINS = DEBUG
|
CORS_ALLOW_ALL_ORIGINS = DEBUG
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Production: no source mount; image contains code
|
# Production: no source mount; image contains code
|
||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
image: mysql:8.0
|
image: docker.iranserver.com/mysql:8
|
||||||
environment:
|
environment:
|
||||||
MYSQL_DATABASE: ${DB_NAME}
|
MYSQL_DATABASE: ${DB_NAME}
|
||||||
MYSQL_USER: ${DB_USER}
|
MYSQL_USER: ${DB_USER}
|
||||||
@@ -16,19 +16,6 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
phpmyadmin:
|
|
||||||
image: phpmyadmin:latest
|
|
||||||
environment:
|
|
||||||
PMA_HOST: db
|
|
||||||
PMA_PORT: 3306
|
|
||||||
UPLOAD_LIMIT: 64M
|
|
||||||
ports:
|
|
||||||
- "8081:80"
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
web:
|
web:
|
||||||
build: .
|
build: .
|
||||||
env_file:
|
env_file:
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
# Development: volumes mount source so code updates apply without rebuild
|
# Development: volumes mount source so code updates apply without rebuild
|
||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
image: mysql:8.0
|
image: docker-mirror.liara.ir/mysql:8.0
|
||||||
environment:
|
environment:
|
||||||
MYSQL_DATABASE: ${DB_NAME:-croplogic}
|
MYSQL_DATABASE: ${DB_NAME:-croplogic}
|
||||||
MYSQL_USER: ${DB_USER:-croplogic}
|
MYSQL_USER: ${DB_USER:-croplogic}
|
||||||
@@ -16,7 +16,7 @@ services:
|
|||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
phpmyadmin:
|
phpmyadmin:
|
||||||
image: phpmyadmin:latest
|
image: docker-mirror.liara.ir/phpmyadmin:latest
|
||||||
environment:
|
environment:
|
||||||
PMA_HOST: db
|
PMA_HOST: db
|
||||||
PMA_PORT: 3306
|
PMA_PORT: 3306
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
+14
-7
@@ -1,7 +1,14 @@
|
|||||||
Django>=5.0,<6
|
Django>=5.0,<5.2
|
||||||
djangorestframework>=3.14,<4
|
djangorestframework>=3.14,<3.16
|
||||||
djangorestframework-simplejwt>=5.3,<6
|
djangorestframework-simplejwt>=5.3,<5.4
|
||||||
django-cors-headers>=4.3,<5
|
django-cors-headers>=4.3,<4.5
|
||||||
mysqlclient>=2.2,<3
|
drf-spectacular>=0.27,<0.28
|
||||||
gunicorn>=22,<25
|
drf-spectacular-sidecar>=2024.7.1,<2025
|
||||||
python-dotenv>=1.0,<2
|
celery[redis]>=5.3,<5.4
|
||||||
|
redis>=5.0,<5.1
|
||||||
|
|
||||||
|
mysqlclient>=2.2,<2.3
|
||||||
|
gunicorn>=22,<23
|
||||||
|
python-dotenv>=1.0,<1.1
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user