feat: v1.0 AI炫图工作台上线+业务做实+按需裁剪+主题精致化
- AI炫图: 自研程序化场景合成引擎(风格×时间×场景, 家具剪影/光束/暗角, seed构图微差), 阶段化生成动画+四方案画廊+PNG下载, 引擎抽为共享模块 - 业务做实: 报价接入产品价格本(use_price_book), 注册邀请码+返利真实计数, 报价单转订单携带图纸 - 按需裁剪: 下线短信验证码登录/支付收银, 商城改为功能总览 - 主题精致化: 品牌色设计变量+Element Plus主题覆盖+视觉升级 - 全接口回归通过, 移除项405确认
This commit is contained in:
@@ -28,14 +28,27 @@ async def login(body: LoginRequest, db: DbSession):
|
||||
}, message="登录成功")
|
||||
|
||||
|
||||
class RegisterBody(BaseModel):
|
||||
phone_number: str
|
||||
password: str
|
||||
invite_code: str = ""
|
||||
|
||||
|
||||
@router.post("/register")
|
||||
async def register(body: LoginRequest, db: DbSession):
|
||||
"""注册并直接返回 token。"""
|
||||
async def register(body: RegisterBody, db: DbSession):
|
||||
"""注册(支持邀请码,真实记录邀请关系)。"""
|
||||
service = AuthService(db)
|
||||
try:
|
||||
user = await service.register(body.phone_number, body.password)
|
||||
except ValueError as e:
|
||||
return fail(str(e))
|
||||
if body.invite_code.startswith("HMC"):
|
||||
try:
|
||||
inviter_id = int(body.invite_code[3:])
|
||||
if inviter_id != user.id:
|
||||
user.invited_by = inviter_id
|
||||
except ValueError:
|
||||
pass
|
||||
from huamenchuang.core.security import create_token
|
||||
return ok(data={"token": create_token(user.id), "user_id": user.id}, message="注册成功")
|
||||
|
||||
@@ -86,11 +99,16 @@ async def change_password(body: PasswordBody, user: CurrentUser, db: DbSession):
|
||||
|
||||
|
||||
@router.get("/invitation")
|
||||
async def invitation_info(user: CurrentUser):
|
||||
"""邀请信息(对齐原站 personal-center/invitation 返利)。"""
|
||||
async def invitation_info(user: CurrentUser, db: DbSession):
|
||||
"""邀请信息(真实计数:被邀请人 = invited_by 指向当前用户的账户)。"""
|
||||
from sqlalchemy import func, select
|
||||
from huamenchuang.models.user import User
|
||||
invited = (await db.execute(
|
||||
select(func.count()).select_from(User).where(User.invited_by == user.id)
|
||||
)).scalar_one()
|
||||
return ok(data={
|
||||
"invite_code": f"HMC{user.id:06d}",
|
||||
"invited_count": 0,
|
||||
"bounty": "0.00",
|
||||
"rule": "好友通过邀请码注册并购买后,可获得相应奖励",
|
||||
"invited_count": invited,
|
||||
"bounty": f"{invited * 20:.2f}",
|
||||
"rule": "好友通过邀请码注册后,每位奖励 ¥20(购买后可抵扣)",
|
||||
})
|
||||
|
||||
@@ -1,133 +1,47 @@
|
||||
"""平台扩展路由:验证码登录、支付、渲染设置、下料方案、设备、分享、文章。"""
|
||||
"""平台扩展路由:渲染设置、下料方案、设备、分享、文章、AI 炫图风格配置。"""
|
||||
import datetime
|
||||
import random
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import desc, func, select
|
||||
from sqlalchemy import desc, 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 Quotation
|
||||
from huamenchuang.core.response import fail, ok
|
||||
from huamenchuang.models.contract import Contract
|
||||
from huamenchuang.models.extend import Article, CompanyParam, Device, PayOrder, SmsCode
|
||||
from huamenchuang.models.extend import Article, CompanyParam, Device
|
||||
from huamenchuang.models.order import Order
|
||||
from huamenchuang.services.quote_service import quote as calc_quote
|
||||
|
||||
router = APIRouter(tags=["平台扩展"])
|
||||
|
||||
# ---------------- 短信验证码(provider 适配层) ----------------
|
||||
|
||||
async def _issue_code(db, phone: str, scene: str) -> str:
|
||||
"""签发验证码。未接入短信签名时走 mock 通道(控制台输出,响应不回传码)。"""
|
||||
code = f"{random.randint(0, 999999):06d}"
|
||||
db.add(SmsCode(phone_number=phone, code=code, scene=scene))
|
||||
print(f"[SMS:mock] {phone} 的{scene}验证码: {code}") # 接入真实短信后替换为服务商调用
|
||||
return code
|
||||
|
||||
|
||||
class CodeBody(BaseModel):
|
||||
phone_number: str
|
||||
scene: str = Field(default="login", pattern="^(login|reset|bind)$")
|
||||
|
||||
|
||||
@router.post("/verification-code-new")
|
||||
async def send_code(body: CodeBody, db: DbSession):
|
||||
"""发送短信验证码(mock 通道,见服务端日志)。"""
|
||||
await _issue_code(db, body.phone_number, body.scene)
|
||||
return ok(message="验证码已发送")
|
||||
|
||||
|
||||
class CodeLoginBody(BaseModel):
|
||||
phone_number: str
|
||||
code: str
|
||||
|
||||
|
||||
@router.post("/code-login")
|
||||
async def code_login(body: CodeLoginBody, db: DbSession):
|
||||
"""验证码登录:校验验证码后直接登录/注册。"""
|
||||
stmt = (select(SmsCode).where(SmsCode.phone_number == body.phone_number, SmsCode.used == 0,
|
||||
SmsCode.scene == "login").order_by(desc(SmsCode.id)).limit(1))
|
||||
record = (await db.execute(stmt)).scalar_one_or_none()
|
||||
if record is None or record.code != body.code:
|
||||
return fail("验证码错误或已过期")
|
||||
record.used = 1
|
||||
from huamenchuang.core.security import create_token
|
||||
from huamenchuang.models.user import User
|
||||
from huamenchuang.services.auth_service import hash_password
|
||||
u = (await db.execute(select(User).where(User.phone_number == body.phone_number))).scalar_one_or_none()
|
||||
if u is None:
|
||||
u = User(phone_number=body.phone_number, password_hash=hash_password(body.code),
|
||||
nickname=f"用户{body.phone_number[-4:]}")
|
||||
db.add(u)
|
||||
await db.flush()
|
||||
return ok(data={"token": create_token(u.id), "user": {"id": u.id, "nickname": u.nickname}},
|
||||
message="登录成功")
|
||||
|
||||
|
||||
# ---------------- 支付订单(通道适配层) ----------------
|
||||
|
||||
class PayBody(BaseModel):
|
||||
subject: str
|
||||
amount: float = Field(gt=0)
|
||||
channel: str = Field(default="wechat", pattern="^(wechat|alipay)$")
|
||||
|
||||
|
||||
@router.post("/pay-orders")
|
||||
async def create_pay_order(body: PayBody, user: CurrentUser, db: DbSession):
|
||||
"""创建支付订单。微信/支付宝商户凭据配置后接通真实下单;当前返回待支付单。"""
|
||||
today = datetime.date.today().strftime("%Y%m%d")
|
||||
seq = random.randint(0, 9999)
|
||||
po = PayOrder(company_id=user.company_id, user_id=user.id, subject=body.subject[:128],
|
||||
amount=body.amount, channel=body.channel, pay_no=f"PAY{today}{seq:04d}")
|
||||
db.add(po)
|
||||
await db.flush()
|
||||
return ok(data={
|
||||
"pay_no": po.pay_no, "amount": float(po.amount), "channel": po.channel,
|
||||
"status": po.status,
|
||||
"pay_url": "", # 微信/支付宝下单接口返回的支付链接,凭据就绪后填充
|
||||
"tip": "支付通道未配置商户凭据,下单已记录,接通后自动唤起收银台",
|
||||
}, message="支付订单已创建")
|
||||
|
||||
|
||||
@router.get("/pay-orders")
|
||||
async def list_pay_orders(user: CurrentUser, db: DbSession, page: int = 1, page_size: int = 15):
|
||||
conds = [PayOrder.company_id == user.company_id]
|
||||
total = (await db.execute(select(func.count()).select_from(PayOrder).where(*conds))).scalar_one()
|
||||
stmt = (select(PayOrder).where(*conds).order_by(desc(PayOrder.id))
|
||||
.offset((page - 1) * page_size).limit(page_size))
|
||||
items = list((await db.execute(stmt)).scalars())
|
||||
return ok(data=paginated([{
|
||||
"pay_no": p.pay_no, "subject": p.subject, "amount": float(p.amount),
|
||||
"channel": p.channel, "status": p.status, "created_at": str(p.created_at),
|
||||
} for p in items], page, page_size, total))
|
||||
|
||||
|
||||
# ---------------- 渲染设置 + 简易渲染参数 ----------------
|
||||
# ---------------- 渲染设置 + AI 炫图风格配置 ----------------
|
||||
|
||||
class RenderSettingBody(BaseModel):
|
||||
wall_color: str = "#f5f0e8"
|
||||
scene: str = Field(default="indoor", pattern="^(indoor|outdoor|studio)$")
|
||||
glass_tint: str = Field(default="#cfe3ee", pattern="^#[0-9a-fA-F]{6}$")
|
||||
ai_style: str = Field(default="modern", pattern="^(modern|chinese|european|nordic)$")
|
||||
ai_time: str = Field(default="day", pattern="^(day|dusk|night)$")
|
||||
|
||||
|
||||
async def _get_param(db, company_id: int, key: str):
|
||||
stmt = select(CompanyParam).where(CompanyParam.company_id == company_id, CompanyParam.key == key)
|
||||
return (await db.execute(stmt)).scalar_one_or_none()
|
||||
|
||||
|
||||
@router.get("/rendering-info")
|
||||
async def get_render_setting(user: CurrentUser, db: DbSession):
|
||||
stmt = select(CompanyParam).where(CompanyParam.company_id == user.company_id,
|
||||
CompanyParam.key == "render_setting")
|
||||
p = (await db.execute(stmt)).scalar_one_or_none()
|
||||
import json as _json
|
||||
return ok(data=_json.loads(p.value) if p else {
|
||||
"wall_color": "#f5f0e8", "scene": "indoor", "glass_tint": "#cfe3ee"})
|
||||
p = await _get_param(db, user.company_id, "render_setting")
|
||||
defaults = {"wall_color": "#f5f0e8", "scene": "indoor", "glass_tint": "#cfe3ee",
|
||||
"ai_style": "modern", "ai_time": "day"}
|
||||
return ok(data={**defaults, **(_json.loads(p.value) if p else {})})
|
||||
|
||||
|
||||
@router.post("/rendering-info")
|
||||
async def save_render_setting(body: RenderSettingBody, user: CurrentUser, db: DbSession):
|
||||
import json as _json
|
||||
stmt = select(CompanyParam).where(CompanyParam.company_id == user.company_id,
|
||||
CompanyParam.key == "render_setting")
|
||||
p = (await db.execute(stmt)).scalar_one_or_none()
|
||||
p = await _get_param(db, user.company_id, "render_setting")
|
||||
if p is None:
|
||||
p = CompanyParam(company_id=user.company_id, key="render_setting")
|
||||
db.add(p)
|
||||
@@ -176,6 +90,16 @@ async def cut_solution(body: CutSolutionBody, user: CurrentUser, db: DbSession):
|
||||
message="下料方案已生成")
|
||||
|
||||
|
||||
@router.get("/cut_solution/{contract_id}")
|
||||
async def get_cut_solution(contract_id: int, user: CurrentUser, db: DbSession):
|
||||
"""读取已保存的下料方案参数。"""
|
||||
p = await _get_param(db, user.company_id, f"cut_solution_{contract_id}")
|
||||
if p is None:
|
||||
return fail("尚未生成下料方案")
|
||||
import json as _json
|
||||
return ok(data=_json.loads(p.value))
|
||||
|
||||
|
||||
async def _save_param(db, company_id: int, key: str, value: str) -> None:
|
||||
stmt = select(CompanyParam).where(CompanyParam.company_id == company_id, CompanyParam.key == key)
|
||||
p = (await db.execute(stmt)).scalar_one_or_none()
|
||||
@@ -239,19 +163,16 @@ def make_share_token(order_id: int) -> str:
|
||||
|
||||
|
||||
def parse_share_token(token: str) -> int | None:
|
||||
import base64
|
||||
import base64, hmac as _hmac
|
||||
try:
|
||||
raw = base64.urlsafe_b64decode(token + "=" * (-len(token) % 4)).decode()
|
||||
oid_s, sig = raw.split(".", 1)
|
||||
oid = int(oid_s)
|
||||
return oid if hmac.compare_digest(sig, _sign(oid)) else None
|
||||
return oid if _hmac.compare_digest(sig, _sign(oid)) else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
import hmac # noqa: E402
|
||||
|
||||
|
||||
@router.get("/shared-order/{token}")
|
||||
async def shared_order(token: str, db: DbSession):
|
||||
"""通过分享 token 公开只读订单摘要(无需登录)。"""
|
||||
|
||||
@@ -15,6 +15,7 @@ class QuoteRequest(BaseModel):
|
||||
template_id: int | None = Field(default=None, description="或直接指定样板")
|
||||
order_id: int | None = Field(default=None, description="或对已有订单报价")
|
||||
prices: dict | None = Field(default=None, description="价格本覆盖 {frame,mullion,sash,glass,hardware}")
|
||||
use_price_book: bool = Field(default=False, description="从产品管理价格本取价(按材料类别均价)")
|
||||
|
||||
|
||||
async def _resolve_drawing(db, body: QuoteRequest) -> str | dict | None:
|
||||
@@ -32,14 +33,38 @@ async def _resolve_drawing(db, body: QuoteRequest) -> str | dict | None:
|
||||
return None
|
||||
|
||||
|
||||
async def _price_book(db, company_id: int) -> dict | None:
|
||||
"""从产品价格本推导报价参数:各类材料均价。"""
|
||||
from sqlalchemy import func, select
|
||||
from huamenchuang.models.business import Product
|
||||
rows = (await db.execute(
|
||||
select(Product.material, func.avg(Product.price))
|
||||
.where(Product.company_id == company_id, Product.enabled == 1)
|
||||
.group_by(Product.material))).all()
|
||||
if not rows:
|
||||
return None
|
||||
avg = {m: float(p or 0) for m, p in rows}
|
||||
prices: dict = {}
|
||||
if avg.get("profile"):
|
||||
prices["frame"] = prices["mullion"] = prices["sash"] = round(avg["profile"], 2)
|
||||
if avg.get("glass"):
|
||||
prices["glass"] = round(avg["glass"], 2)
|
||||
if avg.get("hardware"):
|
||||
prices["hardware"] = round(avg["hardware"], 2)
|
||||
return prices or None
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def offer_price(body: QuoteRequest, user: CurrentUser, db: DbSession):
|
||||
"""算料报价:型材清单 + 玻璃面积 + 五金 + 总价。"""
|
||||
drawing = await _resolve_drawing(db, body)
|
||||
if drawing is None:
|
||||
return fail("未找到图纸数据")
|
||||
prices = body.prices
|
||||
if prices is None and body.use_price_book:
|
||||
prices = await _price_book(db, user.company_id)
|
||||
try:
|
||||
result = calc_quote(drawing, body.prices)
|
||||
result = calc_quote(drawing, prices)
|
||||
except Exception:
|
||||
return fail("图纸数据解析失败")
|
||||
return ok(data=result, message="报价成功")
|
||||
|
||||
Reference in New Issue
Block a user