Compare commits

...

4 Commits

48 changed files with 3747 additions and 282 deletions

View File

@ -1,8 +1,8 @@
from fastapi import APIRouter, Depends, Header
from sqlalchemy.orm import Session
from backend.app.api.deps import get_auth_service
from backend.app.core.error_codes import ErrorCode
from backend.app.core.exceptions import AppException
from backend.app.db import get_db_session
from backend.app.schemas.auth import LoginRequest
from backend.app.schemas.common import success_payload
from backend.app.services.auth_service import AuthService
@ -11,21 +11,26 @@ router = APIRouter(prefix="/api/auth", tags=["auth"])
@router.post("/login")
def login(payload: LoginRequest, auth_service: AuthService = Depends(get_auth_service)) -> dict:
user = auth_service.login(payload.username, payload.role_type)
if not user:
raise AppException(code=ErrorCode.UNAUTHORIZED, message="账号不存在或角色不匹配", status_code=400)
return success_payload(user)
def login(
payload: LoginRequest,
auth_service: AuthService = Depends(get_auth_service),
session: Session = Depends(get_db_session),
) -> dict:
return success_payload(auth_service.login(payload.username, payload.password, payload.role_type, session))
@router.get("/me")
def me(
authorization: str | None = Header(default=None),
auth_service: AuthService = Depends(get_auth_service),
session: Session = Depends(get_db_session),
) -> dict:
token = (authorization or "").removeprefix("Bearer").strip()
user = auth_service.get_me(token)
user = auth_service.get_me(token, session)
if not user:
from backend.app.core.error_codes import ErrorCode
from backend.app.core.exceptions import AppException
raise AppException(code=ErrorCode.UNAUTHORIZED, message="未登录或登录失效", status_code=401)
return success_payload(user)

View File

@ -8,6 +8,7 @@ from backend.app.services.product_service import product_service
from backend.app.services.reminder_service import reminder_service
from backend.app.services.report_service import report_service
from backend.app.services.supplier_service import supplier_service
from backend.app.services.system_service import system_service
# 统一依赖入口,后续如果切换到真实容器或数据库实现,只需要改这里。
@ -49,3 +50,7 @@ def get_config_service():
def get_audit_service():
return audit_service
def get_system_service():
return system_service

View File

@ -1,22 +1,18 @@
from fastapi import APIRouter
from backend.app.schemas.common import success_payload
from backend.app.schemas.files import CreateUploadTokenRequest, SaveAttachmentRequest
from backend.app.services.file_service import file_service
router = APIRouter(prefix="/api/files", tags=["files"])
@router.post("/upload-token")
def create_upload_token() -> dict:
return success_payload(
{
"upload_url": "https://oss-upload-url",
"file_url": "https://oss-public-url/pickup.jpg",
"headers": {},
"expire_at": "2026-05-14 12:00:00",
}
)
def create_upload_token(payload: CreateUploadTokenRequest) -> dict:
return success_payload(file_service.create_upload_token(payload.model_dump()))
@router.post("/attachments")
def save_attachment() -> dict:
return success_payload({"attachment_id": 8001, "saved": True})
def save_attachment(payload: SaveAttachmentRequest) -> dict:
return success_payload(file_service.save_attachment(payload.model_dump()))

View File

@ -4,7 +4,7 @@ from sqlalchemy.orm import Session
from backend.app.api.deps import get_logistics_service
from backend.app.db import get_db_session
from backend.app.schemas.common import success_payload
from backend.app.schemas.logistics import CreateLogisticsTaskRequest
from backend.app.schemas.logistics import CreateLogisticsTaskRequest, CreateLogisticsTraceRequest, DriverTaskOperateRequest
from backend.app.services.logistics_service import LogisticsService
router = APIRouter(tags=["logistics"])
@ -72,28 +72,31 @@ def get_driver_task(
@router.post("/api/driver/tasks/{task_id}/accept")
def accept_driver_task(
task_id: int,
payload: DriverTaskOperateRequest,
logistics_service: LogisticsService = Depends(get_logistics_service),
session: Session = Depends(get_db_session),
) -> dict:
return success_payload(logistics_service.accept_task(task_id, session))
return success_payload(logistics_service.accept_task(task_id, payload.model_dump(), session))
@router.post("/api/driver/tasks/{task_id}/pickup")
def pickup_driver_task(
task_id: int,
payload: DriverTaskOperateRequest,
logistics_service: LogisticsService = Depends(get_logistics_service),
session: Session = Depends(get_db_session),
) -> dict:
return success_payload(logistics_service.pickup_task(task_id, session))
return success_payload(logistics_service.pickup_task(task_id, payload.model_dump(), session))
@router.post("/api/driver/tasks/{task_id}/deliver")
def deliver_driver_task(
task_id: int,
payload: DriverTaskOperateRequest,
logistics_service: LogisticsService = Depends(get_logistics_service),
session: Session = Depends(get_db_session),
) -> dict:
return success_payload(logistics_service.deliver_task(task_id, session))
return success_payload(logistics_service.deliver_task(task_id, payload.model_dump(), session))
@router.get("/api/logistics/{order_id}/trace")
@ -107,7 +110,7 @@ def get_logistics_trace(
@router.post("/api/logistics/{order_id}/trace")
def create_logistics_trace(
order_id: int,
payload: CreateLogisticsTraceRequest,
logistics_service: LogisticsService = Depends(get_logistics_service),
) -> dict:
return success_payload(logistics_service.create_trace(order_id))
return success_payload(logistics_service.create_trace(order_id, payload.model_dump()))

View File

@ -1,36 +1,94 @@
from fastapi import APIRouter, Query
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from backend.app.api.deps import get_product_service
from backend.app.db import get_db_session
from backend.app.schemas.common import success_payload
from backend.app.services.product_service import product_service
from backend.app.schemas.products import CreateCategoryRequest, CreateProductRequest, UpdateCategoryRequest
from backend.app.services.product_service import ProductService
router = APIRouter(tags=["products"])
@router.get("/api/product-categories")
def list_categories() -> dict:
return success_payload(product_service.list_categories())
def list_categories(
category_name: str | None = Query(default=None),
status: int | None = Query(default=None),
page_no: int = Query(default=1),
page_size: int = Query(default=20),
product_service: ProductService = Depends(get_product_service),
session: Session = Depends(get_db_session),
) -> dict:
result = product_service.list_categories(
session=session,
filters={"category_name": category_name, "status": status},
)
start = max(page_no - 1, 0) * page_size
result["list"] = result["list"][start : start + page_size]
result["page_no"] = page_no
result["page_size"] = page_size
return success_payload(result)
@router.post("/api/product-categories")
def create_category() -> dict:
return success_payload(product_service.create_category())
def create_category(
payload: CreateCategoryRequest,
product_service: ProductService = Depends(get_product_service),
session: Session = Depends(get_db_session),
) -> dict:
return success_payload(product_service.create_category(payload.model_dump(), session))
@router.put("/api/product-categories/{category_id}")
def update_category(category_id: int) -> dict:
return success_payload(product_service.update_category(category_id))
def update_category(
category_id: int,
payload: UpdateCategoryRequest,
product_service: ProductService = Depends(get_product_service),
session: Session = Depends(get_db_session),
) -> dict:
return success_payload(product_service.update_category(category_id, payload.model_dump(), session))
@router.get("/api/products")
def list_products(product_name: str | None = Query(default=None)) -> dict:
return success_payload(product_service.list_products({"product_name": product_name}))
def list_products(
product_name: str | None = Query(default=None),
specification: str | None = Query(default=None),
category_id: int | None = Query(default=None),
status: int | None = Query(default=None),
page_no: int = Query(default=1),
page_size: int = Query(default=20),
product_service: ProductService = Depends(get_product_service),
session: Session = Depends(get_db_session),
) -> dict:
result = product_service.list_products(
{
"product_name": product_name,
"specification": specification,
"category_id": category_id,
"status": status,
},
session,
)
start = max(page_no - 1, 0) * page_size
result["list"] = result["list"][start : start + page_size]
result["page_no"] = page_no
result["page_size"] = page_size
return success_payload(result)
@router.post("/api/products")
def create_product() -> dict:
return success_payload(product_service.create_product())
def create_product(
payload: CreateProductRequest,
product_service: ProductService = Depends(get_product_service),
session: Session = Depends(get_db_session),
) -> dict:
return success_payload(product_service.create_product(payload.model_dump(), session))
@router.get("/api/products/{product_id}")
def get_product(product_id: int) -> dict:
return success_payload(product_service.get_product(product_id))
def get_product(
product_id: int,
product_service: ProductService = Depends(get_product_service),
session: Session = Depends(get_db_session),
) -> dict:
return success_payload(product_service.get_product(product_id, session))

View File

@ -1,16 +1,40 @@
from fastapi import APIRouter, Query
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from backend.app.api.deps import get_supplier_service
from backend.app.db import get_db_session
from backend.app.schemas.common import success_payload
from backend.app.services.supplier_service import supplier_service
from backend.app.schemas.suppliers import CreateSupplierRequest
from backend.app.services.supplier_service import SupplierService
router = APIRouter(prefix="/api/suppliers", tags=["suppliers"])
@router.get("")
def list_suppliers(supplier_type: str | None = Query(default=None)) -> dict:
return success_payload(supplier_service.list_suppliers({"supplier_type": supplier_type}))
def list_suppliers(
supplier_name: str | None = Query(default=None),
supplier_type: str | None = Query(default=None),
status: int | None = Query(default=None),
page_no: int = Query(default=1),
page_size: int = Query(default=20),
supplier_service: SupplierService = Depends(get_supplier_service),
session: Session = Depends(get_db_session),
) -> dict:
result = supplier_service.list_suppliers(
{"supplier_name": supplier_name, "supplier_type": supplier_type, "status": status},
session,
)
start = max(page_no - 1, 0) * page_size
result["list"] = result["list"][start : start + page_size]
result["page_no"] = page_no
result["page_size"] = page_size
return success_payload(result)
@router.post("")
def create_supplier() -> dict:
return success_payload(supplier_service.create_supplier())
def create_supplier(
payload: CreateSupplierRequest,
supplier_service: SupplierService = Depends(get_supplier_service),
session: Session = Depends(get_db_session),
) -> dict:
return success_payload(supplier_service.create_supplier(payload.model_dump(), session))

View File

@ -1,64 +1,150 @@
from fastapi import APIRouter
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from backend.app.api.deps import get_system_service
from backend.app.db import get_db_session
from backend.app.schemas.common import success_payload
from backend.app.schemas.system import (
CreateMenuRequest,
CreateRoleRequest,
CreateUserRequest,
ResetPasswordRequest,
UpdateMenuRequest,
UpdateRoleRequest,
UpdateUserRequest,
)
from backend.app.services.system_service import SystemService
router = APIRouter(prefix="/api/system", tags=["system"])
@router.get("/users")
def list_users() -> dict:
return success_payload(
def list_users(
username: str | None = Query(default=None),
real_name: str | None = Query(default=None),
mobile: str | None = Query(default=None),
role_id: int | None = Query(default=None),
status: int | None = Query(default=None),
page_no: int = Query(default=1),
page_size: int = Query(default=20),
system_service: SystemService = Depends(get_system_service),
session: Session = Depends(get_db_session),
) -> dict:
result = system_service.list_users(
session,
{
"total": 2,
"page_no": 1,
"page_size": 20,
"list": [
{
"user_id": 1,
"username": "sales01",
"real_name": "张三",
"mobile": "13800000000",
"role_id": 2,
"role_name": "业务员",
"role_code": "salesman",
"status": 1,
"created_at": "2026-05-14 10:00:00",
}
],
}
"username": username,
"real_name": real_name,
"mobile": mobile,
"role_id": role_id,
"status": status,
},
)
start = max(page_no - 1, 0) * page_size
result["list"] = result["list"][start : start + page_size]
result["page_no"] = page_no
result["page_size"] = page_size
return success_payload(result)
@router.post("/users")
def create_user(
payload: CreateUserRequest,
system_service: SystemService = Depends(get_system_service),
session: Session = Depends(get_db_session),
) -> dict:
return success_payload(system_service.create_user(payload.model_dump(), session))
@router.put("/users/{user_id}")
def update_user(
user_id: int,
payload: UpdateUserRequest,
system_service: SystemService = Depends(get_system_service),
session: Session = Depends(get_db_session),
) -> dict:
return success_payload(system_service.update_user(user_id, payload.model_dump(), session))
@router.post("/users/{user_id}/reset-password")
def reset_password(
user_id: int,
payload: ResetPasswordRequest,
system_service: SystemService = Depends(get_system_service),
session: Session = Depends(get_db_session),
) -> dict:
return success_payload(system_service.reset_password(user_id, payload.new_password, session))
@router.get("/roles")
def list_roles() -> dict:
return success_payload(
{
"total": 2,
"page_no": 1,
"page_size": 20,
"list": [
{"role_id": 1, "role_name": "管理员", "role_code": "admin", "status": 1},
{"role_id": 2, "role_name": "业务员", "role_code": "salesman", "status": 1},
],
}
def list_roles(
role_name: str | None = Query(default=None),
role_code: str | None = Query(default=None),
status: int | None = Query(default=None),
page_no: int = Query(default=1),
page_size: int = Query(default=20),
system_service: SystemService = Depends(get_system_service),
session: Session = Depends(get_db_session),
) -> dict:
result = system_service.list_roles(
session,
{"role_name": role_name, "role_code": role_code, "status": status},
)
start = max(page_no - 1, 0) * page_size
result["list"] = result["list"][start : start + page_size]
result["page_no"] = page_no
result["page_size"] = page_size
return success_payload(result)
@router.post("/roles")
def create_role(
payload: CreateRoleRequest,
system_service: SystemService = Depends(get_system_service),
session: Session = Depends(get_db_session),
) -> dict:
return success_payload(system_service.create_role(payload.model_dump(), session))
@router.put("/roles/{role_id}")
def update_role(
role_id: int,
payload: UpdateRoleRequest,
system_service: SystemService = Depends(get_system_service),
session: Session = Depends(get_db_session),
) -> dict:
return success_payload(system_service.update_role(role_id, payload.model_dump(), session))
@router.get("/menus")
def list_menus() -> dict:
def list_menus(
status: int | None = Query(default=None),
menu_type: str | None = Query(default=None),
system_service: SystemService = Depends(get_system_service),
session: Session = Depends(get_db_session),
) -> dict:
return success_payload(
[
{
"menu_id": 1,
"parent_id": 0,
"menu_name": "订单管理",
"menu_path": "/orders",
"menu_type": "page",
"permission_code": "order:list",
"icon": "order",
"sort_no": 10,
"status": 1,
"children": [],
}
]
system_service.list_menus(
session,
{"status": status, "menu_type": menu_type},
)
)
@router.post("/menus")
def create_menu(
payload: CreateMenuRequest,
system_service: SystemService = Depends(get_system_service),
session: Session = Depends(get_db_session),
) -> dict:
return success_payload(system_service.create_menu(payload.model_dump(), session))
@router.put("/menus/{menu_id}")
def update_menu(
menu_id: int,
payload: UpdateMenuRequest,
system_service: SystemService = Depends(get_system_service),
session: Session = Depends(get_db_session),
) -> dict:
return success_payload(system_service.update_menu(menu_id, payload.model_dump(), session))

View File

@ -0,0 +1,79 @@
from sqlalchemy import select
from sqlalchemy.orm import Session
from backend.app.models.business import Product, ProductCategory
class ProductRepository:
def list_categories(self, session: Session, filters: dict) -> list[ProductCategory]:
stmt = select(ProductCategory).where(ProductCategory.deleted == 0)
if filters.get("category_name"):
stmt = stmt.where(ProductCategory.category_name.contains(filters["category_name"]))
if filters.get("status") is not None:
stmt = stmt.where(ProductCategory.status == filters["status"])
stmt = stmt.order_by(ProductCategory.sort_no.asc(), ProductCategory.id.asc())
return list(session.execute(stmt).scalars())
def get_category(self, session: Session, category_id: int) -> ProductCategory | None:
stmt = select(ProductCategory).where(ProductCategory.id == category_id, ProductCategory.deleted == 0)
return session.execute(stmt).scalar_one_or_none()
def get_category_by_code(self, session: Session, category_code: str) -> ProductCategory | None:
stmt = select(ProductCategory).where(
ProductCategory.category_code == category_code,
ProductCategory.deleted == 0,
)
return session.execute(stmt).scalar_one_or_none()
def create_category(self, session: Session, payload: dict) -> ProductCategory:
category = ProductCategory(**payload)
session.add(category)
session.flush()
return category
def update_category(self, session: Session, category: ProductCategory, payload: dict) -> ProductCategory:
for key, value in payload.items():
setattr(category, key, value)
session.add(category)
session.flush()
return category
def list_products(self, session: Session, filters: dict) -> list[Product]:
stmt = select(Product).where(Product.deleted == 0)
if filters.get("product_name"):
stmt = stmt.where(Product.product_name.contains(filters["product_name"]))
if filters.get("specification"):
stmt = stmt.where(Product.specification.contains(filters["specification"]))
if filters.get("category_id") is not None:
stmt = stmt.where(Product.category_id == filters["category_id"])
if filters.get("status") is not None:
stmt = stmt.where(Product.status == filters["status"])
stmt = stmt.order_by(Product.id.desc())
return list(session.execute(stmt).scalars())
def get_product(self, session: Session, product_id: int) -> Product | None:
stmt = select(Product).where(Product.id == product_id, Product.deleted == 0)
return session.execute(stmt).scalar_one_or_none()
def get_product_by_name_and_specification(
self,
session: Session,
product_name: str,
specification: str,
) -> Product | None:
stmt = select(Product).where(
Product.product_name == product_name,
Product.specification == specification,
Product.deleted == 0,
)
return session.execute(stmt).scalar_one_or_none()
def create_product(self, session: Session, payload: dict) -> Product:
product = Product(**payload)
session.add(product)
session.flush()
return product

View File

@ -0,0 +1,38 @@
from sqlalchemy import select
from sqlalchemy.orm import Session
from backend.app.models.business import Supplier
class SupplierRepository:
def list_suppliers(self, session: Session, filters: dict) -> list[Supplier]:
stmt = select(Supplier).where(Supplier.deleted == 0)
if filters.get("supplier_name"):
stmt = stmt.where(Supplier.supplier_name.contains(filters["supplier_name"]))
if filters.get("supplier_type"):
stmt = stmt.where(Supplier.supplier_type == filters["supplier_type"])
if filters.get("status") is not None:
stmt = stmt.where(Supplier.status == filters["status"])
stmt = stmt.order_by(Supplier.id.desc())
return list(session.execute(stmt).scalars())
def get_supplier_by_name_and_type(
self,
session: Session,
supplier_name: str,
supplier_type: str,
) -> Supplier | None:
stmt = select(Supplier).where(
Supplier.supplier_name == supplier_name,
Supplier.supplier_type == supplier_type,
Supplier.deleted == 0,
)
return session.execute(stmt).scalar_one_or_none()
def create_supplier(self, session: Session, payload: dict) -> Supplier:
supplier = Supplier(**payload)
session.add(supplier)
session.flush()
return supplier

View File

@ -0,0 +1,142 @@
from sqlalchemy import select
from sqlalchemy.orm import Session
from backend.app.models.system import Menu, Role, User
class SystemRepository:
def get_user_by_username(self, session: Session, username: str) -> User | None:
stmt = select(User).where(User.username == username)
return session.execute(stmt).scalar_one_or_none()
def get_user(self, session: Session, user_id: int) -> User | None:
stmt = select(User).where(User.id == user_id)
return session.execute(stmt).scalar_one_or_none()
def create_user(self, session: Session, payload: dict) -> User:
user = User(
username=payload["username"],
password_hash=payload["password_hash"],
real_name=payload["real_name"],
mobile=payload.get("mobile") or "",
role_id=payload["role_id"],
status=payload.get("status", 1),
)
session.add(user)
session.flush()
return user
def update_user(self, user: User, payload: dict) -> User:
user.real_name = payload["real_name"]
user.mobile = payload.get("mobile") or ""
user.role_id = payload["role_id"]
user.status = payload.get("status", 1)
return user
def reset_password(self, user: User, password_hash: str) -> User:
user.password_hash = password_hash
return user
def get_role(self, session: Session, role_id: int) -> Role | None:
stmt = select(Role).where(Role.id == role_id)
return session.execute(stmt).scalar_one_or_none()
def get_role_by_code(self, session: Session, role_code: str) -> Role | None:
stmt = select(Role).where(Role.role_code == role_code)
return session.execute(stmt).scalar_one_or_none()
def create_role(self, session: Session, payload: dict) -> Role:
role = Role(
role_name=payload["role_name"],
role_code=payload["role_code"],
status=payload.get("status", 1),
remark=payload.get("remark"),
)
session.add(role)
session.flush()
return role
def update_role(self, role: Role, payload: dict) -> Role:
role.role_name = payload["role_name"]
role.status = payload.get("status", 1)
role.remark = payload.get("remark")
return role
def get_menu(self, session: Session, menu_id: int) -> Menu | None:
stmt = select(Menu).where(Menu.id == menu_id)
return session.execute(stmt).scalar_one_or_none()
def get_menu_by_permission_code(self, session: Session, permission_code: str) -> Menu | None:
stmt = select(Menu).where(Menu.permission_code == permission_code)
return session.execute(stmt).scalar_one_or_none()
def create_menu(self, session: Session, payload: dict) -> Menu:
menu = Menu(
parent_id=payload.get("parent_id", 0),
menu_name=payload["menu_name"],
menu_path=payload.get("menu_path") or "",
menu_type=payload.get("menu_type", "page"),
permission_code=payload["permission_code"],
icon=payload.get("icon"),
sort_no=payload.get("sort_no", 0),
status=payload.get("status", 1),
)
session.add(menu)
session.flush()
return menu
def update_menu(self, menu: Menu, payload: dict) -> Menu:
menu.parent_id = payload.get("parent_id", 0)
menu.menu_name = payload["menu_name"]
menu.menu_path = payload.get("menu_path") or ""
menu.menu_type = payload.get("menu_type", "page")
menu.permission_code = payload["permission_code"]
menu.icon = payload.get("icon")
menu.sort_no = payload.get("sort_no", 0)
menu.status = payload.get("status", 1)
return menu
def list_users(self, session: Session, filters: dict) -> list[tuple[User, Role | None]]:
stmt = select(User, Role).outerjoin(Role, User.role_id == Role.id)
if filters.get("username"):
stmt = stmt.where(User.username.contains(filters["username"]))
if filters.get("real_name"):
stmt = stmt.where(User.real_name.contains(filters["real_name"]))
if filters.get("mobile"):
stmt = stmt.where(User.mobile.contains(filters["mobile"]))
if filters.get("role_id") is not None:
stmt = stmt.where(User.role_id == filters["role_id"])
if filters.get("status") is not None:
stmt = stmt.where(User.status == filters["status"])
stmt = stmt.order_by(User.id.asc())
return list(session.execute(stmt).all())
def list_roles(self, session: Session, filters: dict) -> list[Role]:
stmt = select(Role)
if filters.get("role_name"):
stmt = stmt.where(Role.role_name.contains(filters["role_name"]))
if filters.get("role_code"):
stmt = stmt.where(Role.role_code.contains(filters["role_code"]))
if filters.get("status") is not None:
stmt = stmt.where(Role.status == filters["status"])
stmt = stmt.order_by(Role.id.asc())
return list(session.execute(stmt).scalars())
def count_active_users_by_role(self, session: Session, role_id: int) -> int:
stmt = select(User).where(User.role_id == role_id, User.status == 1)
return len(list(session.execute(stmt).scalars()))
def list_menus(self, session: Session, filters: dict) -> list[Menu]:
stmt = select(Menu)
if filters.get("status") is not None:
stmt = stmt.where(Menu.status == filters["status"])
if filters.get("menu_type"):
stmt = stmt.where(Menu.menu_type == filters["menu_type"])
stmt = stmt.order_by(Menu.sort_no.asc(), Menu.id.asc())
return list(session.execute(stmt).scalars())

View File

@ -0,0 +1,19 @@
from pydantic import BaseModel, Field
class CreateUploadTokenRequest(BaseModel):
biz_type: str
biz_id: int = Field(gt=0)
file_name: str
file_type: str
file_size: int = Field(gt=0)
class SaveAttachmentRequest(BaseModel):
biz_type: str
biz_id: int = Field(gt=0)
file_name: str
file_url: str
file_type: str
file_size: int = Field(gt=0)

View File

@ -21,3 +21,21 @@ class CreateLogisticsTaskRequest(BaseModel):
factory_id: int | None = None
remark: str | None = None
class LogisticsFilePayload(BaseModel):
file_url: str
file_name: str
class DriverTaskOperateRequest(BaseModel):
photo_files: list[LogisticsFilePayload] = Field(default_factory=list)
video_files: list[LogisticsFilePayload] = Field(default_factory=list)
remark: str | None = None
class CreateLogisticsTraceRequest(BaseModel):
task_id: int = Field(gt=0)
node_time: str
node_desc: str
node_type: str
source_platform: str

View File

@ -0,0 +1,28 @@
from pydantic import BaseModel
class CreateCategoryRequest(BaseModel):
category_name: str
category_code: str
sort_no: int = 0
status: int = 1
remark: str | None = None
class UpdateCategoryRequest(BaseModel):
category_name: str
category_code: str
sort_no: int = 0
status: int = 1
remark: str | None = None
class CreateProductRequest(BaseModel):
product_name: str
specification: str
unit: str
category_id: int | None = None
cost_price: float = 0
sale_price: float = 0
status: int = 1
remark: str | None = None

View File

@ -0,0 +1,12 @@
from pydantic import BaseModel
class CreateSupplierRequest(BaseModel):
supplier_name: str
supplier_type: str
contact_name: str | None = None
contact_mobile: str | None = None
address: str | None = None
template_type: str | None = None
status: int = 1
remark: str | None = None

View File

@ -0,0 +1,56 @@
from pydantic import BaseModel
class CreateUserRequest(BaseModel):
username: str
password: str
real_name: str
mobile: str | None = None
role_id: int
status: int = 1
class UpdateUserRequest(BaseModel):
real_name: str
mobile: str | None = None
role_id: int
status: int = 1
class ResetPasswordRequest(BaseModel):
new_password: str
class CreateRoleRequest(BaseModel):
role_name: str
role_code: str
status: int = 1
remark: str | None = None
class UpdateRoleRequest(BaseModel):
role_name: str
status: int = 1
remark: str | None = None
class CreateMenuRequest(BaseModel):
parent_id: int = 0
menu_name: str
menu_path: str = ""
menu_type: str = "page"
permission_code: str
icon: str | None = None
sort_no: int = 0
status: int = 1
class UpdateMenuRequest(BaseModel):
parent_id: int = 0
menu_name: str
menu_path: str = ""
menu_type: str = "page"
permission_code: str
icon: str | None = None
sort_no: int = 0
status: int = 1

View File

@ -1,12 +1,117 @@
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from backend.app.core.error_codes import ErrorCode
from backend.app.core.exceptions import AppException
from backend.app.repositories.system_repository import SystemRepository
from backend.app.services.demo_store import demo_store
class AuthService:
def login(self, username: str, role_type: str) -> dict | None:
DEFAULT_ROLE_MENUS = {
"salesman": [{"menu_name": "我的订单", "menu_path": "/orders"}],
"manager": [{"menu_name": "审批中心", "menu_path": "/pages/approve-list"}],
"driver": [{"menu_name": "我的任务", "menu_path": "/pages/task-list"}],
"admin": [{"menu_name": "后台首页", "menu_path": "/dashboard"}],
}
DEFAULT_ROLE_PERMISSIONS = {
"salesman": ["order:create", "order:list", "order:submit", "customer:list"],
"manager": ["order:list", "order:approve", "order:cancel-approve", "logistics:task:create"],
"driver": ["driver:task:list", "driver:task:accept", "driver:task:pickup", "driver:task:deliver"],
"admin": ["system:user:list", "system:role:list", "system:menu:list", "order:list"],
}
def __init__(self) -> None:
self.repository = SystemRepository()
def login(self, username: str, password: str, role_type: str, session: Session | None = None) -> dict | None:
if session is not None:
try:
user = self.repository.get_user_by_username(session, username)
if user is None or user.password_hash != password:
raise AppException(
code=ErrorCode.UNAUTHORIZED,
message="账号不存在或密码错误",
status_code=401,
)
role = self.repository.get_role(session, user.role_id)
if role is None:
raise AppException(code=ErrorCode.NOT_FOUND, message="角色不存在", status_code=404)
if user.status != 1:
raise AppException(code=ErrorCode.FORBIDDEN, message="账号已停用", status_code=403)
if role.status != 1 or role.role_code != role_type:
raise AppException(code=ErrorCode.FORBIDDEN, message="登录角色与账号不匹配", status_code=403)
return self._build_profile(session, user, role, include_token=True)
except AppException:
raise
except SQLAlchemyError:
pass
return demo_store.login(username, role_type)
def get_me(self, token: str) -> dict | None:
def get_me(self, token: str, session: Session | None = None) -> dict | None:
if session is not None and token.startswith("db-user-"):
try:
user_id = int(token.split("-")[2])
user = self.repository.get_user(session, user_id)
if user is None or user.status != 1:
return None
role = self.repository.get_role(session, user.role_id)
if role is None or role.status != 1:
return None
return self._build_profile(session, user, role, include_token=False)
except (SQLAlchemyError, ValueError, IndexError):
pass
return demo_store.get_me(token)
def _build_profile(self, session: Session, user, role, include_token: bool) -> dict:
menus = self._load_role_menus(session, role.role_code)
permissions = self._load_role_permissions(session, role.role_code)
return {
"user_id": user.id,
"username": user.username,
"real_name": user.real_name,
"mobile": user.mobile,
"role_id": role.id,
"role_name": role.role_name,
"role_code": role.role_code,
"token": f"db-user-{user.id}-{role.role_code}" if include_token else None,
"menus": menus,
"permissions": permissions,
}
def _load_role_menus(self, session: Session, role_code: str) -> list[dict]:
menus = self.repository.list_menus(session, {"status": 1})
if not menus:
return self.DEFAULT_ROLE_MENUS.get(role_code, [])
if role_code == "admin":
return [{"menu_name": menu.menu_name, "menu_path": menu.menu_path} for menu in menus]
allowed_paths = {item["menu_path"] for item in self.DEFAULT_ROLE_MENUS.get(role_code, [])}
filtered = [
{"menu_name": menu.menu_name, "menu_path": menu.menu_path}
for menu in menus
if menu.menu_path in allowed_paths
]
return filtered or self.DEFAULT_ROLE_MENUS.get(role_code, [])
def _load_role_permissions(self, session: Session, role_code: str) -> list[str]:
menus = self.repository.list_menus(session, {"status": 1})
if not menus:
return self.DEFAULT_ROLE_PERMISSIONS.get(role_code, [])
if role_code == "admin":
permissions = [menu.permission_code for menu in menus if menu.permission_code]
return permissions or self.DEFAULT_ROLE_PERMISSIONS.get(role_code, [])
allowed = set(self.DEFAULT_ROLE_PERMISSIONS.get(role_code, []))
permissions = [menu.permission_code for menu in menus if menu.permission_code in allowed]
return permissions or self.DEFAULT_ROLE_PERMISSIONS.get(role_code, [])
auth_service = AuthService()

View File

@ -1,6 +1,8 @@
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from backend.app.core.error_codes import ErrorCode
from backend.app.core.exceptions import AppException
from backend.app.repositories.customer_repository import CustomerRepository
@ -60,17 +62,18 @@ class CustomerService:
def create_customer(self, session: Session | None = None, payload: dict | None = None) -> dict:
if session is not None and payload is not None:
try:
if not payload["customer_name"].strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="客户姓名不能为空", status_code=400)
if not payload["mobile"].strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="客户手机号不能为空", status_code=400)
existed = self.repository.find_by_name_and_mobile(
session,
payload["customer_name"],
payload["mobile"],
)
if existed is not None:
return {
"customer_id": existed.id,
"customer_name": existed.customer_name,
"mobile": existed.mobile,
}
raise AppException(code=ErrorCode.DUPLICATE, message="客户已存在", status_code=400)
customer = self.repository.create_customer(
session,
@ -92,6 +95,9 @@ class CustomerService:
"customer_name": customer.customer_name,
"mobile": customer.mobile,
}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
@ -116,6 +122,8 @@ class CustomerService:
"customer_type": customer.customer_type,
"salesman_id": customer.salesman_id,
"salesman_name": "",
"arrears_amount": 0,
"status": 1,
"credit_limit": float(customer.credit_limit or 0),
"remark": customer.remark,
}
@ -132,6 +140,8 @@ class CustomerService:
"customer_type": "channel",
"salesman_id": 1,
"salesman_name": "演示业务员",
"arrears_amount": 0,
"status": 1,
"credit_limit": 10000,
"remark": "演示备注",
}

View File

@ -0,0 +1,41 @@
from datetime import datetime, timedelta
from backend.app.core.error_codes import ErrorCode
from backend.app.core.exceptions import AppException
class FileService:
def __init__(self) -> None:
self.attachments: list[dict] = []
self.next_attachment_id = 8001
def create_upload_token(self, payload: dict) -> dict:
if not payload["file_name"].strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="文件名不能为空", status_code=400)
expire_at = datetime.now() + timedelta(hours=1)
return {
"upload_url": "https://oss-upload-url",
"file_url": f"https://oss-public-url/{payload['biz_type']}/{payload['biz_id']}/{payload['file_name']}",
"headers": {},
"expire_at": expire_at.strftime("%Y-%m-%d %H:%M:%S"),
}
def save_attachment(self, payload: dict) -> dict:
attachment = {
"attachment_id": self.next_attachment_id,
"biz_type": payload["biz_type"],
"biz_id": payload["biz_id"],
"file_name": payload["file_name"],
"file_url": payload["file_url"],
"file_type": payload["file_type"],
"file_size": payload["file_size"],
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
self.next_attachment_id += 1
self.attachments.append(attachment)
return attachment
file_service = FileService()

View File

@ -14,6 +14,7 @@ class LogisticsService:
def __init__(self) -> None:
self.logistics_repository = LogisticsRepository()
self.order_repository = OrderRepository()
self.trace_records: dict[int, list[dict]] = {}
def list_tasks(self, session: Session | None = None, filters: dict | None = None) -> dict:
if session is not None:
@ -88,29 +89,73 @@ class LogisticsService:
def get_driver_task(self, task_id: int, session: Session | None = None) -> dict:
return self.get_task(task_id, session=session)
def accept_task(self, task_id: int, session: Session | None = None) -> dict:
return self._change_task_status(task_id, "pending", "accepted", "accepted", session)
def accept_task(self, task_id: int, payload: dict | None = None, session: Session | None = None) -> dict:
result = self._change_task_status(task_id, "pending", "accepted", "accepted", session)
self._append_trace(
result["order_id"],
{
"task_id": task_id,
"node_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"node_desc": "司机已接单",
"node_type": "accepted",
"source_platform": "driver",
"remark": (payload or {}).get("remark"),
},
)
return result
def pickup_task(self, task_id: int, session: Session | None = None) -> dict:
return self._change_task_status(task_id, "accepted", "picked_up", "picked_up", session)
def pickup_task(self, task_id: int, payload: dict | None = None, session: Session | None = None) -> dict:
result = self._change_task_status(task_id, "accepted", "picked_up", "picked_up", session)
self._append_trace(
result["order_id"],
{
"task_id": task_id,
"node_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"node_desc": "司机已揽货",
"node_type": "picked_up",
"source_platform": "driver",
"remark": (payload or {}).get("remark"),
"photo_files": (payload or {}).get("photo_files", []),
"video_files": (payload or {}).get("video_files", []),
},
)
return result
def deliver_task(self, task_id: int, session: Session | None = None) -> dict:
return self._change_task_status(task_id, "picked_up", "delivered", "delivered", session)
def deliver_task(self, task_id: int, payload: dict | None = None, session: Session | None = None) -> dict:
result = self._change_task_status(task_id, "picked_up", "delivered", "delivered", session)
self._append_trace(
result["order_id"],
{
"task_id": task_id,
"node_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"node_desc": "司机已送达",
"node_type": "delivered",
"source_platform": "driver",
"remark": (payload or {}).get("remark"),
"photo_files": (payload or {}).get("photo_files", []),
"video_files": (payload or {}).get("video_files", []),
},
)
return result
def get_trace(self, order_id: int) -> dict:
return {
"order_id": order_id,
"trace_list": [
{
"node_time": "2026-05-14 12:00:00",
"node_desc": "司机已揽货",
"node_type": "picked_up",
"source_platform": "manual",
}
],
"trace_list": self.trace_records.get(
order_id,
[
{
"node_time": "2026-05-14 12:00:00",
"node_desc": "司机已揽货",
"node_type": "picked_up",
"source_platform": "manual",
}
],
),
}
def create_trace(self, order_id: int) -> dict:
def create_trace(self, order_id: int, payload: dict) -> dict:
self._append_trace(order_id, payload)
return {"order_id": order_id, "created": True}
def _change_task_status(
@ -134,11 +179,16 @@ class LogisticsService:
if order is not None:
self.order_repository.update_order_status(session, order, order_status)
session.commit()
return {"task_id": task.id, "status": task.status}
return {"task_id": task.id, "status": task.status, "order_id": task.order_id}
except SQLAlchemyError:
session.rollback()
return {"task_id": task_id, "status": target_status}
task = demo_store.get_logistics_task(task_id)
return {
"task_id": task_id,
"status": target_status,
"order_id": task["order_id"] if task else 0,
}
def _map_task_summary(self, task, session: Session) -> dict:
order = self.order_repository.get_order(session, task.order_id)
@ -168,6 +218,9 @@ class LogisticsService:
summary["customer_name"] = order.customer_name if order else ""
return summary
def _append_trace(self, order_id: int, payload: dict) -> None:
trace_list = self.trace_records.setdefault(order_id, [])
trace_list.append(payload)
logistics_service = LogisticsService()

View File

@ -1,5 +1,38 @@
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from backend.app.core.error_codes import ErrorCode
from backend.app.core.exceptions import AppException
from backend.app.repositories.product_repository import ProductRepository
class ProductService:
def list_categories(self) -> dict:
def __init__(self) -> None:
self.repository = ProductRepository()
def list_categories(self, session: Session | None = None, filters: dict | None = None) -> dict:
if session is not None:
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:
pass
return {
"total": 2,
"page_no": 1,
@ -10,18 +43,94 @@ class ProductService:
],
}
def create_category(self) -> dict:
def create_category(self, payload: dict, session: Session | None = None) -> dict:
if session is not None:
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)
session.commit()
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:
session.rollback()
return {
"category_id": 3,
"category_name": "新增分类",
"category_code": "new-category",
"status": 1,
"category_name": payload["category_name"],
"category_code": payload["category_code"],
"status": payload.get("status", 1),
}
def update_category(self, category_id: int) -> dict:
def update_category(self, category_id: int, payload: dict, session: Session | None = None) -> dict:
if session is not None:
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()
return {"category_id": category_id, "updated": True}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
return {"category_id": category_id, "updated": True}
def list_products(self, filters: dict | None = None) -> dict:
def list_products(self, filters: dict | None = None, session: Session | None = None) -> dict:
if session is not None:
try:
products = self.repository.list_products(session, filters or {})
categories = {
category.id: category
for category in self.repository.list_categories(session, {})
}
return {
"total": len(products),
"page_no": 1,
"page_size": len(products) or 20,
"list": [
{
"product_id": product.id,
"product_name": product.product_name,
"specification": product.specification,
"unit": product.unit,
"category_id": product.category_id,
"category_name": categories[product.category_id].category_name
if product.category_id in categories
else (product.category or ""),
"cost_price": float(product.cost_price or 0),
"sale_price": float(product.sale_price or 0),
"status": product.status,
"remark": product.remark,
}
for product in products
],
}
except SQLAlchemyError:
pass
product_list = [
{
"product_id": 2001,
@ -57,16 +166,85 @@ class ProductService:
"list": product_list,
}
def create_product(self) -> dict:
def create_product(self, payload: dict, session: Session | None = None) -> dict:
if session is not None:
try:
if not payload["product_name"].strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="产品名称不能为空", status_code=400)
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,
payload["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"])
if category is None or category.status != 1:
raise AppException(code=ErrorCode.NOT_FOUND, message="产品分类不存在或已停用", status_code=404)
product = self.repository.create_product(
session,
{
**payload,
"category": category.category_name if category else None,
},
)
session.commit()
return {
"product_id": product.id,
"product_name": product.product_name,
"specification": product.specification,
"unit": product.unit,
"category_id": product.category_id,
"status": product.status,
}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
return {
"product_id": 2003,
"product_name": "新建产品",
"specification": "30kg",
"unit": "",
"status": 1,
"product_name": payload["product_name"],
"specification": payload["specification"],
"unit": payload["unit"],
"status": payload.get("status", 1),
}
def get_product(self, product_id: int) -> dict:
def get_product(self, product_id: int, session: Session | None = None) -> dict:
if session is not None:
try:
product = self.repository.get_product(session, product_id)
if product is not None:
category_name = product.category or ""
if product.category_id is not None:
category = self.repository.get_category(session, product.category_id)
if category is not None:
category_name = category.category_name
return {
"product_id": product.id,
"product_name": product.product_name,
"specification": product.specification,
"unit": product.unit,
"category_id": product.category_id,
"category_name": category_name,
"cost_price": float(product.cost_price or 0),
"sale_price": float(product.sale_price or 0),
"status": product.status,
"remark": product.remark,
}
except SQLAlchemyError:
pass
return {
"product_id": product_id,
"product_name": "演示产品A",

View File

@ -1,5 +1,41 @@
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from backend.app.core.error_codes import ErrorCode
from backend.app.core.exceptions import AppException
from backend.app.repositories.supplier_repository import SupplierRepository
class SupplierService:
def list_suppliers(self, filters: dict | None = None) -> dict:
def __init__(self) -> None:
self.repository = SupplierRepository()
def list_suppliers(self, filters: dict | None = None, session: Session | None = None) -> dict:
if session is not None:
try:
suppliers = self.repository.list_suppliers(session, filters or {})
return {
"total": len(suppliers),
"page_no": 1,
"page_size": len(suppliers) or 20,
"list": [
{
"supplier_id": supplier.id,
"supplier_name": supplier.supplier_name,
"supplier_type": supplier.supplier_type,
"contact_name": supplier.contact_name,
"contact_mobile": supplier.contact_mobile,
"address": supplier.address,
"template_type": supplier.template_type,
"status": supplier.status,
"remark": supplier.remark,
}
for supplier in suppliers
],
}
except SQLAlchemyError:
pass
supplier_list = [
{
"supplier_id": 1001,
@ -31,12 +67,40 @@ class SupplierService:
"list": supplier_list,
}
def create_supplier(self) -> dict:
def create_supplier(self, payload: dict, session: Session | None = None) -> dict:
if session is not None:
try:
if not payload["supplier_name"].strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="供应商名称不能为空", status_code=400)
if not payload["supplier_type"].strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="供应商类型不能为空", status_code=400)
existed = self.repository.get_supplier_by_name_and_type(
session,
payload["supplier_name"],
payload["supplier_type"],
)
if existed is not None:
raise AppException(code=ErrorCode.DUPLICATE, message="供应商已存在", status_code=400)
supplier = self.repository.create_supplier(session, payload)
session.commit()
return {
"supplier_id": supplier.id,
"supplier_name": supplier.supplier_name,
"supplier_type": supplier.supplier_type,
"status": supplier.status,
}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
return {
"supplier_id": 1003,
"supplier_name": "新建供应商",
"supplier_type": "factory",
"status": 1,
"supplier_name": payload["supplier_name"],
"supplier_type": payload["supplier_type"],
"status": payload.get("status", 1),
}

View File

@ -0,0 +1,392 @@
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from backend.app.core.error_codes import ErrorCode
from backend.app.core.exceptions import AppException
from backend.app.repositories.system_repository import SystemRepository
class SystemService:
VALID_MENU_TYPES = {"catalog", "page", "button"}
def __init__(self) -> None:
self.repository = SystemRepository()
def list_users(self, session: Session | None = None, filters: dict | None = None) -> dict:
if session is not None:
try:
rows = self.repository.list_users(session, filters or {})
return {
"total": len(rows),
"page_no": 1,
"page_size": len(rows) or 20,
"list": [
self._build_user_row(user, role)
for user, role in rows
],
}
except SQLAlchemyError:
pass
return {
"total": 1,
"page_no": 1,
"page_size": 20,
"list": [
{
"user_id": 1,
"username": "sales01",
"real_name": "张三",
"mobile": "13800000000",
"role_id": 2,
"role_name": "业务员",
"role_code": "salesman",
"status": 1,
"created_at": "2026-05-14 10:00:00",
"updated_at": "2026-05-14 10:00:00",
}
],
}
def create_user(self, payload: dict, session: Session | None = None) -> dict:
self._validate_create_user(payload)
if session is not None:
try:
existed = self.repository.get_user_by_username(session, payload["username"])
if existed is not None:
raise AppException(code=ErrorCode.DUPLICATE, message="用户名已存在", status_code=400)
role = self.repository.get_role(session, payload["role_id"])
if role is None or role.status != 1:
raise AppException(code=ErrorCode.NOT_FOUND, message="角色不存在或已停用", status_code=404)
user = self.repository.create_user(
session,
{
**payload,
"password_hash": payload["password"],
},
)
session.commit()
return {
"user_id": user.id,
"username": user.username,
"real_name": user.real_name,
"mobile": user.mobile,
"role_id": user.role_id,
"status": user.status,
}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
return {
"user_id": 2,
"username": payload["username"],
"real_name": payload["real_name"],
"mobile": payload.get("mobile") or "",
"role_id": payload["role_id"],
"status": payload.get("status", 1),
}
def update_user(self, user_id: int, payload: dict, session: Session | None = None) -> dict:
self._validate_update_user(payload)
if session is not None:
try:
user = self.repository.get_user(session, user_id)
if user is None:
raise AppException(code=ErrorCode.NOT_FOUND, message="用户不存在", status_code=404)
role = self.repository.get_role(session, payload["role_id"])
if role is None or role.status != 1:
raise AppException(code=ErrorCode.NOT_FOUND, message="角色不存在或已停用", status_code=404)
self.repository.update_user(user, payload)
session.commit()
return {"user_id": user_id, "updated": True}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
return {"user_id": user_id, "updated": True}
def reset_password(self, user_id: int, new_password: str, session: Session | None = None) -> dict:
self._validate_password(new_password)
if session is not None:
try:
user = self.repository.get_user(session, user_id)
if user is None:
raise AppException(code=ErrorCode.NOT_FOUND, message="用户不存在", status_code=404)
self.repository.reset_password(user, new_password)
session.commit()
return {"user_id": user_id, "reset": True}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
return {"user_id": user_id, "reset": True}
def list_roles(self, session: Session | None = None, filters: dict | None = None) -> dict:
if session is not None:
try:
roles = self.repository.list_roles(session, filters or {})
return {
"total": len(roles),
"page_no": 1,
"page_size": len(roles) or 20,
"list": [
{
"role_id": role.id,
"role_name": role.role_name,
"role_code": role.role_code,
"status": role.status,
"remark": role.remark,
"created_at": role.created_at.strftime("%Y-%m-%d %H:%M:%S") if role.created_at else "",
}
for role in roles
],
}
except SQLAlchemyError:
pass
return {
"total": 2,
"page_no": 1,
"page_size": 20,
"list": [
{"role_id": 1, "role_name": "管理员", "role_code": "admin", "status": 1, "remark": "系统管理"},
{"role_id": 2, "role_name": "业务员", "role_code": "salesman", "status": 1, "remark": "业务录单"},
],
}
def create_role(self, payload: dict, session: Session | None = None) -> dict:
self._validate_create_role(payload)
if session is not None:
try:
existed = self.repository.get_role_by_code(session, payload["role_code"])
if existed is not None:
raise AppException(code=ErrorCode.DUPLICATE, message="角色编码已存在", status_code=400)
role = self.repository.create_role(session, payload)
session.commit()
return {
"role_id": role.id,
"role_name": role.role_name,
"role_code": role.role_code,
"status": role.status,
}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
return {
"role_id": 3,
"role_name": payload["role_name"],
"role_code": payload["role_code"],
"status": payload.get("status", 1),
}
def update_role(self, role_id: int, payload: dict, session: Session | None = None) -> dict:
self._validate_update_role(payload)
if session is not None:
try:
role = self.repository.get_role(session, role_id)
if role is None:
raise AppException(code=ErrorCode.NOT_FOUND, message="角色不存在", status_code=404)
if role.role_code == "admin" and payload.get("status", 1) != 1:
raise AppException(code=ErrorCode.BUSINESS_RULE_FAILED, message="管理员角色不能停用", status_code=400)
if payload.get("status", 1) != 1 and self.repository.count_active_users_by_role(session, role_id) > 0:
raise AppException(code=ErrorCode.BUSINESS_RULE_FAILED, message="当前角色下仍有启用用户", status_code=400)
self.repository.update_role(role, payload)
session.commit()
return {"role_id": role_id, "updated": True}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
return {"role_id": role_id, "updated": True}
def list_menus(self, session: Session | None = None, filters: dict | None = None) -> list[dict]:
if session is not None:
try:
menus = self.repository.list_menus(session, filters or {})
if menus:
return self._build_menu_tree(
[
{
"menu_id": menu.id,
"parent_id": menu.parent_id,
"menu_name": menu.menu_name,
"menu_path": menu.menu_path,
"menu_type": menu.menu_type,
"permission_code": menu.permission_code,
"icon": menu.icon,
"sort_no": menu.sort_no,
"status": menu.status,
}
for menu in menus
]
)
except SQLAlchemyError:
pass
return [
{
"menu_id": 1,
"parent_id": 0,
"menu_name": "订单管理",
"menu_path": "/orders",
"menu_type": "page",
"permission_code": "order:list",
"icon": "order",
"sort_no": 10,
"status": 1,
"children": [],
}
]
def create_menu(self, payload: dict, session: Session | None = None) -> dict:
self._validate_menu_payload(payload)
if session is not None:
try:
self._validate_menu_parent(session, payload["parent_id"], None)
existed = self.repository.get_menu_by_permission_code(session, payload["permission_code"])
if existed is not None:
raise AppException(code=ErrorCode.DUPLICATE, message="权限编码已存在", status_code=400)
menu = self.repository.create_menu(session, payload)
session.commit()
return {
"menu_id": menu.id,
"menu_name": menu.menu_name,
"permission_code": menu.permission_code,
"status": menu.status,
}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
return {
"menu_id": 2,
"menu_name": payload["menu_name"],
"permission_code": payload["permission_code"],
"status": payload.get("status", 1),
}
def update_menu(self, menu_id: int, payload: dict, session: Session | None = None) -> dict:
self._validate_menu_payload(payload)
if session is not None:
try:
menu = self.repository.get_menu(session, menu_id)
if menu is None:
raise AppException(code=ErrorCode.NOT_FOUND, message="菜单不存在", status_code=404)
self._validate_menu_parent(session, payload["parent_id"], menu_id)
existed = self.repository.get_menu_by_permission_code(session, payload["permission_code"])
if existed is not None and existed.id != menu_id:
raise AppException(code=ErrorCode.DUPLICATE, message="权限编码已存在", status_code=400)
self.repository.update_menu(menu, payload)
session.commit()
return {"menu_id": menu_id, "updated": True}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
return {"menu_id": menu_id, "updated": True}
def _build_user_row(self, user: object, role: object | None) -> dict:
return {
"user_id": user.id,
"username": user.username,
"real_name": user.real_name,
"mobile": user.mobile,
"role_id": user.role_id,
"role_name": role.role_name if role else "",
"role_code": role.role_code if role else "",
"status": user.status,
"created_at": user.created_at.strftime("%Y-%m-%d %H:%M:%S") if user.created_at else "",
"updated_at": user.updated_at.strftime("%Y-%m-%d %H:%M:%S") if user.updated_at else "",
}
def _build_menu_tree(self, menu_list: list[dict]) -> list[dict]:
nodes = {item["menu_id"]: {**item, "children": []} for item in menu_list}
roots: list[dict] = []
for item in nodes.values():
parent_id = item["parent_id"]
if parent_id and parent_id in nodes:
nodes[parent_id]["children"].append(item)
else:
roots.append(item)
return roots
def _validate_create_user(self, payload: dict) -> None:
if not payload["username"].strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="用户名不能为空", status_code=400)
self._validate_password(payload["password"])
self._validate_update_user(payload)
def _validate_update_user(self, payload: dict) -> None:
if not payload["real_name"].strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="姓名不能为空", status_code=400)
if not payload.get("role_id"):
raise AppException(code=ErrorCode.PARAM_ERROR, message="角色不能为空", status_code=400)
def _validate_password(self, password: str) -> None:
if len(password.strip()) < 6:
raise AppException(code=ErrorCode.PARAM_ERROR, message="密码长度不能少于6位", status_code=400)
def _validate_create_role(self, payload: dict) -> None:
if not payload["role_name"].strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="角色名称不能为空", status_code=400)
if not payload["role_code"].strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="角色编码不能为空", status_code=400)
def _validate_update_role(self, payload: dict) -> None:
if not payload["role_name"].strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="角色名称不能为空", status_code=400)
def _validate_menu_payload(self, payload: dict) -> None:
if not payload["menu_name"].strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="菜单名称不能为空", status_code=400)
if not payload["permission_code"].strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="权限编码不能为空", status_code=400)
if payload["menu_type"] not in self.VALID_MENU_TYPES:
raise AppException(code=ErrorCode.PARAM_ERROR, message="菜单类型不合法", status_code=400)
def _validate_menu_parent(self, session: Session, parent_id: int, menu_id: int | None) -> None:
if not parent_id:
return
if menu_id is not None and parent_id == menu_id:
raise AppException(code=ErrorCode.BUSINESS_RULE_FAILED, message="父级菜单不能为自身", status_code=400)
parent_menu = self.repository.get_menu(session, parent_id)
if parent_menu is None:
raise AppException(code=ErrorCode.NOT_FOUND, message="父级菜单不存在", status_code=404)
system_service = SystemService()

View File

@ -16,6 +16,8 @@ Page({
actionLoading: false,
taskId: null,
taskInfo: null,
actionRemark: "",
traceList: [],
},
async onLoad(options) {
@ -39,6 +41,15 @@ Page({
statusText: mapTaskStatus(data.status),
},
});
if (data.order_id) {
const traceData = await app.request({
url: `/api/logistics/${data.order_id}/trace`,
method: "GET",
});
this.setData({
traceList: traceData.trace_list || [],
});
}
} catch (error) {
this.setData({
message: error.message || "加载任务详情失败",
@ -48,6 +59,12 @@ Page({
}
},
handleRemarkInput(event) {
this.setData({
actionRemark: event.detail.value,
});
},
getActionConfig() {
const status = this.data.taskInfo?.status;
if (status === "pending") {
@ -86,10 +103,23 @@ Page({
await app.request({
url: action.url,
method: "POST",
data: {
photo_files: this.data.taskInfo?.status === "accepted" || this.data.taskInfo?.status === "picked_up"
? [
{
file_url: "https://oss.example.com/demo-photo.jpg",
file_name: "demo-photo.jpg",
},
]
: [],
video_files: [],
remark: this.data.actionRemark || action.successMessage,
},
});
await this.loadDetail();
this.setData({
message: action.successMessage,
actionRemark: "",
});
} catch (error) {
this.setData({

View File

@ -16,9 +16,22 @@
<view>业务员:{{taskInfo.salesman_name || "-"}}</view>
<view>备注:{{taskInfo.remark || "-"}}</view>
</view>
<view wx:if="{{taskInfo}}" class="card">
<view>操作备注</view>
<textarea class="textarea" placeholder="请输入接单、揽货或送达备注" value="{{actionRemark}}" bindinput="handleRemarkInput"></textarea>
</view>
<view wx:if="{{taskInfo}}" class="actions">
<button size="mini" type="primary" loading="{{actionLoading}}" disabled="{{taskInfo.status === 'delivered' || taskInfo.status === 'canceled'}}" bindtap="handleAction">
{{ taskInfo.status === 'pending' ? '接单' : (taskInfo.status === 'accepted' ? '确认揽货' : (taskInfo.status === 'picked_up' ? '确认送达' : '已完成')) }}
</button>
</view>
<view wx:if="{{traceList.length}}" class="card">
<view>物流轨迹</view>
<view wx:for="{{traceList}}" wx:key="node_time" class="trace-row">
<view>时间:{{item.node_time}}</view>
<view>节点:{{item.node_desc}}</view>
<view>来源:{{item.source_platform}}</view>
<view wx:if="{{item.remark}}">备注:{{item.remark}}</view>
</view>
</view>
</view>

View File

@ -4,3 +4,5 @@
.card { margin-top: 20rpx; padding: 20rpx; background: #ffffff; border-radius: 12rpx; display: grid; gap: 10rpx; }
.actions { display: flex; gap: 16rpx; margin-top: 20rpx; }
.message { margin-top: 20rpx; color: #047857; font-size: 26rpx; }
.textarea { width: 100%; min-height: 160rpx; border: 1rpx solid #d1d5db; border-radius: 12rpx; padding: 20rpx; box-sizing: border-box; background: #ffffff; }
.trace-row { padding-top: 10rpx; border-top: 1rpx solid #e5e7eb; display: grid; gap: 6rpx; }

View File

@ -1,5 +1,48 @@
App({
globalData: {
appName: "mini-manager"
appName: "mini-manager",
apiBaseUrl: "http://127.0.0.1:8000",
authToken: "",
managerUser: null,
},
request(options) {
const token = this.globalData.authToken;
return new Promise((resolve, reject) => {
wx.request({
...options,
url: `${this.globalData.apiBaseUrl}${options.url}`,
header: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(options.header || {}),
},
success: (response) => {
const result = response.data || {};
if (response.statusCode >= 400 || result.code !== 0) {
reject(new Error(result.message || `请求失败:${response.statusCode}`));
return;
}
resolve(result.data);
},
fail: reject,
});
});
},
mapOrderStatus(status) {
const statusMap = {
pending_approve: "待审批",
cancel_pending: "待取消审批",
approved: "已审批",
rejected: "已驳回",
pending_factory: "待发厂",
pending_driver: "待司机接单",
accepted: "已接单",
picked_up: "已揽货",
delivered: "已送达",
canceled: "已取消",
};
return statusMap[status] || status || "-";
}
});

View File

@ -1 +1,97 @@
Page({});
function mapOrderStatus(status) {
const statusMap = {
pending_approve: "待审批",
cancel_pending: "待取消审批",
approved: "已审批",
rejected: "已驳回",
canceled: "已取消",
};
return statusMap[status] || status || "-";
}
Page({
data: {
taskId: null,
loading: true,
actionLoading: false,
message: "",
approveOpinion: "",
orderInfo: null,
},
async onLoad(options) {
this.setData({
taskId: Number(options.id || 0),
});
await this.loadDetail();
},
async loadDetail() {
const app = getApp();
this.setData({ loading: true, message: "" });
try {
const data = await app.request({
url: `/api/orders/${this.data.taskId}`,
method: "GET",
});
this.setData({
orderInfo: {
...data,
statusText: mapOrderStatus(data.order_status),
salePriceTotalText: Number(data.sale_price_total || 0).toFixed(2),
costPriceTotalText: Number(data.cost_price_total || 0).toFixed(2),
rebateTotalText: Number(data.rebate_total || 0).toFixed(2),
freightTotalText: Number(data.freight_total || 0).toFixed(2),
profitTotalText: Number(data.profit_total || 0).toFixed(2),
},
});
} catch (error) {
this.setData({
message: error.message || "加载审批详情失败",
});
} finally {
this.setData({ loading: false });
}
},
handleOpinionInput(event) {
this.setData({
approveOpinion: event.detail.value,
});
},
async handleApprove(event) {
const { result } = event.currentTarget.dataset;
const app = getApp();
const orderInfo = this.data.orderInfo;
if (!orderInfo) {
return;
}
const targetUrl = orderInfo.order_status === "cancel_pending"
? `/api/orders/${orderInfo.order_id}/cancel-approve`
: `/api/orders/${orderInfo.order_id}/approve`;
this.setData({ actionLoading: true, message: "" });
try {
await app.request({
url: targetUrl,
method: "POST",
data: {
approve_result: result,
approve_opinion: this.data.approveOpinion || (result === "pass" ? "同意" : "退回"),
},
});
await this.loadDetail();
this.setData({
message: result === "pass" ? "审批处理成功" : "已完成退回处理",
});
} catch (error) {
this.setData({
message: error.message || "审批处理失败",
});
} finally {
this.setData({ actionLoading: false });
}
},
});

View File

@ -1,12 +1,29 @@
<view class="page">
<view class="title">审批详情页</view>
<view class="desc">订单信息、客户信息、明细、利润计算、审批记录</view>
<view class="card">
<view>销售额100.00</view>
<view>成本60.00</view>
<view>回扣5.00</view>
<view>运费10.00</view>
<view>利润20.00</view>
<view>操作:通过 / 退回</view>
<view class="desc">查看订单信息和利润后,直接完成审批处理。</view>
<view wx:if="{{message}}" class="message">{{message}}</view>
<view wx:if="{{loading}}" class="desc">正在加载审批详情...</view>
<view wx:elif="{{orderInfo}}" class="card">
<view>订单编号:{{orderInfo.order_no}}</view>
<view>客户:{{orderInfo.customer_name}}</view>
<view>手机号:{{orderInfo.customer_mobile}}</view>
<view>审批类型:{{orderInfo.statusText}}</view>
<view>销售额:{{orderInfo.salePriceTotalText}}</view>
<view>成本:{{orderInfo.costPriceTotalText}}</view>
<view>回扣:{{orderInfo.rebateTotalText}}</view>
<view>运费:{{orderInfo.freightTotalText}}</view>
<view>利润:{{orderInfo.profitTotalText}}</view>
<view>备注:{{orderInfo.remark || "-"}}</view>
</view>
<view wx:if="{{orderInfo}}" class="card">
<textarea class="textarea" placeholder="请输入审批意见" bindinput="handleOpinionInput" value="{{approveOpinion}}"></textarea>
<view class="actions">
<button size="mini" type="primary" loading="{{actionLoading}}" data-result="pass" bindtap="handleApprove">
{{ orderInfo.order_status === 'cancel_pending' ? '同意取消' : '审批通过' }}
</button>
<button size="mini" loading="{{actionLoading}}" data-result="reject" bindtap="handleApprove">
{{ orderInfo.order_status === 'cancel_pending' ? '驳回取消' : '退回订单' }}
</button>
</view>
</view>
</view>

View File

@ -2,3 +2,6 @@
.title { font-size: 36rpx; font-weight: 600; margin-bottom: 16rpx; }
.desc { font-size: 28rpx; color: #4b5563; }
.card { margin-top: 20rpx; padding: 20rpx; background: #ffffff; border-radius: 12rpx; display: grid; gap: 10rpx; }
.message { margin-top: 20rpx; color: #047857; font-size: 26rpx; }
.textarea { width: 100%; min-height: 160rpx; border: 1rpx solid #d1d5db; border-radius: 12rpx; padding: 20rpx; box-sizing: border-box; background: #ffffff; }
.actions { display: flex; gap: 16rpx; margin-top: 10rpx; }

View File

@ -1 +1,71 @@
Page({});
function mapOrderStatus(status) {
const statusMap = {
pending_approve: "待审批",
cancel_pending: "待取消审批",
approved: "已审批",
rejected: "已驳回",
canceled: "已取消",
};
return statusMap[status] || status || "-";
}
Page({
data: {
loading: true,
message: "",
approveType: "all",
orderRows: [],
},
async onShow() {
await this.loadOrders();
},
async loadOrders() {
const app = getApp();
this.setData({ loading: true, message: "" });
try {
const statuses = this.data.approveType === "cancel" ? ["cancel_pending"] :
(this.data.approveType === "order" ? ["pending_approve"] : ["pending_approve", "cancel_pending"]);
const requestList = statuses.map((status) =>
app.request({
url: `/api/orders?order_status=${status}&page_no=1&page_size=50`,
method: "GET",
})
);
const responseList = await Promise.all(requestList);
const rows = responseList.flatMap((data) => data.list || []).map((item) => ({
orderId: item.order_id,
orderNo: item.order_no,
customerName: item.customer_name,
customerMobile: item.customer_mobile,
orderStatus: item.order_status,
statusText: mapOrderStatus(item.order_status),
amount: Number(item.sale_price_total || 0).toFixed(2),
}));
this.setData({
orderRows: rows,
});
} catch (error) {
this.setData({
message: error.message || "加载待审批订单失败",
});
} finally {
this.setData({ loading: false });
}
},
handleChangeType(event) {
this.setData({
approveType: event.currentTarget.dataset.type,
});
this.loadOrders();
},
handleOpenDetail(event) {
const { id } = event.currentTarget.dataset;
wx.navigateTo({
url: `/pages/approve-detail?id=${id}`,
});
},
});

View File

@ -1,11 +1,23 @@
<view class="page">
<view class="title">审批首页 / 待审批列表</view>
<view class="desc">筛选:客户姓名、业务员、时间</view>
<view class="card">
<view>订单编号SO202605140001</view>
<view>客户:李四</view>
<view>业务员:张三</view>
<view>销售金额100.00</view>
<view>利润预览20.00</view>
<view class="desc">切换待订单审批或待取消审批,并进入详情处理。</view>
<view class="tabs">
<button size="mini" data-type="all" bindtap="handleChangeType" type="{{approveType === 'all' ? 'primary' : 'default'}}">全部待办</button>
<button size="mini" data-type="order" bindtap="handleChangeType" type="{{approveType === 'order' ? 'primary' : 'default'}}">待订单审批</button>
<button size="mini" data-type="cancel" bindtap="handleChangeType" type="{{approveType === 'cancel' ? 'primary' : 'default'}}">待取消审批</button>
</view>
<view wx:if="{{message}}" class="message">{{message}}</view>
<view wx:if="{{loading}}" class="desc">正在加载审批待办...</view>
<view wx:elif="{{!orderRows.length}}" class="card">
<view>当前没有待审批订单</view>
</view>
<view wx:else>
<view class="card" wx:for="{{orderRows}}" wx:key="orderId" data-id="{{item.orderId}}" bindtap="handleOpenDetail">
<view>订单编号:{{item.orderNo}}</view>
<view>客户:{{item.customerName}}</view>
<view>手机号:{{item.customerMobile}}</view>
<view>审批类型:{{item.statusText}}</view>
<view>销售金额:{{item.amount}}</view>
</view>
</view>
</view>

View File

@ -2,3 +2,5 @@
.title { font-size: 36rpx; font-weight: 600; margin-bottom: 16rpx; }
.desc { font-size: 28rpx; color: #4b5563; }
.card { margin-top: 20rpx; padding: 20rpx; background: #ffffff; border-radius: 12rpx; display: grid; gap: 10rpx; }
.tabs { display: flex; gap: 16rpx; margin-top: 20rpx; flex-wrap: wrap; }
.message { margin-top: 20rpx; color: #047857; font-size: 26rpx; }

View File

@ -1 +1,78 @@
Page({});
Page({
data: {
loading: true,
message: "",
summary: {
pendingApproveCount: 0,
pendingFactoryCount: 0,
pendingDriverCount: 0,
},
recentOrders: [],
},
async onShow() {
await this.loadDashboard();
},
async loadDashboard() {
const app = getApp();
this.setData({ loading: true, message: "" });
try {
const statuses = ["pending_approve", "pending_factory", "pending_driver"];
const responseList = await Promise.all(
statuses.map((status) =>
app.request({
url: `/api/orders?order_status=${status}&page_no=1&page_size=20`,
method: "GET",
})
)
);
const [pendingApproveData, pendingFactoryData, pendingDriverData] = responseList;
const recentOrders = [
...(pendingApproveData.list || []),
...(pendingFactoryData.list || []),
...(pendingDriverData.list || []),
].slice(0, 8).map((item) => ({
orderId: item.order_id,
orderNo: item.order_no,
customerName: item.customer_name,
statusText: app.mapOrderStatus(item.order_status),
amount: Number(item.sale_price_total || 0).toFixed(2),
}));
this.setData({
summary: {
pendingApproveCount: (pendingApproveData.list || []).length,
pendingFactoryCount: (pendingFactoryData.list || []).length,
pendingDriverCount: (pendingDriverData.list || []).length,
},
recentOrders,
});
} catch (error) {
this.setData({
message: error.message || "加载经营概览失败",
});
} finally {
this.setData({ loading: false });
}
},
goApproveList() {
wx.navigateTo({
url: "/pages/approve-list",
});
},
goSupplierText() {
wx.navigateTo({
url: "/pages/supplier-text",
});
},
goTaskCreate() {
wx.navigateTo({
url: "/pages/task-create",
});
},
});

View File

@ -1,9 +1,24 @@
<view class="page">
<view class="title">经营数据概览</view>
<view class="card">
<view>订单数10</view>
<view>订单金额10000.00</view>
<view>分类金额:工业品 6000.00</view>
<view>固定提成汇总500.00</view>
<view wx:if="{{message}}" class="message">{{message}}</view>
<view wx:if="{{loading}}" class="desc">正在加载经营概览...</view>
<view wx:else class="card">
<view>待审批订单:{{summary.pendingApproveCount}}</view>
<view>待发厂订单:{{summary.pendingFactoryCount}}</view>
<view>待司机接单:{{summary.pendingDriverCount}}</view>
</view>
<view wx:if="{{!loading}}" class="actions">
<button size="mini" type="primary" bindtap="goApproveList">进入审批列表</button>
<button size="mini" bindtap="goSupplierText">进入发厂确认</button>
<button size="mini" bindtap="goTaskCreate">进入任务分配</button>
</view>
<view wx:if="{{!loading && recentOrders.length}}" class="card">
<view class="title-small">最近待办订单</view>
<view wx:for="{{recentOrders}}" wx:key="orderId" class="item-row">
<view>订单:{{item.orderNo}}</view>
<view>客户:{{item.customerName}}</view>
<view>状态:{{item.statusText}}</view>
<view>金额:{{item.amount}}</view>
</view>
</view>
</view>

View File

@ -2,3 +2,7 @@
.title { font-size: 36rpx; font-weight: 600; margin-bottom: 16rpx; }
.desc { font-size: 28rpx; color: #4b5563; }
.card { margin-top: 20rpx; padding: 20rpx; background: #ffffff; border-radius: 12rpx; display: grid; gap: 10rpx; }
.message { margin-top: 20rpx; color: #047857; font-size: 26rpx; }
.actions { display: flex; gap: 16rpx; margin-top: 20rpx; flex-wrap: wrap; }
.title-small { font-size: 30rpx; font-weight: 600; margin-bottom: 10rpx; }
.item-row { padding-top: 10rpx; border-top: 1rpx solid #e5e7eb; display: grid; gap: 6rpx; }

View File

@ -1,5 +1,45 @@
Page({
data: {
title: "登录页"
}
username: "admin01",
password: "123456",
loading: false,
message: "",
},
handleInput(event) {
const { field } = event.currentTarget.dataset;
this.setData({
[field]: event.detail.value,
});
},
async handleLogin() {
const app = getApp();
this.setData({ loading: true, message: "" });
try {
const data = await app.request({
url: "/api/auth/login",
method: "POST",
data: {
username: this.data.username,
password: this.data.password,
role_type: "admin",
},
});
app.globalData.authToken = data.token;
app.globalData.managerUser = data;
this.setData({
message: `欢迎,${data.real_name || data.username}`,
});
wx.navigateTo({
url: "/pages/approve-list",
});
} catch (error) {
this.setData({
message: error.message || "管理层登录失败",
});
} finally {
this.setData({ loading: false });
}
},
});

View File

@ -1,4 +1,12 @@
<view class="page">
<view class="title">登录页</view>
<view class="desc">预留 username、password、role_type=manager 登录入口。</view>
<view class="title">管理层登录</view>
<view class="desc">登录后进入待审批列表并处理订单审批。</view>
<view class="card">
<input class="input" value="{{username}}" data-field="username" bindinput="handleInput" placeholder="请输入管理账号" />
<input class="input" password value="{{password}}" data-field="password" bindinput="handleInput" placeholder="请输入密码" />
<button type="primary" loading="{{loading}}" bindtap="handleLogin">
{{ loading ? "登录中..." : "登录管理端" }}
</button>
<view wx:if="{{message}}" class="message">{{message}}</view>
</view>
</view>

View File

@ -1,3 +1,6 @@
.page { padding: 24rpx; }
.title { font-size: 36rpx; font-weight: 600; margin-bottom: 16rpx; }
.desc { font-size: 28rpx; color: #4b5563; }
.card { margin-top: 20rpx; padding: 20rpx; background: #ffffff; border-radius: 12rpx; display: grid; gap: 16rpx; }
.input { border: 1rpx solid #d1d5db; border-radius: 12rpx; padding: 20rpx; background: #ffffff; }
.message { color: #047857; font-size: 26rpx; }

View File

@ -1 +1,118 @@
Page({});
Page({
data: {
loading: true,
actionLoading: false,
message: "",
orderRows: [],
selectedOrderId: null,
selectedOrderNo: "",
textContent: "",
templateType: "default",
},
async onShow() {
await this.loadOrders();
},
async loadOrders() {
const app = getApp();
this.setData({ loading: true, message: "" });
try {
const [approvedData, pendingFactoryData] = await Promise.all([
app.request({
url: "/api/orders?order_status=approved&page_no=1&page_size=50",
method: "GET",
}),
app.request({
url: "/api/orders?order_status=pending_factory&page_no=1&page_size=50",
method: "GET",
}),
]);
const orderRows = [...(approvedData.list || []), ...(pendingFactoryData.list || [])].map((item) => ({
orderId: item.order_id,
orderNo: item.order_no,
customerName: item.customer_name,
statusText: app.mapOrderStatus(item.order_status),
}));
this.setData({ orderRows });
} catch (error) {
this.setData({
message: error.message || "加载待发厂订单失败",
});
} finally {
this.setData({ loading: false });
}
},
handleChooseOrder(event) {
const { id, no } = event.currentTarget.dataset;
this.setData({
selectedOrderId: id,
selectedOrderNo: no,
textContent: "",
});
},
handleTextInput(event) {
this.setData({
textContent: event.detail.value,
});
},
async handleGenerateText() {
if (!this.data.selectedOrderId) {
this.setData({ message: "请先选择一笔订单" });
return;
}
const app = getApp();
this.setData({ actionLoading: true, message: "" });
try {
const data = await app.request({
url: `/api/orders/${this.data.selectedOrderId}/supplier-text`,
method: "POST",
data: { supplier_id: 1001 },
});
this.setData({
textContent: data.text_content,
templateType: data.template_type,
message: "发厂文案已生成",
});
} catch (error) {
this.setData({
message: error.message || "生成发厂文案失败",
});
} finally {
this.setData({ actionLoading: false });
}
},
async handleConfirmText() {
if (!this.data.selectedOrderId) {
this.setData({ message: "请先选择一笔订单" });
return;
}
const app = getApp();
this.setData({ actionLoading: true, message: "" });
try {
await app.request({
url: `/api/orders/${this.data.selectedOrderId}/supplier-text/confirm`,
method: "POST",
data: {
supplier_id: 1001,
text_content: this.data.textContent,
remark: "微信管理层端确认发厂",
},
});
this.setData({
message: `订单 ${this.data.selectedOrderNo} 已确认下发工厂`,
});
await this.loadOrders();
} catch (error) {
this.setData({
message: error.message || "确认发厂失败",
});
} finally {
this.setData({ actionLoading: false });
}
},
});

View File

@ -1,9 +1,23 @@
<view class="page">
<view class="title">下发工厂文本页</view>
<view class="desc">订单编号、工厂、模板类型、文本内容</view>
<view class="desc">选择订单后生成文案,并直接确认下发工厂。</view>
<view wx:if="{{message}}" class="message">{{message}}</view>
<view wx:if="{{loading}}" class="desc">正在加载待发厂订单...</view>
<view wx:else class="card">
<view class="title-small">待发厂订单</view>
<view wx:for="{{orderRows}}" wx:key="orderId" class="item-row" data-id="{{item.orderId}}" data-no="{{item.orderNo}}" bindtap="handleChooseOrder">
<view>订单编号:{{item.orderNo}}</view>
<view>客户:{{item.customerName}}</view>
<view>状态:{{item.statusText}}</view>
</view>
</view>
<view class="card">
<view>模板类型default</view>
<view>文本内容:订单下发文本示例</view>
<view>操作:生成文本 / 复制文本 / 确认下发</view>
<view>已选订单:{{selectedOrderNo || "未选择"}}</view>
<view>模板类型:{{templateType}}</view>
<textarea class="textarea" placeholder="点击生成文案后可查看并编辑文本内容" value="{{textContent}}" bindinput="handleTextInput"></textarea>
</view>
<view class="actions">
<button size="mini" type="primary" loading="{{actionLoading}}" bindtap="handleGenerateText">生成文案</button>
<button size="mini" loading="{{actionLoading}}" bindtap="handleConfirmText">确认下发</button>
</view>
</view>

View File

@ -2,3 +2,8 @@
.title { font-size: 36rpx; font-weight: 600; margin-bottom: 16rpx; }
.desc { font-size: 28rpx; color: #4b5563; }
.card { margin-top: 20rpx; padding: 20rpx; background: #ffffff; border-radius: 12rpx; display: grid; gap: 10rpx; }
.message { margin-top: 20rpx; color: #047857; font-size: 26rpx; }
.title-small { font-size: 30rpx; font-weight: 600; margin-bottom: 10rpx; }
.item-row { padding-top: 10rpx; border-top: 1rpx solid #e5e7eb; display: grid; gap: 6rpx; }
.textarea { width: 100%; min-height: 180rpx; border: 1rpx solid #d1d5db; border-radius: 12rpx; padding: 20rpx; box-sizing: border-box; background: #ffffff; }
.actions { display: flex; gap: 16rpx; margin-top: 20rpx; }

View File

@ -1 +1,102 @@
Page({});
Page({
data: {
loading: true,
saving: false,
message: "",
orderRows: [],
selectedOrderId: null,
selectedOrderNo: "",
driverId: 10,
pickupAddress: "",
deliveryAddress: "",
pickupContent: "",
quantity: 1,
factoryId: 1001,
remark: "微信管理层端创建司机任务",
},
async onShow() {
await this.loadOrders();
},
async loadOrders() {
const app = getApp();
this.setData({ loading: true, message: "" });
try {
const data = await app.request({
url: "/api/orders?order_status=pending_factory&page_no=1&page_size=50",
method: "GET",
});
this.setData({
orderRows: (data.list || []).map((item) => ({
orderId: item.order_id,
orderNo: item.order_no,
customerName: item.customer_name,
statusText: app.mapOrderStatus(item.order_status),
})),
});
} catch (error) {
this.setData({
message: error.message || "加载待分配订单失败",
});
} finally {
this.setData({ loading: false });
}
},
handleChooseOrder(event) {
const { id, no, customer } = event.currentTarget.dataset;
this.setData({
selectedOrderId: id,
selectedOrderNo: no,
pickupAddress: "工厂A",
deliveryAddress: `${customer} 收货地址`,
pickupContent: `${customer} 订单货物`,
quantity: 1,
});
},
handleInput(event) {
const { field } = event.currentTarget.dataset;
this.setData({
[field]: event.detail.value,
});
},
async handleSave() {
if (!this.data.selectedOrderId) {
this.setData({ message: "请先选择一笔订单" });
return;
}
const app = getApp();
this.setData({ saving: true, message: "" });
try {
const data = await app.request({
url: "/api/logistics/tasks",
method: "POST",
data: {
order_id: this.data.selectedOrderId,
driver_id: Number(this.data.driverId),
pickup_address: this.data.pickupAddress,
delivery_address: this.data.deliveryAddress,
pickup_content: this.data.pickupContent,
quantity: Number(this.data.quantity),
factory_id: Number(this.data.factoryId),
remark: this.data.remark,
},
});
this.setData({
message: `司机任务 ${data.task_no} 创建成功`,
selectedOrderId: null,
selectedOrderNo: "",
});
await this.loadOrders();
} catch (error) {
this.setData({
message: error.message || "创建司机任务失败",
});
} finally {
this.setData({ saving: false });
}
},
});

View File

@ -1,12 +1,27 @@
<view class="page">
<view class="title">司机任务分配页</view>
<view class="desc">字段order_id、driver_id、pickup_address、delivery_address、pickup_content、quantity、factory_id、remark</view>
<view class="desc">选择待发厂订单并直接下发司机任务。</view>
<view wx:if="{{message}}" class="message">{{message}}</view>
<view wx:if="{{loading}}" class="desc">正在加载待分配订单...</view>
<view wx:else class="card">
<view class="title-small">待分配订单</view>
<view wx:for="{{orderRows}}" wx:key="orderId" class="item-row" data-id="{{item.orderId}}" data-no="{{item.orderNo}}" data-customer="{{item.customerName}}" bindtap="handleChooseOrder">
<view>订单编号:{{item.orderNo}}</view>
<view>客户:{{item.customerName}}</view>
<view>状态:{{item.statusText}}</view>
</view>
</view>
<view class="card">
<view>操作:保存 / 取消</view>
<view>权限:管理层、管理员</view>
<view>已选订单:{{selectedOrderNo || "未选择"}}</view>
<input class="input" value="{{driverId}}" data-field="driverId" bindinput="handleInput" placeholder="司机ID" />
<input class="input" value="{{factoryId}}" data-field="factoryId" bindinput="handleInput" placeholder="工厂ID" />
<input class="input" value="{{pickupAddress}}" data-field="pickupAddress" bindinput="handleInput" placeholder="取货地址" />
<input class="input" value="{{deliveryAddress}}" data-field="deliveryAddress" bindinput="handleInput" placeholder="送达地址" />
<input class="input" value="{{pickupContent}}" data-field="pickupContent" bindinput="handleInput" placeholder="提货内容" />
<input class="input" value="{{quantity}}" data-field="quantity" bindinput="handleInput" placeholder="数量" />
<textarea class="textarea" value="{{remark}}" data-field="remark" bindinput="handleInput" placeholder="备注"></textarea>
</view>
<view class="actions">
<button size="mini">保存</button>
<button size="mini">取消</button>
<button size="mini" type="primary" loading="{{saving}}" bindtap="handleSave">保存任务</button>
</view>
</view>

View File

@ -3,3 +3,8 @@
.desc { font-size: 28rpx; color: #4b5563; }
.card { margin-top: 20rpx; padding: 20rpx; background: #ffffff; border-radius: 12rpx; display: grid; gap: 10rpx; }
.actions { display: flex; gap: 16rpx; margin-top: 20rpx; }
.message { margin-top: 20rpx; color: #047857; font-size: 26rpx; }
.title-small { font-size: 30rpx; font-weight: 600; margin-bottom: 10rpx; }
.item-row { padding-top: 10rpx; border-top: 1rpx solid #e5e7eb; display: grid; gap: 6rpx; }
.input { border: 1rpx solid #d1d5db; border-radius: 12rpx; padding: 20rpx; background: #ffffff; }
.textarea { width: 100%; min-height: 160rpx; border: 1rpx solid #d1d5db; border-radius: 12rpx; padding: 20rpx; box-sizing: border-box; background: #ffffff; }

View File

@ -98,6 +98,437 @@ export function fetchMasterData() {
return wait({ modules: masterDataModules, customers: customerRows });
}
export async function fetchSystemUsers(filters = {}) {
try {
const query = buildQuery({
username: filters.username?.trim() || undefined,
real_name: filters.real_name?.trim() || undefined,
mobile: filters.mobile?.trim() || undefined,
role_id: filters.role_id || undefined,
status: filters.status || undefined,
page_no: 1,
page_size: 50,
});
const data = await request(`/api/system/users?${query}`);
return {
rows: (data.list || []).map((item) => ({
userId: item.user_id,
username: item.username,
realName: item.real_name,
mobile: item.mobile || "-",
roleId: item.role_id,
roleName: item.role_name || "-",
roleCode: item.role_code || "-",
status: Number(item.status || 1) === 1 ? "启用" : "停用",
createdAt: item.created_at || "-",
updatedAt: item.updated_at || "-",
})),
isMock: false,
};
} catch (error) {
return wait({
rows: [
{
userId: 1,
username: "sales01",
realName: "张三",
mobile: "13800000000",
roleId: 2,
roleName: "业务员",
roleCode: "salesman",
status: "启用",
createdAt: "2026-05-14 10:00:00",
updatedAt: "2026-05-14 10:00:00",
},
],
isMock: true,
});
}
}
export async function createSystemUser(payload) {
return request("/api/system/users", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function updateSystemUser(userId, payload) {
return request(`/api/system/users/${userId}`, {
method: "PUT",
body: JSON.stringify(payload),
});
}
export async function resetSystemUserPassword(userId, payload) {
return request(`/api/system/users/${userId}/reset-password`, {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function fetchSystemRoles(filters = {}) {
try {
const query = buildQuery({
role_name: filters.role_name?.trim() || undefined,
role_code: filters.role_code?.trim() || undefined,
status: filters.status || undefined,
page_no: 1,
page_size: 50,
});
const data = await request(`/api/system/roles?${query}`);
return {
rows: (data.list || []).map((item) => ({
roleId: item.role_id,
roleName: item.role_name,
roleCode: item.role_code,
status: Number(item.status || 1) === 1 ? "启用" : "停用",
remark: item.remark || "-",
createdAt: item.created_at || "-",
})),
isMock: false,
};
} catch (error) {
return wait({
rows: [
{
roleId: 1,
roleName: "管理员",
roleCode: "admin",
status: "启用",
remark: "系统管理",
createdAt: "2026-05-14 10:00:00",
},
{
roleId: 2,
roleName: "业务员",
roleCode: "salesman",
status: "启用",
remark: "业务录单",
createdAt: "2026-05-14 10:00:00",
},
],
isMock: true,
});
}
}
export async function createSystemRole(payload) {
return request("/api/system/roles", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function updateSystemRole(roleId, payload) {
return request(`/api/system/roles/${roleId}`, {
method: "PUT",
body: JSON.stringify(payload),
});
}
function flattenSystemMenus(list = [], level = 0) {
return list.flatMap((item) => [
{
menuId: item.menu_id,
parentId: item.parent_id,
menuName: item.menu_name,
menuPath: item.menu_path || "-",
menuType: item.menu_type || "-",
permissionCode: item.permission_code || "-",
icon: item.icon || "-",
sortNo: item.sort_no ?? 0,
status: Number(item.status || 1) === 1 ? "启用" : "停用",
level,
},
...flattenSystemMenus(item.children || [], level + 1),
]);
}
export async function fetchSystemMenus(filters = {}) {
try {
const query = buildQuery({
status: filters.status || undefined,
menu_type: filters.menu_type || undefined,
});
const data = await request(`/api/system/menus?${query}`);
return {
rows: flattenSystemMenus(data || []),
isMock: false,
};
} catch (error) {
return wait({
rows: [
{
menuId: 1,
parentId: 0,
menuName: "订单管理",
menuPath: "/orders",
menuType: "page",
permissionCode: "order:list",
icon: "order",
sortNo: 10,
status: "启用",
level: 0,
},
],
isMock: true,
});
}
}
export async function createSystemMenu(payload) {
return request("/api/system/menus", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function updateSystemMenu(menuId, payload) {
return request(`/api/system/menus/${menuId}`, {
method: "PUT",
body: JSON.stringify(payload),
});
}
function mapSettlementType(value) {
const map = {
monthly: "月结",
cash: "现结",
delivered: "送达结算",
};
return map[value] || value || "-";
}
function mapCustomerType(value) {
const map = {
channel: "渠道客户",
retail: "零售客户",
project: "项目客户",
};
return map[value] || value || "-";
}
function buildCustomerRow(item) {
return {
customerId: item.customer_id,
name: item.customer_name,
mobile: item.mobile,
address: item.address || "-",
settlement: mapSettlementType(item.settlement_type),
settlementType: item.settlement_type || "",
settlementDays: item.settlement_days ?? 0,
customerType: mapCustomerType(item.customer_type),
customerTypeValue: item.customer_type || "",
salesman: item.salesman_name || (item.salesman_id ? `业务员${item.salesman_id}` : "-"),
salesmanId: item.salesman_id ?? null,
arrears: Number(item.arrears_amount || 0).toFixed(2),
status: Number(item.status || 1) === 1 ? "启用" : "停用",
remark: item.remark || "-",
creditLimit: Number(item.credit_limit || 0).toFixed(2),
};
}
export async function fetchCustomerList(filters = {}) {
try {
const query = buildQuery({
customer_name: filters.customer_name?.trim() || undefined,
mobile: filters.mobile?.trim() || undefined,
customer_type: filters.customer_type || undefined,
settlement_type: filters.settlement_type || undefined,
salesman_id: filters.salesman_id || undefined,
page_no: 1,
page_size: 50,
});
const data = await request(`/api/customers?${query}`);
return {
rows: (data.list || []).map(buildCustomerRow),
isMock: false,
};
} catch (error) {
return wait({
rows: customerRows.map((item, index) => ({
customerId: 3000 + index + 1,
name: item.name,
mobile: item.mobile,
address: "演示地址",
settlement: item.settlement,
settlementType: item.settlement === "月结" ? "monthly" : "cash",
settlementDays: item.settlement === "月结" ? 30 : 0,
customerType: "渠道客户",
customerTypeValue: "channel",
salesman: item.salesman,
salesmanId: index + 1,
arrears: item.arrears.replace(/[^\d.]/g, "") || "0.00",
status: "启用",
remark: item.action,
creditLimit: "10000.00",
})),
isMock: true,
});
}
}
export async function fetchCustomerDetail(customerId) {
return request(`/api/customers/${customerId}`);
}
export async function createCustomer(payload) {
return request("/api/customers", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function fetchProductCategoryList(filters = {}) {
try {
const query = buildQuery({
category_name: filters.category_name?.trim() || undefined,
status: filters.status || undefined,
page_no: 1,
page_size: 50,
});
const data = await request(`/api/product-categories?${query}`);
return {
rows: (data.list || []).map((item) => ({
categoryId: item.category_id,
categoryName: item.category_name,
categoryCode: item.category_code,
sortNo: item.sort_no ?? 0,
status: Number(item.status || 1) === 1 ? "启用" : "停用",
remark: item.remark || "-",
})),
isMock: false,
};
} catch (error) {
return wait({
rows: [
{
categoryId: 1,
categoryName: "工业品",
categoryCode: "industry",
sortNo: 1,
status: "启用",
remark: "-",
},
],
isMock: true,
});
}
}
export async function createProductCategory(payload) {
return request("/api/product-categories", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function fetchProductList(filters = {}) {
try {
const query = buildQuery({
product_name: filters.product_name?.trim() || undefined,
specification: filters.specification?.trim() || undefined,
category_id: filters.category_id || undefined,
status: filters.status || undefined,
page_no: 1,
page_size: 50,
});
const data = await request(`/api/products?${query}`);
return {
rows: (data.list || []).map((item) => ({
productId: item.product_id,
productName: item.product_name,
specification: item.specification || "-",
unit: item.unit || "-",
categoryId: item.category_id,
categoryName: item.category_name || "-",
costPrice: Number(item.cost_price || 0).toFixed(2),
salePrice: Number(item.sale_price || 0).toFixed(2),
status: Number(item.status || 1) === 1 ? "启用" : "停用",
remark: item.remark || "-",
})),
isMock: false,
};
} catch (error) {
return wait({
rows: [
{
productId: 2001,
productName: "演示产品A",
specification: "10kg",
unit: "吨",
categoryId: 1,
categoryName: "工业品",
costPrice: "60.00",
salePrice: "100.00",
status: "启用",
remark: "-",
},
],
isMock: true,
});
}
}
export async function createProduct(payload) {
return request("/api/products", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function fetchSupplierList(filters = {}) {
try {
const query = buildQuery({
supplier_name: filters.supplier_name?.trim() || undefined,
supplier_type: filters.supplier_type || undefined,
status: filters.status || undefined,
page_no: 1,
page_size: 50,
});
const data = await request(`/api/suppliers?${query}`);
return {
rows: (data.list || []).map((item) => ({
supplierId: item.supplier_id,
supplierName: item.supplier_name,
supplierType: item.supplier_type || "-",
contactName: item.contact_name || "-",
contactMobile: item.contact_mobile || "-",
address: item.address || "-",
templateType: item.template_type || "-",
status: Number(item.status || 1) === 1 ? "启用" : "停用",
remark: item.remark || "-",
})),
isMock: false,
};
} catch (error) {
return wait({
rows: [
{
supplierId: 1001,
supplierName: "工厂A",
supplierType: "factory",
contactName: "王师傅",
contactMobile: "13800000001",
address: "演示工厂地址",
templateType: "default",
status: "启用",
remark: "-",
},
],
isMock: true,
});
}
}
export async function createSupplier(payload) {
return request("/api/suppliers", {
method: "POST",
body: JSON.stringify(payload),
});
}
export function fetchBusinessData() {
return wait({ modules: businessModules, tasks: taskRows });
}

File diff suppressed because it is too large Load Diff

View File

@ -1,52 +0,0 @@
<template>
<section class="page">
<h2>系统管理</h2>
<p v-if="loading" class="loading">加载中...</p>
<div v-else class="grid">
<article v-for="module in systemModules" :key="module.title" class="card">
<h3>{{ module.title }}</h3>
<ul>
<li v-for="item in module.items" :key="item">{{ item }}</li>
</ul>
</article>
</div>
<section v-if="!loading" class="toolbar">
<input type="text" placeholder="按账号或姓名筛选" />
<select>
<option>全部角色</option>
<option>管理员</option>
<option>业务员</option>
</select>
<button>新增用户</button>
<button>导出</button>
</section>
</section>
</template>
<script setup>
import { onMounted, ref } from "vue";
import { fetchSystemModules } from "../mockApi";
const loading = ref(true);
const systemModules = ref([]);
onMounted(async () => {
systemModules.value = await fetchSystemModules();
loading.value = false;
});
</script>
<style scoped>
.page { background: #fff; border-radius: 8px; padding: 20px; }
h2 { margin: 0 0 12px; }
.loading { margin: 0; color: #6b7280; }
.toolbar { display: flex; gap: 12px; margin-top: 20px; }
.toolbar input, .toolbar select { min-width: 220px; border: 1px solid #d1d5db; border-radius: 8px; padding: 10px 12px; background: #fff; }
.toolbar button { border: 1px solid #d1d5db; border-radius: 8px; padding: 10px 14px; background: #fff; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
.card { border: 1px solid #e5e7eb; border-radius: 8px; padding: 14px; }
h3 { margin: 0 0 10px; font-size: 16px; }
ul { margin: 0; padding-left: 18px; }
li + li { margin-top: 8px; }
</style>

View File

@ -40,7 +40,7 @@
| 提醒接口 | 02-API细化设计 | 只有占位返回;未接真实提醒任务 | 待完成 |
| 报表接口 | 02-API细化设计 | 只有演示聚合结果;未接真实统计 | 待完成 |
| 配置与审计接口 | 02-API细化设计 | 已有 service 骨架;未接真实表与写入逻辑 | 待完成 |
| 系统管理接口 | 02-API细化设计 | 已有接口骨架;未接真实用户/角色/菜单管理 | 待完成 |
| 系统管理接口 | 02-API细化设计 | 已接入真实用户/角色/菜单查询与新增链路,并补齐用户更新/重置密码、角色更新、菜单更新接口;角色授权关系尚待表结构支持 | 待完成 |
## 四、数据库
@ -104,3 +104,10 @@
| 2026-05-14 | 已完成一个可检查的完整任务:`frontend/web-admin` 管理端订单审批闭环现已支持工作台查看真实审批待办摘要、审批中心按订单编号/客户姓名/审批类型筛选待办订单、进入审批详情查看真实订单明细与审批记录,并可执行订单审批与取消审批;后端同步补齐订单列表 `order_no` 筛选能力以支撑管理端精确查询。 |
| 2026-05-14 | 已完成一个可检查的完整任务:`frontend/web-admin` 履约调度闭环现已支持查看待发厂订单、生成发厂文案、确认下发工厂、创建司机任务并查看真实任务列表;后端同步补齐物流任务请求模型、数据库仓储、真实任务创建与订单状态从 `pending_factory` 流转到 `pending_driver` 的联动。 |
| 2026-05-14 | 已整理 `任务状态跟踪.md` 的总览口径,使其与当前真实进度一致;并已完成一个可检查的完整任务:`frontend/mini-driver` 司机端执行闭环现已支持司机登录、查看真实任务列表、查看任务详情,并按状态执行接单、确认揽货、确认送达;后端已有司机任务状态与订单状态联动。 |
| 2026-05-14 | 已完成一个可检查的完整任务:`frontend/mini-manager` 微信管理层端审批闭环现已支持管理层登录、查看待订单审批/待取消审批列表、进入审批详情并执行通过/退回或同意取消/驳回取消;前端直接复用已打通的真实订单审批与取消审批接口。 |
| 2026-05-14 | 已继续提速推进微信管理层端:补齐 `dashboard / supplier-text / task-create` 三页真实联动,管理层端现已支持查看待审批/待发厂/待司机接单概览、生成并确认发厂文案、以及直接创建司机任务,微信管理层主线已基本覆盖审批后到履约下发的核心流程。 |
| 2026-05-14 | 已继续提速推进跨端履约留痕:补齐文件上传凭证与附件保存接口、增强物流轨迹新增与查询、并让司机端揽货/送达操作携带备注与模拟附件写入轨迹,形成“司机执行 + 轨迹留痕”闭环。 |
| 2026-05-14 | 已按加速节奏成组推进后端基础能力:认证接口开始优先接入真实 `sys_user/sys_role/sys_menu` 数据,产品分类/产品、供应商、系统管理查询接口已统一补成“数据库优先,失败回退演示数据”链路,并补齐对应请求模型、仓储与服务层。 |
| 2026-05-14 | 已继续按高节奏收口客户模块:后端客户创建补齐真实参数校验、重复客户错误口径和详情返回字段;`frontend/web-admin` 基础资料页已改为优先请求真实 `/api/customers``/api/customers/{id}`,支持客户筛选、查看详情和新增客户,客户主线已从演示态推进到可联调状态。 |
| 2026-05-14 | 已继续扩展 `frontend/web-admin` 基础资料页的真实化范围:产品分类、产品、工厂/供应商三块已接入真实查询与新增接口;后端同步补齐分类/产品/供应商创建的必要参数校验、重复数据校验与数据库优先链路,基础资料主页面已从单一客户演示页推进为多模块真实联调页。 |
| 2026-05-14 | 已继续严格参照 `02-API细化设计.md` 推进系统管理模块:后端已补齐 `/api/system/users`、`/api/system/users/{user_id}`、`/api/system/users/{user_id}/reset-password`、`/api/system/roles`、`/api/system/roles/{role_id}`、`/api/system/menus`、`/api/system/menus/{menu_id}` 的真实请求模型、参数校验、数据库优先 service/repository 链路;`frontend/web-admin` 的 `SystemPage` 已从演示页改为真实用户/角色/菜单联调页,支持列表查询与新增。 |