feat: v0.7 全功能复刻与前端适配
后端: - 产品价格本模块(型材/玻璃/五金/配件, 供报价取价) - 报价单模块(BJ单号, 手动报价/样板算料/转订单) - 售后单(问题描述必填, 对齐原站校验)与状态流转 - 量尺单(洞口尺寸清单)与阳光房订单(order_type=1) - 个人中心: 资料/密码修改/邀请返利(HMC邀请码) 前端: - 新增8页面: 产品/报价单/售后/量尺/阳光房/商城/使用指南/个人中心 - 登录页注册双Tab, 响应式侧边栏(<900px自动折叠) - Element Plus 图标全局注册修复 - 共15个页面覆盖原站核心业务路由
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"""认证路由:登录/注册/当前用户。"""
|
||||
"""认证路由:登录/注册/当前用户/个人中心。"""
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
|
||||
from huamenchuang.core.deps import CurrentUser, DbSession
|
||||
from huamenchuang.core.response import fail, ok
|
||||
@@ -42,8 +43,54 @@ async def register(body: LoginRequest, db: DbSession):
|
||||
@router.get("/current-user")
|
||||
async def current_user(user: CurrentUser):
|
||||
"""当前登录用户信息。"""
|
||||
from huamenchuang.core.config import settings
|
||||
return ok(data={
|
||||
"id": user.id, "nickname": user.nickname, "phone_number": user.phone_number,
|
||||
"company_id": user.company_id, "language": user.language,
|
||||
"enabled_modules": settings.enabled_modules,
|
||||
"try_expiration_time": str(user.try_expiration_time) if user.try_expiration_time else None,
|
||||
})
|
||||
|
||||
|
||||
class ProfileBody(BaseModel):
|
||||
nickname: str | None = None
|
||||
email: str | None = None
|
||||
|
||||
|
||||
class PasswordBody(BaseModel):
|
||||
old_password: str
|
||||
new_password: str
|
||||
|
||||
|
||||
@router.put("/profile")
|
||||
async def update_profile(body: ProfileBody, user: CurrentUser, db: DbSession):
|
||||
"""修改个人资料(对齐原站 personal-center)。"""
|
||||
from huamenchuang.services.auth_service import hash_password # noqa: F401
|
||||
if body.nickname is not None:
|
||||
user.nickname = body.nickname[:64]
|
||||
if body.email is not None:
|
||||
user.email = body.email[:128]
|
||||
return ok(message="资料已更新")
|
||||
|
||||
|
||||
@router.put("/password")
|
||||
async def change_password(body: PasswordBody, user: CurrentUser, db: DbSession):
|
||||
"""修改密码(校验旧密码)。"""
|
||||
from huamenchuang.services.auth_service import hash_password, verify_password
|
||||
if not verify_password(body.old_password, user.password_hash):
|
||||
return fail("旧密码错误")
|
||||
if len(body.new_password) < 4:
|
||||
return fail("新密码至少4位")
|
||||
user.password_hash = hash_password(body.new_password)
|
||||
return ok(message="密码已修改")
|
||||
|
||||
|
||||
@router.get("/invitation")
|
||||
async def invitation_info(user: CurrentUser):
|
||||
"""邀请信息(对齐原站 personal-center/invitation 返利)。"""
|
||||
return ok(data={
|
||||
"invite_code": f"HMC{user.id:06d}",
|
||||
"invited_count": 0,
|
||||
"bounty": "0.00",
|
||||
"rule": "好友通过邀请码注册并购买后,可获得相应奖励",
|
||||
})
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
"""业务扩展路由:产品价格本、报价单、售后、量尺、阳光房订单过滤。"""
|
||||
import datetime
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import func, or_, select
|
||||
|
||||
from huamenchuang.core.database import SessionLocal
|
||||
from huamenchuang.core.deps import CurrentUser, DbSession
|
||||
from huamenchuang.core.response import fail, ok, paginated
|
||||
from huamenchuang.models.business import AfterSale, MeasureOrder, Product, Quotation
|
||||
from huamenchuang.models.order import Order
|
||||
|
||||
router = APIRouter(tags=["业务扩展"])
|
||||
|
||||
|
||||
# ---------------- 产品价格本 ----------------
|
||||
|
||||
class ProductBody(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=128)
|
||||
material: str = Field(default="profile", pattern="^(profile|glass|hardware|addon)$")
|
||||
spec: str = ""
|
||||
unit: str = "米"
|
||||
price: float = 0
|
||||
cost: float = 0
|
||||
note: str = ""
|
||||
enabled: int = 1
|
||||
|
||||
|
||||
@router.get("/products")
|
||||
async def list_products(user: CurrentUser, db: DbSession, page: int = 1, page_size: int = 15,
|
||||
keyword: str = "", material: str | None = None):
|
||||
conds = [Product.company_id == user.company_id]
|
||||
if material:
|
||||
conds.append(Product.material == material)
|
||||
if kw := keyword.strip():
|
||||
like = f"%{kw}%"
|
||||
conds.append(or_(Product.name.like(like), Product.spec.like(like)))
|
||||
where = *conds,
|
||||
total = (await db.execute(select(func.count()).select_from(Product).where(*where))).scalar_one()
|
||||
stmt = (select(Product).where(*where).order_by(Product.id.desc())
|
||||
.offset((page - 1) * page_size).limit(page_size))
|
||||
items = list((await db.execute(stmt)).scalars())
|
||||
return ok(data=paginated([{
|
||||
"id": p.id, "name": p.name, "material": p.material, "spec": p.spec, "unit": p.unit,
|
||||
"price": float(p.price), "cost": float(p.cost), "enabled": p.enabled,
|
||||
"note": p.note,
|
||||
} for p in items], page, page_size, total))
|
||||
|
||||
|
||||
@router.post("/products")
|
||||
async def create_product(body: ProductBody, user: CurrentUser, db: DbSession):
|
||||
p = Product(company_id=user.company_id, **body.model_dump())
|
||||
db.add(p)
|
||||
await db.flush()
|
||||
return ok(data={"id": p.id}, message="创建成功")
|
||||
|
||||
|
||||
@router.put("/products/{pid}")
|
||||
async def update_product(pid: int, body: ProductBody, user: CurrentUser, db: DbSession):
|
||||
p = await db.get(Product, pid)
|
||||
if p is None or p.company_id != user.company_id:
|
||||
return fail("产品不存在")
|
||||
for k, v in body.model_dump().items():
|
||||
setattr(p, k, v)
|
||||
return ok(message="更新成功")
|
||||
|
||||
|
||||
@router.delete("/products/{pid}")
|
||||
async def delete_product(pid: int, user: CurrentUser, db: DbSession):
|
||||
p = await db.get(Product, pid)
|
||||
if p is None or p.company_id != user.company_id:
|
||||
return fail("产品不存在")
|
||||
await db.delete(p)
|
||||
return ok(message="删除成功")
|
||||
|
||||
|
||||
# ---------------- 报价单 ----------------
|
||||
|
||||
class QuotationBody(BaseModel):
|
||||
customer_id: int | None = None
|
||||
customer_name: str = ""
|
||||
contract_id: int | None = None
|
||||
template_id: int | None = None
|
||||
drawing_data: str = ""
|
||||
items: list[dict] = []
|
||||
glass_area: float = 0
|
||||
width_mm: int = 0
|
||||
height_mm: int = 0
|
||||
total: float = 0
|
||||
note: str = ""
|
||||
status: int = Field(default=0, ge=0, le=3)
|
||||
|
||||
|
||||
async def _next_quote_no(db, company_id: int) -> str:
|
||||
today = datetime.date.today().strftime("%Y%m%d")
|
||||
prefix = f"BJ{today}"
|
||||
stmt = select(Quotation.quote_no).where(
|
||||
Quotation.company_id == company_id, Quotation.quote_no.like(f"{prefix}%")
|
||||
).order_by(Quotation.quote_no.desc()).limit(1)
|
||||
import json as _json
|
||||
last = (await db.execute(stmt)).scalar_one_or_none()
|
||||
seq = int(last[-3:]) + 1 if last else 1
|
||||
return f"{prefix}{seq:03d}"
|
||||
|
||||
|
||||
def _quote_out(q: Quotation) -> dict:
|
||||
return {
|
||||
"id": q.id, "quote_no": q.quote_no, "customer_id": q.customer_id,
|
||||
"customer_name": q.customer_name, "contract_id": q.contract_id,
|
||||
"template_id": q.template_id, "glass_area": float(q.glass_area),
|
||||
"width_mm": q.width_mm, "height_mm": q.height_mm, "total": float(q.total),
|
||||
"status": q.status, "note": q.note, "created_at": str(q.created_at),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/quotations")
|
||||
async def create_quotation(body: QuotationBody, user: CurrentUser, db: DbSession):
|
||||
q = Quotation(
|
||||
company_id=user.company_id, user_id=user.id,
|
||||
quote_no=await _next_quote_no(db, user.company_id),
|
||||
items=__import__("json").dumps(body.items, ensure_ascii=False),
|
||||
**{k: v for k, v in body.model_dump().items() if k != "items"},
|
||||
)
|
||||
db.add(q)
|
||||
await db.flush()
|
||||
return ok(data=_quote_out(q), message="报价单已保存")
|
||||
|
||||
|
||||
@router.get("/quotations")
|
||||
async def list_quotations(user: CurrentUser, db: DbSession, page: int = 1, page_size: int = 15,
|
||||
keyword: str = "", status: int | None = None):
|
||||
conds = [Quotation.company_id == user.company_id]
|
||||
if status is not None:
|
||||
conds.append(Quotation.status == status)
|
||||
if kw := keyword.strip():
|
||||
like = f"%{kw}%"
|
||||
conds.append(or_(Quotation.quote_no.like(like), Quotation.customer_name.like(like)))
|
||||
where = *conds,
|
||||
total = (await db.execute(select(func.count()).select_from(Quotation).where(*where))).scalar_one()
|
||||
stmt = (select(Quotation).where(*where).order_by(Quotation.id.desc())
|
||||
.offset((page - 1) * page_size).limit(page_size))
|
||||
items = list((await db.execute(stmt)).scalars())
|
||||
return ok(data=paginated([_quote_out(q) for q in items], page, page_size, total))
|
||||
|
||||
|
||||
@router.get("/quotations/{qid}")
|
||||
async def quotation_detail(qid: int, user: CurrentUser, db: DbSession):
|
||||
q = await db.get(Quotation, qid)
|
||||
if q is None or q.company_id != user.company_id:
|
||||
return fail("报价单不存在")
|
||||
import json as _json
|
||||
d = _quote_out(q)
|
||||
d["items"] = _json.loads(q.items or "[]")
|
||||
d["drawing_data"] = q.drawing_data
|
||||
return ok(data=d)
|
||||
|
||||
|
||||
@router.put("/quotations/{qid}")
|
||||
async def update_quotation(qid: int, body: QuotationBody, user: CurrentUser, db: DbSession):
|
||||
q = await db.get(Quotation, qid)
|
||||
if q is None or q.company_id != user.company_id:
|
||||
return fail("报价单不存在")
|
||||
for k, v in body.model_dump().items():
|
||||
if k == "items":
|
||||
q.items = __import__("json").dumps(v, ensure_ascii=False)
|
||||
else:
|
||||
setattr(q, k, v)
|
||||
return ok(data=_quote_out(q), message="更新成功")
|
||||
|
||||
|
||||
@router.delete("/quotations/{qid}")
|
||||
async def delete_quotation(qid: int, user: CurrentUser, db: DbSession):
|
||||
q = await db.get(Quotation, qid)
|
||||
if q is None or q.company_id != user.company_id:
|
||||
return fail("报价单不存在")
|
||||
await db.delete(q)
|
||||
return ok(message="删除成功")
|
||||
|
||||
|
||||
# ---------------- 售后单 ----------------
|
||||
|
||||
class AfterSaleBody(BaseModel):
|
||||
title: str = ""
|
||||
description: str = Field(min_length=1, description="问题描述(必填,对齐原站校验)")
|
||||
order_id: int | None = None
|
||||
order_no: str = ""
|
||||
status: int = Field(default=0, ge=0, le=2)
|
||||
result: str = ""
|
||||
|
||||
|
||||
@router.post("/after_sales")
|
||||
async def create_after_sale(body: AfterSaleBody, user: CurrentUser, db: DbSession):
|
||||
a = AfterSale(company_id=user.company_id, user_id=user.id, **body.model_dump())
|
||||
db.add(a)
|
||||
await db.flush()
|
||||
return ok(data={"id": a.id}, message="售后单已提交")
|
||||
|
||||
|
||||
@router.get("/after_sales")
|
||||
async def list_after_sales(user: CurrentUser, db: DbSession, page: int = 1, page_size: int = 15,
|
||||
status: int | None = None):
|
||||
conds = [AfterSale.company_id == user.company_id]
|
||||
if status is not None:
|
||||
conds.append(AfterSale.status == status)
|
||||
where = *conds,
|
||||
total = (await db.execute(select(func.count()).select_from(AfterSale).where(*where))).scalar_one()
|
||||
stmt = (select(AfterSale).where(*where).order_by(AfterSale.id.desc())
|
||||
.offset((page - 1) * page_size).limit(page_size))
|
||||
items = list((await db.execute(stmt)).scalars())
|
||||
return ok(data=paginated([{
|
||||
"id": a.id, "title": a.title, "description": a.description, "order_id": a.order_id,
|
||||
"order_no": a.order_no, "status": a.status, "result": a.result,
|
||||
"created_at": str(a.created_at),
|
||||
} for a in items], page, page_size, total))
|
||||
|
||||
|
||||
class AfterSaleFlowBody(BaseModel):
|
||||
status: int = Field(ge=0, le=2)
|
||||
result: str = ""
|
||||
|
||||
|
||||
@router.put("/after_sales/{aid}")
|
||||
async def update_after_sale(aid: int, body: AfterSaleFlowBody, user: CurrentUser, db: DbSession):
|
||||
"""售后状态流转。"""
|
||||
a = await db.get(AfterSale, aid)
|
||||
if a is None or a.company_id != user.company_id:
|
||||
return fail("售后单不存在")
|
||||
a.status = body.status
|
||||
if body.result:
|
||||
a.result = body.result
|
||||
return ok(message="状态已更新")
|
||||
|
||||
|
||||
class MeasureUpdateBody(BaseModel):
|
||||
customer_name: str | None = None
|
||||
phone: str | None = None
|
||||
address: str | None = None
|
||||
measure_date: str | None = None
|
||||
surveyor: str | None = None
|
||||
status: int | None = Field(default=None, ge=0, le=2)
|
||||
data: list[dict] | None = None
|
||||
note: str | None = None
|
||||
|
||||
|
||||
@router.put("/measures/{mid}")
|
||||
async def update_measure(mid: int, body: MeasureUpdateBody, user: CurrentUser, db: DbSession):
|
||||
"""更新量尺单。"""
|
||||
m = await db.get(MeasureOrder, mid)
|
||||
if m is None or m.company_id != user.company_id:
|
||||
return fail("量尺单不存在")
|
||||
data = body.model_dump(exclude_none=True)
|
||||
md = data.pop("measure_date", None)
|
||||
if md is not None:
|
||||
try:
|
||||
m.measure_date = datetime.datetime.fromisoformat(md)
|
||||
except ValueError:
|
||||
return fail("日期格式错误")
|
||||
items = data.pop("data", None)
|
||||
for k, v in data.items():
|
||||
setattr(m, k, v)
|
||||
if items is not None:
|
||||
m.data = __import__("json").dumps(items, ensure_ascii=False)
|
||||
return ok(message="更新成功")
|
||||
|
||||
|
||||
# ---------------- 量尺单 ----------------
|
||||
|
||||
class MeasureBody(BaseModel):
|
||||
customer_id: int | None = None
|
||||
customer_name: str = ""
|
||||
phone: str = ""
|
||||
address: str = ""
|
||||
measure_date: str | None = None
|
||||
surveyor: str = ""
|
||||
status: int = Field(default=0, ge=0, le=2)
|
||||
data: list[dict] = [] # 洞口尺寸 [{name,width,height,qty}]
|
||||
note: str = ""
|
||||
|
||||
|
||||
@router.post("/measures")
|
||||
async def create_measure(body: MeasureBody, user: CurrentUser, db: DbSession):
|
||||
md = None
|
||||
if body.measure_date:
|
||||
try:
|
||||
md = datetime.datetime.fromisoformat(body.measure_date)
|
||||
except ValueError:
|
||||
return fail("量尺日期格式错误")
|
||||
m = MeasureOrder(company_id=user.company_id, customer_id=body.customer_id,
|
||||
customer_name=body.customer_name, phone=body.phone, address=body.address,
|
||||
measure_date=md, surveyor=body.surveyor, status=body.status,
|
||||
data=__import__("json").dumps(body.data, ensure_ascii=False), note=body.note)
|
||||
db.add(m)
|
||||
await db.flush()
|
||||
return ok(data={"id": m.id}, message="量尺单已创建")
|
||||
|
||||
|
||||
@router.get("/measures")
|
||||
async def list_measures(user: CurrentUser, db: DbSession, page: int = 1, page_size: int = 15,
|
||||
status: int | None = None):
|
||||
conds = [MeasureOrder.company_id == user.company_id]
|
||||
if status is not None:
|
||||
conds.append(MeasureOrder.status == status)
|
||||
where = *conds,
|
||||
total = (await db.execute(select(func.count()).select_from(MeasureOrder).where(*where))).scalar_one()
|
||||
stmt = (select(MeasureOrder).where(*where).order_by(MeasureOrder.id.desc())
|
||||
.offset((page - 1) * page_size).limit(page_size))
|
||||
items = list((await db.execute(stmt)).scalars())
|
||||
import json as _json
|
||||
return ok(data=paginated([{
|
||||
"id": m.id, "customer_name": m.customer_name, "phone": m.phone, "address": m.address,
|
||||
"measure_date": str(m.measure_date) if m.measure_date else None, "surveyor": m.surveyor,
|
||||
"status": m.status, "data": _json.loads(m.data or "[]"), "note": m.note,
|
||||
"created_at": str(m.created_at),
|
||||
} for m in items], page, page_size, total))
|
||||
|
||||
|
||||
# ---------------- 阳光房订单(订单按类型过滤) ----------------
|
||||
|
||||
@router.get("/sunny_house_orders")
|
||||
async def list_sunny_orders(user: CurrentUser, db: DbSession, page: int = 1, page_size: int = 15):
|
||||
"""阳光房订单列表(order_type=1,对齐目标站 sunny_house_order/)。"""
|
||||
conds = [Order.company_id == user.company_id, Order.order_type == 1]
|
||||
total = (await db.execute(select(func.count()).select_from(Order).where(*conds))).scalar_one()
|
||||
stmt = (select(Order).where(*conds).order_by(Order.id.desc())
|
||||
.offset((page - 1) * page_size).limit(page_size))
|
||||
items = list((await db.execute(stmt)).scalars())
|
||||
return ok(data=paginated([{
|
||||
"id": o.id, "order_no": o.order_no, "customer_name": o.customer_name,
|
||||
"money": float(o.money), "status": o.status, "created_at": str(o.created_at),
|
||||
} for o in items], page, page_size, total))
|
||||
@@ -3,6 +3,7 @@ from fastapi import APIRouter
|
||||
|
||||
from huamenchuang.api import (
|
||||
auth,
|
||||
business,
|
||||
contracts,
|
||||
customers,
|
||||
optimize,
|
||||
@@ -21,3 +22,4 @@ api_router.include_router(windoor.router)
|
||||
api_router.include_router(optimize.router)
|
||||
api_router.include_router(quote.router)
|
||||
api_router.include_router(warehouse.router)
|
||||
api_router.include_router(business.router)
|
||||
|
||||
Reference in New Issue
Block a user