- 后端新增 POST /api/products/{id}/specifications 接口
- ProductService 新增 add_specification 方法(含校验和审计日志)
- 前端展开行"+ 添加规格"按钮直接调用新接口
- 编辑/新增规格共用同一个弹窗,自动区分模式
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
563 lines
26 KiB
Python
563 lines
26 KiB
Python
"""产品管理服务层。
|
|
|
|
负责产品分类和产品主档(含规格明细)的增删改查业务逻辑。
|
|
依赖 ProductRepository 进行数据持久化,依赖 audit_service 记录操作审计日志。
|
|
产品采用"产品名称 + 规格"的二级结构,同一产品名称下可有多条规格记录。
|
|
"""
|
|
|
|
from sqlalchemy.exc import SQLAlchemyError
|
|
from sqlalchemy.orm import Session
|
|
|
|
from backend.app.core.cache import cache_delete_pattern, cache_get, cache_set, make_cache_key
|
|
from backend.app.core.error_codes import ErrorCode
|
|
from backend.app.core.exceptions import AppException
|
|
from backend.app.repositories.product_repository import ProductRepository
|
|
from backend.app.services.audit_service import audit_service
|
|
|
|
|
|
class ProductService:
|
|
"""产品服务,封装产品分类和产品规格的 CRUD 业务逻辑。
|
|
|
|
依赖:
|
|
ProductRepository - 产品数据访问
|
|
audit_service - 操作审计日志
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
self.repository = ProductRepository()
|
|
|
|
def list_categories(self, session: Session | None = None, filters: dict | None = None) -> dict:
|
|
"""查询产品分类列表。
|
|
|
|
Args:
|
|
session: 数据库会话
|
|
filters: 查询过滤条件
|
|
|
|
Returns:
|
|
包含 total、page_no、page_size、list 的分页字典
|
|
|
|
被调用路由: products.py - GET /products/categories
|
|
"""
|
|
if session is None:
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="数据库会话未初始化", status_code=500)
|
|
try:
|
|
categories = self.repository.list_categories(session, filters or {})
|
|
return {
|
|
"total": len(categories),
|
|
"page_no": 1,
|
|
"page_size": len(categories) or 20,
|
|
"list": [
|
|
{
|
|
"category_id": category.id,
|
|
"category_name": category.category_name,
|
|
"category_code": category.category_code,
|
|
"sort_no": category.sort_no,
|
|
"status": category.status,
|
|
"remark": category.remark,
|
|
}
|
|
for category in categories
|
|
],
|
|
}
|
|
except SQLAlchemyError as exc:
|
|
raise AppException(code=ErrorCode.SYSTEM_ERROR, message="数据库操作失败", status_code=500) from exc
|
|
|
|
def create_category(self, payload: dict, session: Session | None = None) -> dict:
|
|
"""创建产品分类。
|
|
|
|
校验分类名称和编码非空,并检查编码唯一性。自动记录审计日志。
|
|
|
|
Args:
|
|
payload: 包含 category_name、category_code 的请求体
|
|
session: 数据库会话
|
|
|
|
Returns:
|
|
包含 category_id、category_name、category_code、status 的结果字典
|
|
|
|
被调用路由: products.py - POST /products/categories
|
|
"""
|
|
if session is None:
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="数据库会话未初始化", status_code=500)
|
|
try:
|
|
if not payload["category_name"].strip():
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="分类名称不能为空", status_code=400)
|
|
if not payload["category_code"].strip():
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="分类编码不能为空", status_code=400)
|
|
existed = self.repository.get_category_by_code(session, payload["category_code"])
|
|
if existed is not None:
|
|
raise AppException(code=ErrorCode.DUPLICATE, message="分类编码已存在", status_code=400)
|
|
category = self.repository.create_category(session, payload)
|
|
audit_service.write_log(
|
|
session,
|
|
{
|
|
"operate_type": "product_category_create",
|
|
"biz_type": "product_category",
|
|
"biz_id": category.id,
|
|
"before_value": None,
|
|
"after_value": {"category_name": category.category_name, "category_code": category.category_code},
|
|
"remark": f"新增产品分类 {category.category_name}",
|
|
},
|
|
)
|
|
session.commit()
|
|
cache_delete_pattern("product:*")
|
|
return {
|
|
"category_id": category.id,
|
|
"category_name": category.category_name,
|
|
"category_code": category.category_code,
|
|
"status": category.status,
|
|
}
|
|
except AppException:
|
|
session.rollback()
|
|
raise
|
|
except SQLAlchemyError as exc:
|
|
session.rollback()
|
|
raise AppException(code=ErrorCode.SYSTEM_ERROR, message="数据库操作失败", status_code=500) from exc
|
|
|
|
def update_category(self, category_id: int, payload: dict, session: Session | None = None) -> dict:
|
|
"""更新产品分类。
|
|
|
|
校验分类编码唯一性(排除自身)。
|
|
|
|
Args:
|
|
category_id: 分类 ID
|
|
payload: 包含更新字段的请求体
|
|
session: 数据库会话
|
|
|
|
Returns:
|
|
更新结果字典
|
|
|
|
被调用路由: products.py - PUT /products/categories/{category_id}
|
|
"""
|
|
if session is None:
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="数据库会话未初始化", status_code=500)
|
|
try:
|
|
if not payload["category_name"].strip():
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="分类名称不能为空", status_code=400)
|
|
if not payload["category_code"].strip():
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="分类编码不能为空", status_code=400)
|
|
category = self.repository.get_category(session, category_id)
|
|
if category is None:
|
|
raise AppException(code=ErrorCode.NOT_FOUND, message="产品分类不存在", status_code=404)
|
|
existed = self.repository.get_category_by_code(session, payload["category_code"])
|
|
if existed is not None and existed.id != category_id:
|
|
raise AppException(code=ErrorCode.DUPLICATE, message="分类编码已存在", status_code=400)
|
|
self.repository.update_category(session, category, payload)
|
|
session.commit()
|
|
cache_delete_pattern("product:*")
|
|
return {"category_id": category_id, "updated": True}
|
|
except AppException:
|
|
session.rollback()
|
|
raise
|
|
except SQLAlchemyError as exc:
|
|
session.rollback()
|
|
raise AppException(code=ErrorCode.SYSTEM_ERROR, message="数据库操作失败", status_code=500) from exc
|
|
|
|
def _group_products(self, products: list, categories: dict[int, object]) -> list[dict]:
|
|
"""将产品规格列表按产品名称分组。
|
|
|
|
同一产品名称下的多条规格记录合并为一条,规格列表放在 specifications 字段中。
|
|
自动将第一条规格标记为默认(若无显式默认)。
|
|
|
|
Args:
|
|
products: 产品 ORM 对象列表
|
|
categories: 分类 ID 到分类 ORM 对象的映射
|
|
|
|
Returns:
|
|
按产品名称分组后的产品列表
|
|
"""
|
|
grouped: dict[str, dict] = {}
|
|
for product in products:
|
|
key = product.product_name
|
|
category_name = categories[product.category_id].category_name if product.category_id in categories else (product.category or "")
|
|
entry = grouped.setdefault(
|
|
key,
|
|
{
|
|
"product_name": product.product_name,
|
|
"product_id": product.id,
|
|
"category_id": product.category_id,
|
|
"category_name": category_name,
|
|
"status": product.status,
|
|
"remark": product.remark,
|
|
"specifications": [],
|
|
},
|
|
)
|
|
entry["specifications"].append(
|
|
{
|
|
"product_id": product.id,
|
|
"specification": product.specification,
|
|
"unit": product.unit,
|
|
"cost_price": float(product.cost_price or 0),
|
|
"sale_price": float(product.sale_price or 0),
|
|
"status": product.status,
|
|
"remark": product.remark,
|
|
"is_default": bool(getattr(product, "is_default", 0)),
|
|
}
|
|
)
|
|
for entry in grouped.values():
|
|
if entry["specifications"] and not any(spec["is_default"] for spec in entry["specifications"]):
|
|
entry["specifications"][0]["is_default"] = True
|
|
return list(grouped.values())
|
|
|
|
def list_products(self, filters: dict | None = None, session: Session | None = None) -> dict:
|
|
"""查询产品列表(按名称分组)。
|
|
|
|
先查询所有产品规格,再按产品名称分组聚合。
|
|
|
|
Args:
|
|
filters: 查询过滤条件
|
|
session: 数据库会话
|
|
|
|
Returns:
|
|
包含 total、page_no、page_size、list 的分页字典
|
|
|
|
被调用路由: products.py - GET /products
|
|
"""
|
|
if session is None:
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="数据库会话未初始化", status_code=500)
|
|
cache_key = make_cache_key("product:list", **(filters or {}))
|
|
cached = cache_get(cache_key)
|
|
if cached is not None:
|
|
return cached
|
|
try:
|
|
products = self.repository.list_products(session, filters or {})
|
|
categories = {category.id: category for category in self.repository.list_categories(session, {})}
|
|
grouped_list = self._group_products(products, categories)
|
|
result = {
|
|
"total": len(grouped_list),
|
|
"page_no": 1,
|
|
"page_size": len(grouped_list) or 20,
|
|
"list": grouped_list,
|
|
}
|
|
cache_set(cache_key, result, ttl=120)
|
|
return result
|
|
except SQLAlchemyError as exc:
|
|
raise AppException(code=ErrorCode.SYSTEM_ERROR, message="数据库操作失败", status_code=500) from exc
|
|
|
|
def update_product(self, product_id: int, payload: dict, session: Session | None = None) -> dict:
|
|
"""更新产品主档信息(名称、分类、状态等)。
|
|
|
|
仅更新产品级别的公共字段,不影响规格明细。
|
|
|
|
Args:
|
|
product_id: 产品 ID
|
|
payload: 包含更新字段的请求体
|
|
session: 数据库会话
|
|
|
|
Returns:
|
|
更新结果字典
|
|
|
|
被调用路由: products.py - PUT /products/{product_id}
|
|
"""
|
|
if session is None:
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="数据库会话未初始化", status_code=500)
|
|
try:
|
|
product = self.repository.get_product(session, product_id)
|
|
if product is None:
|
|
raise AppException(code=ErrorCode.NOT_FOUND, message="产品主档不存在", status_code=404)
|
|
if not payload["product_name"].strip():
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="产品名称不能为空", status_code=400)
|
|
category = None
|
|
if payload.get("category_id") is not None:
|
|
category = self.repository.get_category(session, payload["category_id"])
|
|
if category is None or category.status != 1:
|
|
raise AppException(code=ErrorCode.NOT_FOUND, message="产品分类不存在或已停用", status_code=404)
|
|
product.product_name = payload["product_name"]
|
|
product.category_id = payload.get("category_id")
|
|
product.category = category.category_name if category else None
|
|
product.status = payload.get("status", product.status)
|
|
product.remark = payload.get("remark", product.remark)
|
|
session.add(product)
|
|
session.commit()
|
|
cache_delete_pattern("product:*")
|
|
return {"product_id": product.id, "product_name": product.product_name, "updated": True}
|
|
except AppException:
|
|
session.rollback()
|
|
raise
|
|
except SQLAlchemyError as exc:
|
|
session.rollback()
|
|
raise AppException(code=ErrorCode.SYSTEM_ERROR, message="数据库操作失败", status_code=500) from exc
|
|
|
|
def update_product_specification(self, product_id: int, payload: dict, session: Session | None = None) -> dict:
|
|
"""更新产品规格明细。
|
|
|
|
校验规格在同一产品下唯一,可修改规格名称、单位、价格等。
|
|
|
|
Args:
|
|
product_id: 产品(规格) ID
|
|
payload: 包含 specification、unit、cost_price、sale_price 等的请求体
|
|
session: 数据库会话
|
|
|
|
Returns:
|
|
更新结果字典
|
|
|
|
被调用路由: products.py - PUT /products/{product_id}/specification
|
|
"""
|
|
if session is None:
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="数据库会话未初始化", status_code=500)
|
|
try:
|
|
product = self.repository.get_product(session, product_id)
|
|
if product is None:
|
|
raise AppException(code=ErrorCode.NOT_FOUND, message="产品规格不存在", status_code=404)
|
|
if not payload["specification"].strip():
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="产品规格不能为空", status_code=400)
|
|
if not payload["unit"].strip():
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="产品单位不能为空", status_code=400)
|
|
existed = self.repository.get_product_by_name_and_specification(session, product.product_name, payload["specification"])
|
|
if existed is not None and existed.id != product_id:
|
|
raise AppException(code=ErrorCode.DUPLICATE, message="同一产品下的规格不能重复", status_code=400)
|
|
category = None
|
|
if payload.get("category_id") is not None:
|
|
category = self.repository.get_category(session, payload["category_id"])
|
|
if category is None or category.status != 1:
|
|
raise AppException(code=ErrorCode.NOT_FOUND, message="产品分类不存在或已停用", status_code=404)
|
|
product.specification = payload["specification"]
|
|
product.unit = payload["unit"]
|
|
product.category_id = payload.get("category_id")
|
|
product.category = category.category_name if category else product.category
|
|
product.cost_price = payload.get("cost_price", product.cost_price)
|
|
product.sale_price = payload.get("sale_price", product.sale_price)
|
|
product.status = payload.get("status", product.status)
|
|
product.remark = payload.get("remark", product.remark)
|
|
session.add(product)
|
|
session.commit()
|
|
cache_delete_pattern("product:*")
|
|
return {"product_id": product.id, "product_name": product.product_name, "updated": True}
|
|
except AppException:
|
|
session.rollback()
|
|
raise
|
|
except SQLAlchemyError as exc:
|
|
session.rollback()
|
|
raise AppException(code=ErrorCode.SYSTEM_ERROR, message="数据库操作失败", status_code=500) from exc
|
|
|
|
def add_specification(self, product_id: int, payload: dict, session: Session | None = None) -> dict:
|
|
"""给已有产品追加一条规格明细。
|
|
|
|
根据 product_id 找到所属产品,校验规格名不重复后创建新规格记录。
|
|
|
|
Args:
|
|
product_id: 产品组中任一规格的 ID
|
|
payload: 包含 specification、unit、cost_price、sale_price 等的请求体
|
|
session: 数据库会话
|
|
|
|
Returns:
|
|
创建结果字典
|
|
|
|
被调用路由: products.py - POST /products/{product_id}/specifications
|
|
"""
|
|
if session is None:
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="数据库会话未初始化", status_code=500)
|
|
try:
|
|
product = self.repository.get_product(session, product_id)
|
|
if product is None:
|
|
raise AppException(code=ErrorCode.NOT_FOUND, message="产品不存在", status_code=404)
|
|
if not payload.get("specification", "").strip():
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="产品规格不能为空", status_code=400)
|
|
if not payload.get("unit", "").strip():
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="产品单位不能为空", status_code=400)
|
|
existed = self.repository.get_product_by_name_and_specification(
|
|
session, product.product_name, payload["specification"]
|
|
)
|
|
if existed is not None:
|
|
raise AppException(code=ErrorCode.DUPLICATE, message="同一产品下的规格不能重复", status_code=400)
|
|
category = None
|
|
if payload.get("category_id") is not None:
|
|
category = self.repository.get_category(session, payload["category_id"])
|
|
new_spec = self.repository.create_product(session, {
|
|
"product_name": product.product_name,
|
|
"specification": payload["specification"],
|
|
"unit": payload["unit"],
|
|
"category_id": payload.get("category_id") or product.category_id,
|
|
"category": category.category_name if category else product.category,
|
|
"cost_price": payload.get("cost_price", 0),
|
|
"sale_price": payload.get("sale_price", 0),
|
|
"status": payload.get("status", 1),
|
|
"remark": payload.get("remark"),
|
|
})
|
|
audit_service.write_log(session, {
|
|
"operate_type": "product_spec_add",
|
|
"biz_type": "product",
|
|
"biz_id": new_spec.id,
|
|
"after_value": {"product_name": product.product_name, "specification": payload["specification"]},
|
|
"remark": f"给产品 {product.product_name} 新增规格 {payload['specification']}",
|
|
})
|
|
cache_delete_pattern("product:*")
|
|
return {"product_id": new_spec.id, "product_name": product.product_name, "specification": new_spec.specification}
|
|
except AppException:
|
|
session.rollback()
|
|
raise
|
|
except SQLAlchemyError as exc:
|
|
session.rollback()
|
|
raise AppException(code=ErrorCode.SYSTEM_ERROR, message="数据库操作失败", status_code=500) from exc
|
|
|
|
def set_default_specification(self, product_id: int, session: Session | None = None) -> dict:
|
|
"""设置产品默认规格。
|
|
|
|
将指定规格标记为默认,同名产品下的其他规格清除默认标记。
|
|
|
|
Args:
|
|
product_id: 产品(规格) ID
|
|
session: 数据库会话
|
|
|
|
Returns:
|
|
设置结果字典
|
|
|
|
被调用路由: products.py - POST /products/{product_id}/set-default
|
|
"""
|
|
if session is None:
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="数据库会话未初始化", status_code=500)
|
|
try:
|
|
product = self.repository.get_product(session, product_id)
|
|
if product is None:
|
|
raise AppException(code=ErrorCode.NOT_FOUND, message="产品规格不存在", status_code=404)
|
|
siblings = self.repository.list_products(session, {"product_name": product.product_name})
|
|
for sibling in siblings:
|
|
sibling.is_default = 1 if sibling.id == product.id else 0
|
|
session.add(sibling)
|
|
session.flush()
|
|
session.commit()
|
|
cache_delete_pattern("product:*")
|
|
return {"product_id": product.id, "product_name": product.product_name, "is_default": True}
|
|
except AppException:
|
|
session.rollback()
|
|
raise
|
|
except SQLAlchemyError as exc:
|
|
session.rollback()
|
|
raise AppException(code=ErrorCode.SYSTEM_ERROR, message="数据库操作失败", status_code=500) from exc
|
|
|
|
def create_product(self, payload: dict, session: Session | None = None) -> dict:
|
|
"""创建新产品(含规格明细)。
|
|
|
|
产品名称为公共字段,每条规格明细单独创建一条数据库记录。
|
|
同一产品下的规格名称不可重复。自动记录审计日志。
|
|
|
|
Args:
|
|
payload: 包含 product_name、category_id、specifications 列表的请求体
|
|
session: 数据库会话
|
|
|
|
Returns:
|
|
创建结果字典,包含产品名称和所有规格明细
|
|
|
|
被调用路由: products.py - POST /products
|
|
"""
|
|
if session is None:
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="数据库会话未初始化", status_code=500)
|
|
try:
|
|
if not payload["product_name"].strip():
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="产品名称不能为空", status_code=400)
|
|
specifications = payload.get("specifications") or []
|
|
if not specifications:
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="请至少填写一个规格明细", status_code=400)
|
|
|
|
category = None
|
|
if payload.get("category_id") is not None:
|
|
category = self.repository.get_category(session, payload["category_id"])
|
|
if category is None or category.status != 1:
|
|
raise AppException(code=ErrorCode.NOT_FOUND, message="产品分类不存在或已停用", status_code=404)
|
|
|
|
created_specs = []
|
|
for spec in specifications:
|
|
if not spec["specification"].strip():
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="产品规格不能为空", status_code=400)
|
|
if not spec["unit"].strip():
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="产品单位不能为空", status_code=400)
|
|
existed = self.repository.get_product_by_name_and_specification(
|
|
session,
|
|
payload["product_name"],
|
|
spec["specification"],
|
|
)
|
|
if existed is not None:
|
|
raise AppException(code=ErrorCode.DUPLICATE, message="同一产品下的规格不能重复", status_code=400)
|
|
product = self.repository.create_product(
|
|
session,
|
|
{
|
|
"product_name": payload["product_name"],
|
|
"specification": spec["specification"],
|
|
"unit": spec["unit"],
|
|
"category_id": spec.get("category_id") or payload.get("category_id"),
|
|
"category": category.category_name if category else None,
|
|
"cost_price": spec.get("cost_price", 0),
|
|
"sale_price": spec.get("sale_price", 0),
|
|
"status": spec.get("status", payload.get("status", 1)),
|
|
"remark": spec.get("remark") or payload.get("remark"),
|
|
},
|
|
)
|
|
created_specs.append(product)
|
|
audit_service.write_log(
|
|
session,
|
|
{
|
|
"operate_type": "product_create",
|
|
"biz_type": "product",
|
|
"biz_id": created_specs[0].id if created_specs else 0,
|
|
"before_value": None,
|
|
"after_value": {
|
|
"product_name": payload["product_name"],
|
|
"spec_count": len(created_specs),
|
|
"category_id": payload.get("category_id"),
|
|
},
|
|
"remark": f"新增产品 {payload['product_name']},共 {len(created_specs)} 个规格",
|
|
},
|
|
)
|
|
session.commit()
|
|
cache_delete_pattern("product:*")
|
|
return {
|
|
"product_name": payload["product_name"],
|
|
"product_id": created_specs[0].id if created_specs else None,
|
|
"specifications": [
|
|
{
|
|
"product_id": item.id,
|
|
"specification": item.specification,
|
|
"unit": item.unit,
|
|
"cost_price": float(item.cost_price or 0),
|
|
"sale_price": float(item.sale_price or 0),
|
|
"status": item.status,
|
|
"remark": item.remark,
|
|
"is_default": index == 0,
|
|
}
|
|
for index, item in enumerate(created_specs)
|
|
],
|
|
}
|
|
except AppException:
|
|
session.rollback()
|
|
raise
|
|
except SQLAlchemyError as exc:
|
|
session.rollback()
|
|
raise AppException(code=ErrorCode.SYSTEM_ERROR, message="数据库操作失败", status_code=500) from exc
|
|
|
|
def get_product(self, product_id: int, session: Session | None = None) -> dict:
|
|
"""查询单个产品详情(含所有规格明细)。
|
|
|
|
通过产品名称查询同名所有规格,再按分组逻辑返回完整产品信息。
|
|
|
|
Args:
|
|
product_id: 产品(规格) ID
|
|
session: 数据库会话
|
|
|
|
Returns:
|
|
产品详情字典(含 specifications 列表)
|
|
|
|
被调用路由: products.py - GET /products/{product_id}
|
|
"""
|
|
if session is None:
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="数据库会话未初始化", status_code=500)
|
|
cache_key = f"product:detail:{product_id}"
|
|
cached = cache_get(cache_key)
|
|
if cached is not None:
|
|
return cached
|
|
try:
|
|
product = self.repository.get_product(session, product_id)
|
|
if product is None:
|
|
raise AppException(code=ErrorCode.NOT_FOUND, message="产品不存在", status_code=404)
|
|
products = self.repository.list_products(session, {"product_name": product.product_name})
|
|
categories = {category.id: category for category in self.repository.list_categories(session, {})}
|
|
grouped = self._group_products(products, categories)
|
|
if grouped:
|
|
cache_set(cache_key, grouped[0], ttl=120)
|
|
return grouped[0]
|
|
raise AppException(code=ErrorCode.NOT_FOUND, message="产品不存在", status_code=404)
|
|
except AppException:
|
|
raise
|
|
except SQLAlchemyError as exc:
|
|
raise AppException(code=ErrorCode.SYSTEM_ERROR, message="数据库操作失败", status_code=500) from exc
|
|
|
|
|
|
product_service = ProductService()
|