devicetypeAPIGET1

This commit is contained in:
QuangMinh_123
2026-05-21 12:01:10 +07:00
parent 86383e7c03
commit 7aebcf9567
35 changed files with 2784 additions and 145 deletions

View File

View File

View File

View File

9
backend/.env.example Normal file
View File

@@ -0,0 +1,9 @@
#Account Database configuration for PostgreSQL
POSTGRES_USER=your_user_here
POSTGRES_PASSWORD= your_strong_password_here
POSTGRES_DB=your_database_here
#Account Minio S3 configuration
MINIO_ROOT_USER=your_root_user_here
MINIO_ROOT_PASSWORD=your_strong_password_here
MINIO_BUCKET_NAME=your_bucket_name_here

View File

@@ -1,26 +1,30 @@
from flask import Flask, jsonify
# from flask_cors import CORS
import os
from dotenv import load_dotenv
from flask import Flask
load_dotenv() # Đọc file .env
from common.exceptions.handler import (
register_error_handlers
)
app = Flask(__name__) # ← Đây là dòng quan trọng nhất
# Cho phép React (localhost:5173) gọi API từ backend
CORS(app, origins=["http://localhost:5173", "http://127.0.0.1:5173"])
from modules.device_type.routes import (
device_type_bp
)
@app.route('/')
def home():
return jsonify({
"message": "✅ Backend NDMS đang chạy thành công!",
"status": "ok"
})
app = Flask(__name__)
@app.route('/health')
def health():
return jsonify({"status": "healthy"})
# Register Global Exception Handlers
register_error_handlers(app)
# Phần này bạn hay dùng
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000, debug=True)
# Register Blueprints
app.register_blueprint(
device_type_bp,
url_prefix="/api"
)
# @app.route("/")
# def home():
# return {
# "message": "NDMS Backend Running"
# }
if __name__ == "__main__":
app.run(debug=True)

View File

@@ -0,0 +1,9 @@
HTTP_OK = 200
HTTP_CREATED = 201
HTTP_BAD_REQUEST = 400
HTTP_UNAUTHORIZED = 401
HTTP_FORBIDDEN = 403
HTTP_NOT_FOUND = 404
HTTP_CONFLICT = 409
HTTP_UNPROCESSABLE_ENTITY = 422
HTTP_INTERNAL_SERVER_ERROR = 500

View File

@@ -0,0 +1,62 @@
# Đây là exception chung cho toàn bộ dự án
class AppException(Exception): # Tạo ra class AppException kế thừa lại Exception của thư viện
def __init__(
self,
message,
status_code=400,
error_code="BAD_REQUEST",
payload=None
):
super().__init__(message) #kế thừa thêm thuộc tính
self.message = message
self.status_code = status_code
self.error_code = error_code
self.payload = payload or {}
def to_dict(self):
return {
"success": False,
"error": self.error_code,
"message": self.message,
"details": self.payload
}
# Rồi từ thằng kế thừa lại kế thừa tiếp từ thằng AppException
class BadRequestException(AppException):
def __init__(self, message="Bad request", payload=None):
super().__init__(
message=message,
status_code=400,
error_code="BAD_REQUEST",
payload=payload
)
class NotFoundException(AppException):
def __init__(self, message="Resource not found", payload=None):
super().__init__(
message=message,
status_code=404,
error_code="NOT_FOUND",
payload=payload
)
class ConflictException(AppException):
def __init__(self, message="Resource already exists", payload=None):
super().__init__(
message=message,
status_code=409,
error_code="CONFLICT",
payload=payload
)
class UnprocessableEntityException(AppException):
def __init__(self, message="Unprocessable entity", payload=None):
super().__init__(
message=message,
status_code=422,
error_code="UNPROCESSABLE_ENTITY",
payload=payload
)

View File

@@ -0,0 +1,50 @@
from marshmallow import ValidationError
from common.exceptions.app_exception import AppException
from common.response.api_response import error_response
from common.constants.status_code import (
HTTP_BAD_REQUEST,
HTTP_NOT_FOUND,
HTTP_INTERNAL_SERVER_ERROR
)
# Phải tạo global error handler(ở project cũ là để phần này ra app.py nhưng bây giờ chỉ cần tạo file hander rồi đăng kí ở app.py để nó thành global bắt lỗi toàn dự án)
# Ngoài ra để có được các lỗi của toàn dự án, thì phải custom exception theo dự án bằng cách kế thừa lại Exception để custom thêm thuộc tính mới
def register_error_handlers(app): # Đăng ký hàm này ở app.py để nó bắt lỗi toàn dự án
@app.errorhandler(AppException)
def handle_app_exception(error):
return error_response(
message=error.message,
error=error.error_code,
status_code=error.status_code,
details=getattr(error, "payload", None)
)
@app.errorhandler(ValidationError)
def handle_validation_error(error):
return error_response(
message="Invalid request data",
error="VALIDATION_ERROR",
status_code=HTTP_BAD_REQUEST,
details=error.messages
)
@app.errorhandler(404)
def handle_not_found(error):
return error_response(
message="Route not found",
error="NOT_FOUND",
status_code=HTTP_NOT_FOUND
)
@app.errorhandler(500)
def handle_internal_error(error):
return error_response(
message="Internal server error",
error="INTERNAL_SERVER_ERROR",
status_code=HTTP_INTERNAL_SERVER_ERROR
)

View File

@@ -0,0 +1,28 @@
from flask import jsonify
from common.constants.status_code import HTTP_OK, HTTP_BAD_REQUEST
def success_response(
data=None,
message="Success",
status_code=HTTP_OK
):
return jsonify({
"success": True,
"message": message,
"data": data
}), status_code
def error_response(
message="Error",
error="ERROR",
status_code=HTTP_BAD_REQUEST,
details=None
):
return jsonify({
"success": False,
"error": error,
"message": message,
"details": details
}), status_code

View File

@@ -0,0 +1,25 @@
#Connection to Database
from psycopg2.pool import SimpleConnectionPool
import os
from dotenv import load_dotenv
load_dotenv()
db_pool = SimpleConnectionPool(
minconn=1,
maxconn=10,
host=os.getenv("DB_HOST"),
port=os.getenv("DB_PORT"),
database=os.getenv("POSTGRES_DB"),
user=os.getenv("POSTGRES_USER"),
password=os.getenv("POSTGRES_PASSWORD")
)
# Tạo ra 10 conection khác nhau, mỗi lần dùng thì vào đây lấy 1 connection ra => Nó tạo sẵn một nhóm connection. Khi cần thì lấy ra, dùng xong trả lại.
# khác với kiểu cũ là mỗi lần request khác nhau lại phải tạo mới connection nó sẽ bị nặng và lâu hơn
def get_connection():
return db_pool.getconn() # Lấy ra connection trong này
def release_connection(conn):
db_pool.putconn(conn) # trả connection lại pool này

View File

View File

@@ -0,0 +1 @@

View File

View File

@@ -0,0 +1,83 @@
# ============================================
# CONTROLLER: Nhận request → Validate → Gọi Service → Trả response
# ============================================
# Controller KHÔNG chứa business logic (check trùng tên, tính toán...)
# Controller CHỈ làm 3 việc:
# 1. Lấy dữ liệu từ request (body, params, query string)
# 2. Validate dữ liệu (bằng Marshmallow schema)
# 3. Gọi service → trả response
# ============================================
from flask import request
from common.response.api_response import success_response
from common.constants.status_code import HTTP_CREATED
from modules.device_type.service import (
get_device_types_service,
create_device_type_service
)
from modules.device_type.schemas import CreateDeviceTypeSchema
def get_device_types():
"""GET /api/device-types — Lấy danh sách tất cả"""
device_types = get_device_types_service()
return success_response(
data=device_types,
message="Device types retrieved successfully"
)
def get_device_type_by_id(device_type_id):
"""GET /api/device-types/<device_type_id> — Lấy chi tiết theo ID"""
# TODO: Triển khai sau
pass
def create_device_type():
"""
POST /api/device-types — Tạo mới device type
Luồng xử lý:
1. Lấy JSON body từ request
2. Validate bằng Marshmallow schema
- Nếu sai → Marshmallow tự throw ValidationError
- Global handler bắt → trả 400 kèm chi tiết lỗi
3. Gọi service (service sẽ check trùng tên + insert DB)
4. Trả 201 Created + data vừa tạo
"""
# Bước 1: Lấy JSON body từ request
body = request.get_json()
# request.get_json() parse body thành dict Python
# VD: {"name": "Router", "color": "#3B82F6"} → {'name': 'Router', 'color': '#3B82F6'}
# Bước 2: Validate bằng schema
schema = CreateDeviceTypeSchema()
data = schema.load(body)
# schema.load() làm 2 việc:
# a) Validate: check required, type, regex... → nếu sai throw ValidationError
# b) Deserialize: loại bỏ field thừa, áp dụng load_default
# VD: body = {"name": "Router", "color": "#3B82F6", "hack": "xss"}
# → data = {"name": "Router", "color": "#3B82F6", "sort_order": 0, "is_active": True}
# → "hack" bị loại bỏ, sort_order/is_active được thêm default
# Bước 3: Gọi service
new_device_type = create_device_type_service(data)
# Bước 4: Trả response 201 Created
return success_response(
data=new_device_type,
message="Device type created successfully",
status_code=HTTP_CREATED # 201 thay vì 200 — đúng chuẩn REST cho POST thành công
)
def update_device_type(device_type_id):
"""PUT /api/device-types/<device_type_id> — Cập nhật"""
# TODO: Triển khai sau
pass
def delete_device_type(device_type_id):
"""DELETE /api/device-types/<device_type_id> — Xóa"""
# TODO: Triển khai sau
pass

View File

@@ -0,0 +1,30 @@
# Đây là exception cho riêng cho từng module
from common.exceptions.app_exception import (
NotFoundException,
ConflictException,
BadRequestException
)
class DeviceTypeNotFoundException(NotFoundException):
def __init__(self, device_type_id):
super().__init__(
message=f"Device type not found with id={device_type_id}",
payload={"device_type_id": device_type_id}
)
class DeviceTypeAlreadyExistsException(ConflictException):
def __init__(self, name):
super().__init__(
message=f"Device type already exists with name={name}",
payload={"name": name}
)
class DeviceTypeInUseException(BadRequestException):
def __init__(self, device_type_id):
super().__init__(
message="Cannot delete device type because it is being used by devices",
payload={"device_type_id": device_type_id}
)

View File

@@ -0,0 +1,120 @@
from config.database import get_connection, release_connection
# ============================================
# Hàm chung: Chuyển 1 row (tuple) thành dict
# Tại sao tách riêng? Vì nhiều hàm đều cần chuyển row → dict,
# viết 1 lần dùng lại, tránh copy-paste
# ============================================
def _row_to_dict(row):
return {
"id": str(row[0]),
"name": row[1],
"description": row[2],
"icon_url": row[3],
"color": row[4],
"sort_order": row[5],
"is_active": row[6],
"created": row[7].isoformat() if row[7] else None,
"modified": row[8].isoformat() if row[8] else None
}
# ============================================
# GET ALL: Lấy danh sách tất cả device types
# ============================================
def find_all_device_types():
conn = get_connection()
cur = None
try:
cur = conn.cursor()
cur.execute("""
SELECT id, name, description, icon_url, color, sort_order, is_active, created, modified
FROM device_types
ORDER BY sort_order ASC, name ASC
""")
rows = cur.fetchall()
device_types = []
for row in rows:
device_types.append(_row_to_dict(row))
# ✅ FIX: return PHẢI nằm NGOÀI vòng for
# Trước đó return nằm trong for → chỉ trả về 1 row rồi dừng
return device_types
finally:
if cur:
cur.close()
# ✅ FIX: PHẢI trả connection về pool, không thì sau 10 request app sẽ đứng
release_connection(conn)
# ============================================
# FIND BY NAME: Tìm device type theo tên (để check trùng tên khi tạo mới)
# Trả về dict nếu tìm thấy, None nếu không
# ============================================
def find_device_type_by_name(name):
conn = get_connection()
cur = None
try:
cur = conn.cursor()
# Dùng LOWER() để so sánh không phân biệt hoa/thường
# VD: "Router" và "router" coi như trùng
cur.execute("""
SELECT id, name, description, icon_url, color, sort_order, is_active, created, modified
FROM device_types
WHERE LOWER(name) = LOWER(%s)
""", (name,))
# ⚠️ LƯU Ý: (name,) có dấu phẩy → tạo tuple 1 phần tử
# Nếu viết (name) thì Python hiểu là string, không phải tuple → lỗi
row = cur.fetchone() # fetchone() vì chỉ cần 1 kết quả
if row:
return _row_to_dict(row)
return None
finally:
if cur:
cur.close()
release_connection(conn)
# ============================================
# INSERT: Thêm mới 1 device type vào database
# Trả về dict của device type vừa tạo (có id, created, modified từ DB)
# ============================================
def insert_device_type(data):
conn = get_connection()
cur = None
try:
cur = conn.cursor()
cur.execute("""
INSERT INTO device_types (name, description, icon_url, color, sort_order, is_active)
VALUES (%s, %s, %s, %s, %s, %s)
RETURNING id, name, description, icon_url, color, sort_order, is_active, created, modified
""", (
data["name"],
data.get("description"), # .get() trả về None nếu không có key
data.get("icon_url"),
data["color"],
data.get("sort_order", 0), # default = 0 nếu không truyền
data.get("is_active", True) # default = True nếu không truyền
))
# RETURNING: PostgreSQL trả về row vừa INSERT (bao gồm id, created, modified do DB tự tạo)
# → Không cần SELECT lại sau khi INSERT
row = cur.fetchone()
conn.commit() # ⚠️ QUAN TRỌNG: INSERT/UPDATE/DELETE phải commit() để lưu vào DB
# Nếu không commit → dữ liệu chỉ nằm trong transaction, khi connection đóng → mất hết
return _row_to_dict(row)
except Exception:
conn.rollback() # Nếu có lỗi → rollback để hủy transaction, tránh dữ liệu bẩn
raise # raise lại exception để tầng trên (service/controller) xử lý
finally:
if cur:
cur.close()
release_connection(conn)

View File

@@ -0,0 +1,21 @@
from flask import Blueprint
from modules.device_type.controller import (
get_device_types,
get_device_type_by_id,
create_device_type,
update_device_type,
delete_device_type
)
device_type_bp = Blueprint(
"device_type",
__name__,
url_prefix="/device-types"
)
device_type_bp.route("", methods=["GET"])(get_device_types)
device_type_bp.route("/<device_type_id>", methods=["GET"])(get_device_type_by_id)
device_type_bp.route("", methods=["POST"])(create_device_type)
device_type_bp.route("/<device_type_id>", methods=["PUT"])(update_device_type)
device_type_bp.route("/<device_type_id>", methods=["DELETE"])(delete_device_type)

View File

@@ -0,0 +1,67 @@
from marshmallow import Schema, fields, validate
HEX_COLOR_REGEX = r"^#[0-9A-Fa-f]{6}$"
class CreateDeviceTypeSchema(Schema):
name = fields.String(
required=True,
validate=validate.Length(min=1, max=100)
)
description = fields.String(
required=False,
allow_none=True
)
icon_url = fields.String(
required=False,
allow_none=True,
validate=validate.Length(max=512)
)
color = fields.String(
required=True,
validate=validate.Regexp(HEX_COLOR_REGEX)
)
sort_order = fields.Integer(
required=False,
load_default=0
)
is_active = fields.Boolean(
required=False,
load_default=True
)
class UpdateDeviceTypeSchema(Schema):
name = fields.String(
required=False,
validate=validate.Length(min=1, max=100)
)
description = fields.String(
required=False,
allow_none=True
)
icon_url = fields.String(
required=False,
allow_none=True,
validate=validate.Length(max=512)
)
color = fields.String(
required=False,
validate=validate.Regexp(HEX_COLOR_REGEX)
)
sort_order = fields.Integer(
required=False
)
is_active = fields.Boolean(
required=False
)

View File

@@ -0,0 +1,38 @@
# ============================================
# SERVICE LAYER: Chứa Business Logic
# ============================================
# Controller chỉ nhận request + trả response
# Repository chỉ truy vấn DB
# Service ở giữa: chứa LOGIC NGHIỆP VỤ (validate, check điều kiện, tính toán)
# ============================================
from modules.device_type.repository import (
find_all_device_types,
find_device_type_by_name,
insert_device_type
)
from modules.device_type.exceptions import DeviceTypeAlreadyExistsException
def get_device_types_service():
"""Lấy danh sách tất cả device types — hiện tại không có logic gì thêm"""
return find_all_device_types()
def create_device_type_service(data):
"""
Tạo mới device type.
Business logic:
1. Check xem tên đã tồn tại chưa (UNIQUE constraint)
2. Nếu trùng → throw exception
3. Nếu không trùng → insert vào DB
"""
# Bước 1: Check trùng tên
existing = find_device_type_by_name(data["name"])
if existing:
# Throw exception → global handler bắt → trả 409 Conflict
raise DeviceTypeAlreadyExistsException(data["name"])
# Bước 2: Insert vào DB
new_device_type = insert_device_type(data)
return new_device_type

View File

View File

@@ -10,3 +10,5 @@ python-dotenv==1.2.2
SQLAlchemy==2.0.49
typing_extensions==4.15.0
Werkzeug==3.1.8
marshmallow==3.21.2
APScheduler==3.10.4