refactor: switch to free-trial/private-deploy/buyout pricing, fix feature gaps, add SEO landing + deploy config

This commit is contained in:
TradeMate Dev
2026-07-12 08:09:28 +08:00
parent 9ca5d79d8a
commit 04924e3bc4
36 changed files with 1026 additions and 975 deletions
@@ -0,0 +1,40 @@
"""add enterprise_leads table
Revision ID: add_enterprise_leads
Revises: add_perf_indexes
Create Date: 2026-07-11
Stores inbound leads from the private-deployment / source-buyout CTAs.
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'add_enterprise_leads'
down_revision = 'add_perf_indexes'
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
'enterprise_leads',
sa.Column('id', sa.String(36), primary_key=True),
sa.Column('type', sa.String(20), nullable=False, server_default='private'),
sa.Column('name', sa.String(100), nullable=False, server_default=''),
sa.Column('company', sa.String(200), nullable=False, server_default=''),
sa.Column('phone', sa.String(50), nullable=False, server_default=''),
sa.Column('email', sa.String(200), nullable=False, server_default=''),
sa.Column('message', sa.Text(), nullable=False, server_default=''),
sa.Column('status', sa.String(20), nullable=False, server_default='new'),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_index('ix_enterprise_leads_type', 'enterprise_leads', ['type'])
op.create_index('ix_enterprise_leads_status', 'enterprise_leads', ['status'])
def downgrade() -> None:
op.drop_index('ix_enterprise_leads_status', table_name='enterprise_leads')
op.drop_index('ix_enterprise_leads_type', table_name='enterprise_leads')
op.drop_table('enterprise_leads')
+34
View File
@@ -0,0 +1,34 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel, EmailStr
from app.database import get_db
from app.models.enterprise_lead import EnterpriseLead
router = APIRouter()
class LeadCreate(BaseModel):
type: str # private | buyout
name: str
company: str = ""
phone: str = ""
email: str = ""
message: str = ""
@router.post("")
async def create_lead(payload: LeadCreate, db: AsyncSession = Depends(get_db)):
if payload.type not in ("private", "buyout"):
raise HTTPException(status_code=400, detail="Invalid lead type")
lead = EnterpriseLead(
type=payload.type,
name=payload.name,
company=payload.company,
phone=payload.phone,
email=payload.email,
message=payload.message,
)
db.add(lead)
await db.flush()
await db.refresh(lead)
return {"id": lead.id, "type": lead.type, "status": lead.status}
+1
View File
@@ -26,6 +26,7 @@ CSRF_SKIP_ENDPOINTS = [
"/api/v1/payment/",
"/api/v1/whatsapp/webhook",
"/api/v1/ai/",
"/api/v1/leads",
]
+2 -1
View File
@@ -129,7 +129,7 @@ async def health():
return {"status": "ok", "app": settings.APP_NAME, "version": "1.0.0"}
from app.api.v1 import auth, marketing, translate, customer, quotation, whatsapp, product, exchange, push, admin, analytics, teams, onboarding, notification, feedback, payment, interaction, silent_pattern, training, followup, ai_assistant, discovery, discovery_record, certification, invoice, usage, referral, admin_search, search, admin_ai, credits, admin_credits, agent
from app.api.v1 import auth, marketing, translate, customer, quotation, whatsapp, product, exchange, push, admin, analytics, teams, onboarding, notification, feedback, payment, interaction, silent_pattern, training, followup, ai_assistant, discovery, discovery_record, certification, invoice, usage, referral, admin_search, search, admin_ai, credits, admin_credits, agent, leads
app.include_router(auth.router, prefix="/api/v1/auth", tags=["auth"])
app.include_router(marketing.router, prefix="/api/v1/marketing", tags=["marketing"])
@@ -165,6 +165,7 @@ app.include_router(admin_credits.router, prefix="/api/v1/admin", tags=["admin"])
app.include_router(credits.router, prefix="/api/v1/credits", tags=["credits"])
app.include_router(search.router, prefix="/api/v1/search", tags=["search"])
app.include_router(agent.router, prefix="/api/v1/agent", tags=["agent"])
app.include_router(leads.router, prefix="/api/v1/leads", tags=["leads"])
if __name__ == "__main__":
+2
View File
@@ -24,6 +24,7 @@ from .user_credit import UserCredit
from .credit_consumption import CreditConsumption
from .credit_purchase import CreditPurchase
from .agent_pipeline import AgentPipeline
from .enterprise_lead import EnterpriseLead
__all__ = [
"User", "Product",
@@ -47,4 +48,5 @@ __all__ = [
"CreditConsumption",
"CreditPurchase",
"AgentPipeline",
"EnterpriseLead",
]
+17
View File
@@ -0,0 +1,17 @@
from sqlalchemy import Column, String, Text, DateTime, func
from app.database import Base
import uuid
class EnterpriseLead(Base):
__tablename__ = "enterprise_leads"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
type = Column(String(20), nullable=False, default="private") # private | buyout
name = Column(String(100), nullable=False, default="")
company = Column(String(200), nullable=False, default="")
phone = Column(String(50), nullable=False, default="")
email = Column(String(200), nullable=False, default="")
message = Column(Text, nullable=False, default="")
status = Column(String(20), nullable=False, default="new") # new | contacted | done
created_at = Column(DateTime(timezone=True), server_default=func.now())
+36
View File
@@ -0,0 +1,36 @@
import pytest
from httpx import AsyncClient
class TestLeadsAPI:
async def test_create_private_lead(self, client: AsyncClient):
res = await client.post(
"/api/v1/leads",
json={
"type": "private",
"name": "张三",
"company": "测试外贸公司",
"phone": "13800138000",
"message": "想私有化部署",
},
)
assert res.status_code == 200
data = res.json()
assert data["type"] == "private"
assert data["status"] == "new"
assert data["id"]
async def test_create_buyout_lead(self, client: AsyncClient):
res = await client.post(
"/api/v1/leads",
json={"type": "buyout", "name": "李四", "phone": "13900139000"},
)
assert res.status_code == 200
assert res.json()["type"] == "buyout"
async def test_invalid_type_rejected(self, client: AsyncClient):
res = await client.post(
"/api/v1/leads",
json={"type": "wrong", "name": "x", "phone": "1"},
)
assert res.status_code == 400