feat:客服agent接入nl2sql
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
"""按 fin_product 为指定客户生成 fin_holdings 持仓数据(本地开发/演示用)。
|
||||
|
||||
用法:
|
||||
python scripts/seed_holdings_for_customer.py --customer-id 18
|
||||
python scripts/seed_holdings_for_customer.py --customer-id 18 --count 5 --force
|
||||
|
||||
规则:
|
||||
- 只挑选 fin_product 中 status=在售 的产品,按 id 升序取前 count 只;
|
||||
- 每笔持仓的买入净值 = 当前净值 × (1 - 浮动),浮动由固定 seed 生成,保证可复现;
|
||||
- shares = cost_amount / 买入净值(4 位小数),current_value = shares × 当前净值;
|
||||
- profit_loss / profit_ratio 由上述字段推导;
|
||||
- 客户已有持仓时默认拒绝,需 --force 才会追加。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import random
|
||||
import sys
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from config.database.mysql import get_session_factory
|
||||
from model.fin_holdings import FinHoldings
|
||||
from model.fin_product import FinProduct
|
||||
from model.sys_user import SysUser
|
||||
|
||||
_MONEY = Decimal("0.01")
|
||||
_SHARES = Decimal("0.0001")
|
||||
_RATIO = Decimal("0.0001")
|
||||
|
||||
|
||||
def _money(value: Decimal) -> Decimal:
|
||||
return value.quantize(_MONEY, rounding=ROUND_HALF_UP)
|
||||
|
||||
|
||||
async def seed(customer_id: int, *, count: int, force: bool) -> list[dict]:
|
||||
session_factory = get_session_factory()
|
||||
async with session_factory() as db:
|
||||
user = await db.get(SysUser, customer_id)
|
||||
if user is None or user.user_type != "CUSTOMER":
|
||||
raise SystemExit(f"customer_id={customer_id} 不是客户用户或不存在")
|
||||
|
||||
existing = (
|
||||
await db.execute(
|
||||
select(FinHoldings).where(FinHoldings.customer_id == customer_id)
|
||||
)
|
||||
).scalars().all()
|
||||
if existing and not force:
|
||||
raise SystemExit(
|
||||
f"客户 {customer_id} 已有 {len(existing)} 条持仓,如需追加请加 --force"
|
||||
)
|
||||
|
||||
products = (
|
||||
await db.execute(
|
||||
select(FinProduct)
|
||||
.where(FinProduct.status == "在售")
|
||||
.order_by(FinProduct.id)
|
||||
.limit(count)
|
||||
)
|
||||
).scalars().all()
|
||||
if not products:
|
||||
raise SystemExit("fin_product 中没有在售产品")
|
||||
|
||||
rng = random.Random(f"holdings-{customer_id}")
|
||||
created: list[dict] = []
|
||||
for product in products:
|
||||
nav = product.nav or Decimal("1.000000")
|
||||
cost_amount = _money(Decimal(rng.randint(5_000, 50_000)))
|
||||
# 买入净值在当前净值的 88%~97% 之间浮动,形成有涨有跌的持仓
|
||||
buy_nav = _money(nav * Decimal(str(round(rng.uniform(0.88, 0.97), 6))))
|
||||
if buy_nav <= 0:
|
||||
buy_nav = Decimal("1.0000")
|
||||
shares = (cost_amount / buy_nav).quantize(_SHARES, rounding=ROUND_HALF_UP)
|
||||
current_value = _money(shares * nav)
|
||||
profit_loss = _money(current_value - cost_amount)
|
||||
profit_ratio = (profit_loss / cost_amount).quantize(_RATIO, rounding=ROUND_HALF_UP)
|
||||
|
||||
db.add(
|
||||
FinHoldings(
|
||||
customer_id=customer_id,
|
||||
product_id=product.id,
|
||||
shares=shares,
|
||||
cost_amount=cost_amount,
|
||||
current_value=current_value,
|
||||
profit_loss=profit_loss,
|
||||
profit_ratio=profit_ratio,
|
||||
status="持有中",
|
||||
)
|
||||
)
|
||||
created.append(
|
||||
{
|
||||
"product_id": product.id,
|
||||
"product_name": product.product_name,
|
||||
"buy_nav": str(buy_nav),
|
||||
"nav": str(nav),
|
||||
"shares": str(shares),
|
||||
"cost_amount": str(cost_amount),
|
||||
"current_value": str(current_value),
|
||||
"profit_loss": str(profit_loss),
|
||||
"profit_ratio": str(profit_ratio),
|
||||
}
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
return created
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="按 fin_product 生成客户持仓数据")
|
||||
parser.add_argument("--customer-id", type=int, required=True)
|
||||
parser.add_argument("--count", type=int, default=8, help="生成持仓条数(默认 8)")
|
||||
parser.add_argument(
|
||||
"--force", action="store_true", help="客户已有持仓时仍允许追加"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
created = asyncio.run(seed(args.customer_id, count=args.count, force=args.force))
|
||||
total_cost = sum(Decimal(item["cost_amount"]) for item in created)
|
||||
total_value = sum(Decimal(item["current_value"]) for item in created)
|
||||
print(f"客户 {args.customer_id} 新增持仓 {len(created)} 条:")
|
||||
for item in created:
|
||||
print(
|
||||
f" [{item['product_id']}] {item['product_name'][:24]}… "
|
||||
f"买入净值={item['buy_nav']} 当前净值={item['nav']} "
|
||||
f"份额={item['shares']} 成本={item['cost_amount']} "
|
||||
f"市值={item['current_value']} 盈亏={item['profit_loss']} "
|
||||
f"({item['profit_ratio']})"
|
||||
)
|
||||
print(f"合计:成本 {total_cost} 元,市值 {total_value} 元,"
|
||||
f"盈亏 {total_value - total_cost} 元")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,7 +1,18 @@
|
||||
"""读取当前 MySQL 元数据并同步到 Milvus。"""
|
||||
"""同步 NL2SQL 元数据到 Milvus(表名单以 model/ 目录的 ORM 为主)。
|
||||
|
||||
主数据源策略:
|
||||
- 表名单 = model/ 下 SQLAlchemy ORM 定义的全部表(Base.metadata),
|
||||
不再与数据库求交集——数据库尚未建表的 ORM 表也纳入元数据,
|
||||
其表/字段信息从 ORM 定义合成,并在表说明标注"尚未建表";
|
||||
- 数据库里真实存在的 ORM 表,表/列中文注释仍取自 information_schema
|
||||
(列注释只存在于库中,ORM 代码里没有字段级注释);
|
||||
- 名单之外(库里有、model/ 没定义)的表元数据 chunk 会被从 Milvus 删除。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import importlib
|
||||
import pkgutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -14,7 +25,8 @@ from config.database import mysql
|
||||
from config.database.milvus import client as milvus_client
|
||||
from config.database.mysql import get_session_factory
|
||||
from config.settings import settings
|
||||
from nl2sql.metadata_sync import sync_metadata
|
||||
from model.base import Base
|
||||
from nl2sql.metadata_sync import merge_orm_metadata_rows, sync_metadata
|
||||
|
||||
|
||||
TABLES_SQL = text(
|
||||
@@ -36,6 +48,15 @@ COLUMNS_SQL = text(
|
||||
)
|
||||
|
||||
|
||||
def collect_orm_tables() -> set[str]:
|
||||
"""导入 model/ 全部模块,从 Base.metadata 收集 ORM 表名。"""
|
||||
import model
|
||||
|
||||
for module_info in pkgutil.iter_modules(model.__path__):
|
||||
importlib.import_module(f"model.{module_info.name}")
|
||||
return set(Base.metadata.tables.keys())
|
||||
|
||||
|
||||
async def load_information_schema() -> tuple[list[dict], list[dict]]:
|
||||
async with get_session_factory()() as session:
|
||||
tables = [
|
||||
@@ -49,14 +70,43 @@ async def load_information_schema() -> tuple[list[dict], list[dict]]:
|
||||
return tables, columns
|
||||
|
||||
|
||||
async def synchronize() -> int:
|
||||
def _table_name(row: dict) -> str:
|
||||
return str(row.get("TABLE_NAME") or row.get("table_name") or "").strip()
|
||||
|
||||
|
||||
async def synchronize() -> tuple[int, set[str], set[str], set[str]]:
|
||||
"""返回 (upserted, allowed_tables, 名单外表, 未建表的 ORM 表)。"""
|
||||
orm_tables = collect_orm_tables()
|
||||
try:
|
||||
tables, columns = await load_information_schema()
|
||||
return await sync_metadata(milvus_client(), tables, columns)
|
||||
db_tables = {_table_name(row) for row in tables}
|
||||
missing_tables = sorted(orm_tables - db_tables)
|
||||
table_rows, column_rows = merge_orm_metadata_rows(
|
||||
tables,
|
||||
columns,
|
||||
allowed_tables=orm_tables,
|
||||
missing_table_objects=(
|
||||
Base.metadata.tables[name] for name in missing_tables
|
||||
),
|
||||
)
|
||||
upserted = await sync_metadata(
|
||||
milvus_client(),
|
||||
table_rows,
|
||||
column_rows,
|
||||
allowed_tables=orm_tables,
|
||||
)
|
||||
dropped = db_tables - orm_tables
|
||||
return upserted, orm_tables, dropped, set(missing_tables)
|
||||
finally:
|
||||
await mysql.dispose()
|
||||
await milvus_db.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"upserted {asyncio.run(synchronize())} NL2SQL metadata chunks")
|
||||
upserted, allowed, dropped, missing = asyncio.run(synchronize())
|
||||
print(f"ORM 表名单(model/ 全量): {len(allowed)} 张")
|
||||
print(f"其中数据库尚未建表(用 ORM 定义合成元数据): {len(missing)} 张")
|
||||
if missing:
|
||||
print(f" {sorted(missing)}")
|
||||
print(f"已排除的非 ORM 表: {sorted(dropped) if dropped else '无'}")
|
||||
print(f"upserted {upserted} NL2SQL metadata chunks")
|
||||
|
||||
Reference in New Issue
Block a user