This commit is contained in:
zlwu committed 2026-07-12 12:04:43 +08:00
1 parent 7b1d4c8729
commit ffc89d9b57
75 files changed
+12263 -7950

No files matched your search

+1 -1
View File
@@ -14,7 +14,7 @@
# 与 sys_user.tenant_no 一致;登录按 (tenant_no, user_no) 查用户
DEFAULT_TENANT_NO=shanxia2.0
# 用户无 sys_user_role 记录时,Web 管理端 JWT 使用的默认角色(super_admin 可全量菜单)
# WEB_AUTH_DEFAULT_ROLE=super_admin
# WEB_AUTH_DEFAULT_ROLE=admin
# JWT配置示例
# SECRET_KEY=your-secret-key-change-in-production
+1 -1
View File
@@ -1 +1 @@
82692
47712
+2 -1
View File
@@ -525,7 +525,6 @@ def category_add(
"""新增产品分类"""
try:
tenant_no = get_tenant_no(db, _user)
user_info = crm_user_info(_user)
user_id = _user.get("user_id", "") or _user.get("id", "")
data = body.model_dump(exclude_unset=True)
obj = create_category(db, tenant_no=tenant_no, user_id=user_id, **data)
@@ -544,6 +543,7 @@ def category_update(
):
"""更新产品分类"""
try:
user_info = crm_user_info(_user)
user_id = _user.get("user_id", "") or _user.get("id", "")
data = body.model_dump(exclude_unset=True)
category_id = data.pop("id")
@@ -563,6 +563,7 @@ def category_delete(
):
"""删除产品分类"""
try:
user_info = crm_user_info(_user)
soft_delete_category(db, body.id, user_info=user_info)
return success_response(None, message="删除成功")
except ValueError as e:
+1 -3
View File
@@ -398,7 +398,7 @@ def register_contract_routes(router):
try:
tenant_no = get_tenant_no(db, _user)
user_info = crm_user_info(_user)
content = get_template(db, contract_id, tenant_no=tenant_no)
content = get_template(db, contract_id, tenant_no=tenant_no, user_info=user_info)
return success_response(content or "", message="获取模板成功")
except ValueError as e:
return error_response(400, str(e))
@@ -729,7 +729,6 @@ def register_contract_routes(router):
):
try:
tenant_no = get_tenant_no(db, _user)
user_info = crm_user_info(_user)
user_id = _user.get("user_id", "") or _user.get("id", "")
approve_contract_by_contract_id(
db,
@@ -739,7 +738,6 @@ def register_contract_routes(router):
attachments=body.attachments or "",
remark=body.remark or "",
user_id=user_id,
user_info=user_info,
)
db.commit()
return success_response(None, message="审批成功")
+1 -5
View File
@@ -249,7 +249,7 @@ def register_quote_routes(router):
try:
tenant_no = get_tenant_no(db, _user)
user_info = crm_user_info(_user)
content = get_template(db, body.id, tenant_no=tenant_no)
content = get_template(db, body.id, tenant_no=tenant_no, user_info=user_info)
return success_response(content or "")
except ValueError as e:
return error_response(400, str(e))
@@ -627,7 +627,6 @@ def register_quote_routes(router):
):
try:
tenant_no = get_tenant_no(db, _user)
user_info = crm_user_info(_user)
user_id = _user.get("user_id", "") or _user.get("id", "")
approve_quote(
db,
@@ -637,7 +636,6 @@ def register_quote_routes(router):
approval_opinion=body.approvalOpinion or "",
remark=body.remark or "",
user_id=user_id,
user_info=user_info,
)
return success_response(None, message="审批成功")
except ValueError as e:
@@ -653,7 +651,6 @@ def register_quote_routes(router):
):
try:
tenant_no = get_tenant_no(db, _user)
user_info = crm_user_info(_user)
user_id = _user.get("user_id", "") or _user.get("id", "")
withdraw_approval(
db,
@@ -661,7 +658,6 @@ def register_quote_routes(router):
tenant_no=tenant_no,
remark=body.remark or "",
user_id=user_id,
user_info=user_info,
)
return success_response(None, message="撤回成功")
except ValueError as e:
+18 -2
View File
@@ -26,7 +26,11 @@ from app.models.models import SysUser
from app.crud.user import user_crud
from app.core.utils import generate_uuid
from app.core import login_crypto
from app.services.web_auth_roles import iter_user_menu_perm_codes, resolve_web_role_for_user
from app.services.web_auth_roles import (
iter_user_menu_perm_codes,
resolve_web_role_for_user,
user_has_bound_roles,
)
from app.crud.crm.quote import view_by_share_code
router = APIRouter(prefix="", tags=["公共接口"])
@@ -257,6 +261,18 @@ def get_web_auth_menu_codes(request: Request, db: Session = Depends(get_db)):
token = authorization.replace("Bearer ", "")
try:
current_user = get_current_user_by_token(token=token, db=db)
user_id = current_user.get("user_id") or ""
tenant_no = current_user.get("tenant_no")
if not user_has_bound_roles(db, user_id, tenant_no):
return success_response(
{
"isSuperAdmin": False,
"all": False,
"menuCodes": [],
}
)
role = current_user.get("role")
if role == "super_admin":
return success_response(
@@ -268,7 +284,7 @@ def get_web_auth_menu_codes(request: Request, db: Session = Depends(get_db)):
)
try:
codes = iter_user_menu_perm_codes(db, current_user.get("user_id") or "")
codes = iter_user_menu_perm_codes(db, user_id, tenant_no)
menu_codes = sorted(codes)
except OperationalError:
menu_codes = []
+2 -2
View File
@@ -116,8 +116,8 @@ class Settings(BaseSettings):
# 与 docs/sql/shanxia2.0.sql 基线一致:登录按租户 + sys_user.user_no
DEFAULT_TENANT_NO: str = "shanxia2.0"
# 用户未绑定 sys_user_role 时,Web 管理端默认角色(建议 super_admin 便于开发全菜单)
WEB_AUTH_DEFAULT_ROLE: str = "super_admin"
# 无 user_id 等异常链路的 Web 角色兜底;未绑定 sys_user_role 的用户在 resolve_web_role_for_user 中固定为 admin
WEB_AUTH_DEFAULT_ROLE: str = "admin"
# 登录 RSA:私钥 PEM(多实例/负载均衡须配置同一私钥;留空则进程启动时临时生成,仅适合单机开发)
LOGIN_RSA_PRIVATE_KEY_PEM: str = ""
+7 -1
View File
@@ -1342,7 +1342,13 @@ def save_template(
return True
def get_template(db: Session, contract_id: str, *, tenant_no: str) -> str:
def get_template(
db: Session,
contract_id: str,
*,
tenant_no: str,
user_info=None,
) -> str:
entity = _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info)
if not entity:
raise ValueError("合同不存在")
@@ -76,6 +76,8 @@ def _recalc_opportunity_follow_stats(db: Session, opportunity_id: str) -> None:
)
if not opp:
return
# 统计前 flush,确保当前事务内新增/软删的跟进记录已写入,避免 follow_count 少 1
db.flush()
remaining = (
db.query(CrmOpportunityFollow)
.filter(
+32 -36
View File
@@ -384,32 +384,44 @@ def get_product_by_no(db: Session, product_no: str, tenant_no: str) -> Optional[
)
def generate_product_no(db: Session, tenant_no: str) -> str:
"""生成产品编号(格式:CP + yyyyMMdd + 3位序号,如 CP20260530001)"""
today_str = datetime.now().strftime("%Y%m%d")
prefix = f"CP{today_str}"
PRODUCT_NO_PREFIX = "SXWL"
PRODUCT_NO_SEQ_LEN = 6
PRODUCT_NO_GENERATE_MAX_RETRIES = 10
PRODUCT_NO_SEQ_MAX = 10 ** PRODUCT_NO_SEQ_LEN - 1
# 查询今天已有的最大编号
max_no = (
def _max_sxwl_product_seq(db: Session, tenant_no: str) -> int:
"""取当前租户下 SXWL 前缀且后缀为纯数字的产品编号最大流水值"""
prefix_len = len(PRODUCT_NO_PREFIX)
rows = (
db.query(CrmProduct.product_no)
.filter(
CrmProduct.product_no.like(f"{prefix}%"),
CrmProduct.product_no.like(f"{PRODUCT_NO_PREFIX}%"),
CrmProduct.tenant_no == tenant_no,
)
.order_by(CrmProduct.product_no.desc())
.first()
.all()
)
max_seq = 0
for (product_no,) in rows:
if not product_no or len(product_no) <= prefix_len:
continue
suffix = product_no[prefix_len:]
if suffix.isdigit():
max_seq = max(max_seq, int(suffix))
return max_seq
if max_no and max_no[0]:
seq_str = max_no[0][len(prefix):]
try:
seq = int(seq_str) + 1
except ValueError:
seq = 1
else:
seq = 1
return f"{prefix}{seq:03d}"
def generate_product_no(db: Session, tenant_no: str) -> str:
"""生成产品编号(格式:SXWL + 6位递增流水号,如 SXWL000001)"""
seq = _max_sxwl_product_seq(db, tenant_no) + 1
for _ in range(PRODUCT_NO_GENERATE_MAX_RETRIES):
if seq > PRODUCT_NO_SEQ_MAX:
raise ValueError("产品编号序号已达上限")
candidate = f"{PRODUCT_NO_PREFIX}{seq:0{PRODUCT_NO_SEQ_LEN}d}"
if not product_no_exists(db, candidate, tenant_no):
return candidate
seq += 1
raise ValueError("自动生成产品编号失败,请稍后重试")
def generate_barcode(db: Session, tenant_no: str) -> str:
@@ -722,11 +734,6 @@ def soft_delete_oss(db: Session, oss_id: str) -> None:
# ── 报价行产品 ID 解析(对照 CrmProductServiceImpl) ─────────────
QUOTE_AUTO_PRODUCT_NO_PREFIX = "SXWL"
QUOTE_AUTO_PRODUCT_NO_RANDOM_LEN = 8
PRODUCT_NO_GENERATE_MAX_RETRIES = 10
def _normalize_text(value: Any) -> Optional[str]:
if value is None:
return None
@@ -742,19 +749,8 @@ def _get_quote_item_field(data: dict, *keys: str, default=None):
def generate_auto_product_no(db: Session, tenant_no: str) -> str:
"""Java: generateAutoProductNo — SXWL + 8位随机大写字符"""
import secrets
import string
alphabet = string.ascii_uppercase + string.digits
for _ in range(PRODUCT_NO_GENERATE_MAX_RETRIES):
suffix = "".join(
secrets.choice(alphabet) for _ in range(QUOTE_AUTO_PRODUCT_NO_RANDOM_LEN)
)
candidate = f"{QUOTE_AUTO_PRODUCT_NO_PREFIX}{suffix}"
if not product_no_exists(db, candidate, tenant_no):
return candidate
raise ValueError("自动生成产品编号失败,请稍后重试")
"""报价自动建档时生成产品编号(与产品管理新增规则一致)"""
return generate_product_no(db, tenant_no)
def resolve_product_id_for_quote_item(
+1
View File
@@ -479,6 +479,7 @@ class PlTransportDbLink(Base):
plan_product_id = Column(String(64), nullable=False, comment="计划产品行ID")
db_purchase_plan_id = Column(String(64), nullable=False, comment="地磅计划ID")
db_inventory_id = Column(String(64), nullable=False, comment="出入库单ID")
contributed_qty = Column(DECIMAL(15, 3), nullable=True, comment="本次联动贡献量(吨)")
tenant_no = Column(String(64), nullable=False, default="shanxia2.0", comment="租户编号")
order_num = Column(Integer, nullable=True, comment="排序号")
remark = Column(String(512), nullable=True, comment="备注")
+2 -2
View File
@@ -94,7 +94,7 @@ class CustomerCreateBody(BaseModel):
owner_user_id: Optional[str] = Field(default=None, alias="ownerUserId", description="负责人ID")
business_license_img: Optional[str] = Field(
default=None,
validation_alias=AliasChoices("businessLicenseImg", "businessLicenseImages"),
validation_alias=AliasChoices("businessLicenseImages", "businessLicenseImg"),
serialization_alias="businessLicenseImages",
description="营业执照图片URL",
)
@@ -158,7 +158,7 @@ class CustomerUpdateBody(BaseModel):
owner_user_id: Optional[str] = Field(default=None, alias="ownerUserId", description="负责人ID")
business_license_img: Optional[str] = Field(
default=None,
validation_alias=AliasChoices("businessLicenseImg", "businessLicenseImages"),
validation_alias=AliasChoices("businessLicenseImages", "businessLicenseImg"),
serialization_alias="businessLicenseImages",
description="营业执照图片URL",
)
+1 -1
View File
@@ -71,7 +71,7 @@ class ProductCreateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
product_no: Optional[str] = Field(default=None, alias="productNo", description="产品编号(不传则自动生成)")
product_no: Optional[str] = Field(default=None, alias="productNo", description="产品编号(不传则自动生成,格式 SXWL000001)")
product_name: str = Field(..., alias="productName", description="产品名称")
category_id: Optional[str] = Field(default=None, alias="categoryId", description="分类ID")
category_name: Optional[str] = Field(default=None, alias="categoryName", description="分类名称")
+107 -11
View File
@@ -46,6 +46,41 @@ def resolve_partner_company(plan: PlPlan, plan_type: str) -> str:
return ""
def _normalize_to_date(value: date | datetime | str | None) -> Optional[date]:
if value is None:
return None
if isinstance(value, datetime):
return value.date()
if isinstance(value, date):
return value
if isinstance(value, str):
return parse_date_str(value)
return None
def _normalize_plan_date(plan: PlPlan) -> date:
plan_date = plan.plan_date
if isinstance(plan_date, datetime):
return plan_date.date()
if isinstance(plan_date, date):
return plan_date
if isinstance(plan_date, str):
parsed = parse_date_str(plan_date)
return parsed or date.today()
return date.today()
def _resolve_dibang_date(plan: PlPlan) -> date:
"""入库单用送达时间,出库单用取货时间;缺失时回退计划日期。"""
if plan.plan_type == "inbound":
resolved = _normalize_to_date(plan.delivery_time)
elif plan.plan_type == "outbound":
resolved = _normalize_to_date(plan.pickup_time)
else:
resolved = None
return resolved or _normalize_plan_date(plan)
def _month_range(plan_date: date) -> tuple[date, date]:
month_start = plan_date.replace(day=1)
last_day = calendar.monthrange(plan_date.year, plan_date.month)[1]
@@ -68,6 +103,54 @@ def _product_quantity(product: PlPlanProduct) -> float:
return 0.0
def _apply_plan_quantity_delta(
db_plan: DbPurchasePlan,
delta: float,
*,
user_id: Optional[str],
) -> None:
current = float(db_plan.planned_quantity or 0)
db_plan.planned_quantity = max(0.0, current + float(delta))
db_plan.update_by = user_id
def _resolve_link_contributed_qty(
link: PlTransportDbLink,
inventory: Optional[DbInboundOutbound],
) -> float:
if link.contributed_qty is not None:
return float(link.contributed_qty)
if inventory and inventory.quantity is not None:
return float(inventory.quantity)
return 0.0
def _subtract_plan_contribution(
db: Session,
*,
tenant_no: str,
link: PlTransportDbLink,
inventory: Optional[DbInboundOutbound],
user_id: Optional[str],
) -> None:
if not link.db_purchase_plan_id:
return
db_plan = (
db.query(DbPurchasePlan)
.filter(
DbPurchasePlan.id == link.db_purchase_plan_id,
DbPurchasePlan.tenant_no == tenant_no,
DbPurchasePlan.del_flag == 0,
)
.first()
)
if not db_plan:
return
qty = _resolve_link_contributed_qty(link, inventory)
if qty:
_apply_plan_quantity_delta(db_plan, -qty, user_id=user_id)
def _generate_batch_no(db: Session, tenant_no: str) -> str:
rows = (
db.query(DbInboundOutbound.batch_no)
@@ -124,21 +207,23 @@ def _create_db_plan(
product: PlPlanProduct,
business_type: str,
customer_name: str,
dibang_date: date,
month_start: date,
month_end: date,
user_id: Optional[str],
dept_id: str,
contributed_qty: float,
) -> DbPurchasePlan:
snapshot = resolve_crm_product(db, tenant_no, product.product_id)
db_plan = DbPurchasePlan(
id=generate_uuid(),
tenant_no=tenant_no,
plan_no=generate_plan_no(),
plan_date=plan.plan_date,
plan_date=dibang_date,
start_date=month_start,
end_date=month_end,
business_type=business_type,
planned_quantity=Decimal(str(_product_quantity(product))),
planned_quantity=contributed_qty,
vehicle_no=(plan.license_plate or "").strip(),
customer_name=customer_name or None,
remark=f"来源运输计划 {plan.plan_no}",
@@ -167,6 +252,7 @@ def _create_db_inventory(
business_type: str,
customer_name: str,
license_plate: str,
dibang_date: date,
user_id: Optional[str],
dept_id: str,
) -> DbInboundOutbound:
@@ -178,7 +264,7 @@ def _create_db_inventory(
inventory = DbInboundOutbound(
id=next_inbound_outbound_id(db, tenant_no),
tenant_no=tenant_no,
date=plan.plan_date,
date=dibang_date,
partner_company=customer_name,
business_type=business_type,
quantity=Decimal(str(_product_quantity(product))),
@@ -280,6 +366,13 @@ def revoke_regeneratable_dibang_for_transport(
.first()
)
if inventory:
_subtract_plan_contribution(
db,
tenant_no=tenant_no,
link=link,
inventory=inventory,
user_id=user_id,
)
inventory.del_flag = 1
inventory.update_by = user_id
link.del_flag = 1
@@ -306,13 +399,8 @@ def _sync_transport_inventory_links(
if not partner:
raise ValueError("无法解析往来单位,请检查合同供方/需方信息")
plan_date = plan.plan_date
if isinstance(plan_date, datetime):
plan_date = plan_date.date()
elif isinstance(plan_date, str):
parsed = parse_date_str(plan_date)
plan_date = parsed or date.today()
month_start, month_end = _month_range(plan_date)
dibang_date = _resolve_dibang_date(plan)
month_start, month_end = _month_range(dibang_date)
license_plate = (transport.license_plate or plan.license_plate or "").strip()
@@ -322,10 +410,12 @@ def _sync_transport_inventory_links(
if not product.product_id:
raise ValueError(f"产品【{product.product_name}】未关联 CRM 产品,无法生成地磅出入库单")
contributed_qty = _product_quantity(product)
db_plan = find_monthly_db_plan(
db,
tenant_no,
plan_date,
dibang_date,
business_type,
partner,
product.product_id,
@@ -339,11 +429,15 @@ def _sync_transport_inventory_links(
product,
business_type,
partner,
dibang_date,
month_start,
month_end,
user_id,
dept_id,
contributed_qty,
)
else:
_apply_plan_quantity_delta(db_plan, contributed_qty, user_id=user_id)
inventory = _create_db_inventory(
db,
@@ -354,6 +448,7 @@ def _sync_transport_inventory_links(
business_type,
partner,
license_plate,
dibang_date,
user_id,
dept_id,
)
@@ -366,6 +461,7 @@ def _sync_transport_inventory_links(
plan_product_id=product.id,
db_purchase_plan_id=db_plan.id if db_plan else "",
db_inventory_id=inventory.id,
contributed_qty=Decimal(str(contributed_qty)),
remark=f"运输计划 {plan.plan_no}",
create_by=user_id,
create_dept_id=dept_id,
+87 -5
View File
@@ -319,6 +319,78 @@ def clear_hazard_cert_ready(plan: PlPlan) -> None:
plan.hazard_cert_ready_time = None
def _required_hazard_cert_fields(is_toxic: int, is_explosive: int) -> list[str]:
fields: list[str] = []
if is_toxic:
fields.extend(["purchase_cert_url", "transport_cert_url"])
if is_explosive:
fields.append("legal_use_cert_url")
return fields
def _is_hazard_product_cert_complete(
product: PlPlanProduct,
*,
is_toxic: int,
is_explosive: int,
) -> bool:
for field in _required_hazard_cert_fields(is_toxic, is_explosive):
if not str(getattr(product, field, "") or "").strip():
return False
return True
def all_hazard_products_cert_complete(
db: Session,
tenant_no: str,
products: list[PlPlanProduct],
) -> bool:
"""计划内所有危化品产品的所需证书槽位均已上传。"""
if not products:
return True
hazard_map = fetch_hazard_flags_map(
db,
tenant_no,
[product.product_id for product in products if product.product_id],
)
has_hazard = False
for product in products:
is_toxic, is_explosive = resolve_hazard_flags_from_crm(
db,
tenant_no,
product.product_id,
hazard_map=hazard_map,
)
if not (is_toxic or is_explosive):
continue
has_hazard = True
if not _is_hazard_product_cert_complete(
product,
is_toxic=is_toxic,
is_explosive=is_explosive,
):
return False
return has_hazard
def maybe_auto_set_hazard_cert_ready(
db: Session,
*,
tenant_no: str,
plan: PlPlan,
products: list[PlPlanProduct],
user_id: str,
) -> None:
"""全部危化品证书已上传时,自动标记证书齐备(与人工确认等效)。"""
if int(plan.hazard_cert_ready or 0):
return
if not all_hazard_products_cert_complete(db, tenant_no, products):
return
plan.hazard_cert_ready = 1
plan.hazard_cert_ready_by = user_id
plan.hazard_cert_ready_time = datetime.now()
def validate_hazard_cert_ready_for_transport(
db: Session,
tenant_no: str,
@@ -327,11 +399,14 @@ def validate_hazard_cert_ready_for_transport(
) -> None:
if not plan_has_hazard_products(db, tenant_no, products):
return
if not int(plan.hazard_cert_ready or 0):
raise HTTPException(
status_code=400,
detail="该计划危化品证书尚未确认齐备,请至危化品证上传页操作",
)
if int(plan.hazard_cert_ready or 0):
return
if all_hazard_products_cert_complete(db, tenant_no, products):
return
raise HTTPException(
status_code=400,
detail="该计划危化品证书尚未确认齐备,请至危化品证上传页操作",
)
def set_hazard_cert_ready(
@@ -446,6 +521,13 @@ def update_plan_product_certs(
row.update_by = user_id
updated.append(row)
maybe_auto_set_hazard_cert_ready(
db,
tenant_no=tenant_no,
plan=plan,
products=existing_rows,
user_id=user_id,
)
plan.update_by = user_id
db.commit()
for row in updated:
+18 -2
View File
@@ -39,6 +39,17 @@ def _role_rows_for_user(db: Session, user_id: str, tenant_no: str):
)
def user_has_bound_roles(db: Session, user_id: str, tenant_no: Optional[str] = None) -> bool:
"""用户是否在 sys_user_role 中绑定了至少一个有效角色。"""
if not user_id:
return False
tenant = (tenant_no or "").strip() or settings.DEFAULT_TENANT_NO
rows = _role_rows_for_user(db, user_id, tenant)
if not rows and tenant != settings.DEFAULT_TENANT_NO:
rows = _role_rows_for_user(db, user_id, settings.DEFAULT_TENANT_NO)
return bool(rows)
def resolve_web_role_for_user(db: Session, user_id: str, tenant_no: Optional[str] = None) -> str:
"""根据用户关联角色解析 Web 端 role(admin / super_admin)。"""
if not user_id:
@@ -49,17 +60,22 @@ def resolve_web_role_for_user(db: Session, user_id: str, tenant_no: Optional[str
rows = _role_rows_for_user(db, user_id, settings.DEFAULT_TENANT_NO)
names = [str(r[0]).strip() for r in rows if r and r[0]]
if not names:
return settings.WEB_AUTH_DEFAULT_ROLE
# 未绑定角色:视为普通 admin,无菜单权限(不可默认 super_admin 全菜单)
return "admin"
mapped = [_map_role_name_to_web(n) for n in names]
if "super_admin" in mapped:
return "super_admin"
return "admin"
def iter_user_menu_perm_codes(db: Session, user_id: str) -> Set[str]:
def iter_user_menu_perm_codes(
db: Session, user_id: str, tenant_no: Optional[str] = None
) -> Set[str]:
"""用户通过角色关联得到的菜单权限标识(sys_menu.perms,空则退回菜单 id)。"""
if not user_id:
return set()
if not user_has_bound_roles(db, user_id, tenant_no):
return set()
tenant = settings.DEFAULT_TENANT_NO
try:
q = (
+12 -2
View File
@@ -122,7 +122,6 @@
"120": "Community 120",
"121": "Community 121",
"122": "Community 122",
"123": "Community 123",
"124": "Community 124",
"125": "Community 125",
"126": "Community 126",
@@ -149,5 +148,16 @@
"147": "Community 147",
"148": "Community 148",
"149": "Community 149",
"150": "Community 150"
"151": "Community 151",
"152": "Community 152",
"153": "Community 153",
"154": "Community 154",
"155": "Community 155",
"156": "Community 156",
"157": "Community 157",
"158": "Community 158",
"159": "Community 159",
"160": "Community 160",
"162": "Community 162",
"169": "Community 169"
}
+261 -207
View File
@@ -1,16 +1,16 @@
# Graph Report - backend (2026-07-11)
# Graph Report - backend (2026-07-12)
## Corpus Check
- 244 files · ~171,565 words
- 245 files · ~172,306 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 5357 nodes · 38453 edges · 151 communities (148 shown, 3 thin omitted)
- Extraction: 51% EXTRACTED · 49% INFERRED · 0% AMBIGUOUS · INFERRED: 18810 edges (avg confidence: 0.5)
- 5436 nodes · 38642 edges · 161 communities (159 shown, 2 thin omitted)
- Extraction: 51% EXTRACTED · 49% INFERRED · 0% AMBIGUOUS · INFERRED: 18828 edges (avg confidence: 0.5)
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `843e13bc`
- Built from commit: `7b1d4c87`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
@@ -138,7 +138,6 @@
- [[_COMMUNITY_Community 120|Community 120]]
- [[_COMMUNITY_Community 121|Community 121]]
- [[_COMMUNITY_Community 122|Community 122]]
- [[_COMMUNITY_Community 123|Community 123]]
- [[_COMMUNITY_Community 124|Community 124]]
- [[_COMMUNITY_Community 125|Community 125]]
- [[_COMMUNITY_Community 126|Community 126]]
@@ -155,7 +154,18 @@
- [[_COMMUNITY_Community 138|Community 138]]
- [[_COMMUNITY_Community 148|Community 148]]
- [[_COMMUNITY_Community 149|Community 149]]
- [[_COMMUNITY_Community 150|Community 150]]
- [[_COMMUNITY_Community 151|Community 151]]
- [[_COMMUNITY_Community 152|Community 152]]
- [[_COMMUNITY_Community 153|Community 153]]
- [[_COMMUNITY_Community 154|Community 154]]
- [[_COMMUNITY_Community 155|Community 155]]
- [[_COMMUNITY_Community 156|Community 156]]
- [[_COMMUNITY_Community 157|Community 157]]
- [[_COMMUNITY_Community 158|Community 158]]
- [[_COMMUNITY_Community 159|Community 159]]
- [[_COMMUNITY_Community 160|Community 160]]
- [[_COMMUNITY_Community 162|Community 162]]
- [[_COMMUNITY_Community 169|Community 169]]
## God Nodes (most connected - your core abstractions)
1. `Session` - 1521 edges
@@ -181,110 +191,110 @@
- `Any` --uses--> `OperLogMeta` [INFERRED]
scripts/migrate_dibang_data.py → app/core/oper_log_context.py
## Communities (151 total, 3 thin omitted)
## Communities (161 total, 2 thin omitted)
### Community 0 - "Community 0"
Cohesion: 0.10
Nodes (107): date, DbUserContext, int, Session, str, VehicleRechargeApplyCreate, VehicleRechargeApplyRead, VehicleRechargeApplyUpdate (+99 more)
Nodes (112): date, DbUserContext, int, Session, str, VehicleMaintenanceApplyCreate, VehicleMaintenanceApplyRead, VehicleMaintenanceApplyReminder (+104 more)
### Community 1 - "Community 1"
Cohesion: 0.35
Nodes (219): Any, bool, str, UploadFile, CRM 线索管理 Pydantic Schemas, CRM 客户管理 Pydantic Schemas, CRM 商机管理 Pydantic Schemas, CRM 客户公海池规则 Pydantic Schemas (+211 more)
### Community 2 - "Community 2"
Cohesion: 0.19
Nodes (39): ProcessDefCreateBody, ProcessDefUpdateBody, PublishVersionBody, StartInstanceBody, Any, bool, WfProcessInstance, bool (+31 more)
Cohesion: 0.22
Nodes (29): Any, int, Session, str, create_instance(), create_process_def(), create_task(), get_all_tasks_for_business() (+21 more)
### Community 3 - "Community 3"
Cohesion: 0.07
Nodes (168): _apply_default_invoice_info_to_customers(), category_add(), category_delete(), category_tree(), category_update(), clue_assign(), clue_check_convert(), clue_check_duplicate() (+160 more)
Nodes (58): CRM 合同 API 路由 — GET/PUT/DELETE/POST 对齐原 Java CRM, str, CRM 合同 Pydantic Schemas — 对照 CrmContractBo / CrmContractVo, ContractApprovalActionBody, ContractApprovalApproveBody, ContractApprovalByContractBody, ContractApprovalExportBody, ContractApprovalListBody (+50 more)
### Community 4 - "Community 4"
Cohesion: 0.07
Nodes (113): bool, DbUserContext, int, 运输计划管理 - 基础数据 API(承运商、车辆、人员), PlCarrier, PlVehicle, Session, str (+105 more)
Nodes (112): bool, DbUserContext, int, 运输计划管理 - 基础数据 API(承运商、车辆、人员), PlCarrier, PlVehicle, Session, str (+104 more)
### Community 5 - "Community 5"
Cohesion: 0.22
Nodes (28): register_quote_routes(), bool, int, Session, str, accept_quote(), _ensure_quote_in_scope(), generate_pdf() (+20 more)
Cohesion: 0.21
Nodes (36): register_quote_routes(), bool, CRM 报价单 CRUD — 对照 CrmQuoteServiceImpl.java(P2 全量), Session, str, accept_quote(), create_quote(), _enrich_contact_fields() (+28 more)
### Community 6 - "Community 6"
Cohesion: 0.08
Nodes (91): Any, Any, CRM 合同 CRUD — 对照 CrmContractServiceImpl.java(列表/详情 P1), CrmContract, CrmContractProductPriceTier, date, datetime, Decimal (+83 more)
Cohesion: 0.05
Nodes (78): 运输计划管理 - 基础数据 Schema(承运商、车辆、人员、证书), Session, str, TransportInventoryLinkRead, str, TransportSiteLinkRead, CarrierAuditReject, CarrierBase (+70 more)
### Community 7 - "Community 7"
Cohesion: 0.08
Nodes (114): bool, DbUserContext, float, int, PlPlan, PlTransport, Session, str (+106 more)
Cohesion: 0.06
Nodes (63): int, PlPlan, PlTransport, Session, str, UploadFile, Any, PlPlanTransport (+55 more)
### Community 8 - "Community 8"
Cohesion: 0.04
Nodes (115): Any, bool, CRM 产品模块 CRUD(产品分类 + 产品 + 附件), int, Session, str, SysOss, int (+107 more)
Nodes (118): Any, bool, CRM 产品模块 CRUD(产品分类 + 产品 + 附件), int, Session, str, SysOss, int (+110 more)
### Community 9 - "Community 9"
Cohesion: 0.25
Nodes (19): DbUserContext, Session, str, Session, str, int, Session, str (+11 more)
Cohesion: 0.07
Nodes (72): CertRead, date, int, Session, str, int, Session, str (+64 more)
### Community 10 - "Community 10"
Cohesion: 0.06
Nodes (122): bool, Decimal, int, PlanCreateFull, PlanDriverFeedbackUpdateBody, PlanSiteFeedbackUpdateBody, PlPlan, PlPlanProduct (+114 more)
Cohesion: 0.05
Nodes (126): bool, Decimal, int, PlPlan, PlPlanProduct, PlTransport, PlVehicle, Session (+118 more)
### Community 11 - "Community 11"
Cohesion: 0.11
Nodes (104): date, DbUserContext, int, Session, str, VehicleUreaApplyCreate, VehicleUreaApplyRead, VehicleUreaApplyUpdate (+96 more)
Nodes (98): date, DbUserContext, int, Session, str, VehicleUreaApplyCreate, VehicleUreaApplyRead, VehicleUreaApplyUpdate (+90 more)
### Community 12 - "Community 12"
Cohesion: 0.08
Nodes (96): attachment_download_url(), attachment_preview_url(), AttachmentDownloadUrlBody, create_dept(), create_dict_data(), create_dict_type(), create_menu(), create_post() (+88 more)
Cohesion: 0.06
Nodes (169): _apply_default_invoice_info_to_customers(), category_add(), category_delete(), category_tree(), category_update(), clue_assign(), clue_check_convert(), clue_check_duplicate() (+161 more)
### Community 13 - "Community 13"
Cohesion: 0.05
Nodes (44): get_plan_arrange_info(), patch_plan_product_certs(), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输) (+36 more)
### Community 14 - "Community 14"
Cohesion: 0.07
Nodes (46): Exception, bool, bytes, int, Path, str, str, str (+38 more)
Cohesion: 0.05
Nodes (74): BxUserContext, Session, str, UploadFile, Any, bool, bytes, int (+66 more)
### Community 15 - "Community 15"
Cohesion: 0.16
Nodes (89): date, DbUserContext, int, PlanCreateFull, PlanDriverFeedbackUpdateBody, PlanSiteFeedbackUpdateBody, PlPlan, Session (+81 more)
Cohesion: 0.11
Nodes (126): date, DbUserContext, int, PlanCreateFull, PlanDriverFeedbackUpdateBody, PlanSiteFeedbackUpdateBody, PlPlan, Session (+118 more)
### Community 16 - "Community 16"
Cohesion: 0.04
Nodes (69): 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list (+61 more)
Cohesion: 0.03
Nodes (67): invoice_info_list(), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list (+59 more)
### Community 17 - "Community 17"
Cohesion: 0.09
Nodes (59): Any, bool, CrmContract, datetime, int, Query, Session, str (+51 more)
Nodes (57): Any, bool, CrmContract, datetime, int, Query, Session, str (+49 more)
### Community 18 - "Community 18"
Cohesion: 0.16
Nodes (32): Any, bool, BxUserContext, BxWorkflowNode, float, int, ReimburseApplyOut, Session (+24 more)
Nodes (33): Any, bool, BxUserContext, BxWorkflowNode, float, int, ReimburseApplyOut, Session (+25 more)
### Community 19 - "Community 19"
Cohesion: 0.25
Nodes (8): Java: validateFrameworkContractForSubmitApproval, Java: validateFrameworkContractForSubmitApproval, Java: validateFrameworkContractForSubmitApproval, Java: validateFrameworkContractForSubmitApproval, Java: validateFrameworkContractForSubmitApproval, Java: validateFrameworkContractForSubmitApproval, Java: validateFrameworkContractForSubmitApproval, validate_framework_contract_for_submit_approval()
Cohesion: 0.19
Nodes (18): str, Any, bool, Session, str, CRM 模块 Excel 导出列定义(字段与字典类型对齐 Java *Vo @ExcelDictFormat)。, _yes_no(), _build_workbook() (+10 more)
### Community 20 - "Community 20"
Cohesion: 0.07
Nodes (97): Any, int, Session, str, Any, bool, CRM 客户 CRUD(客户主表 + 转移/成交/公海池操作), CrmCustomer (+89 more)
Cohesion: 0.08
Nodes (40): AttachmentUploadResponse, ChangePasswordRequest, Config, LoginRequest, LoginResponse, Pydantic schemas - 用户认证, ResetPasswordRequest, UserCreate (+32 more)
### Community 21 - "Community 21"
Cohesion: 0.11
Nodes (57): Any, bool, int, Session, str, Any, bool, CRM 线索 CRUD(线索主表 + 转化/分配/无效/等级操作) (+49 more)
### Community 22 - "Community 22"
Cohesion: 0.06
Nodes (106): bool, DbInboundOutbound, int, PublicInventoryFeedback, PublicInventoryPageRead, Session, str, UploadFile (+98 more)
Cohesion: 0.19
Nodes (37): bool, DbInboundOutbound, int, PublicInventoryFeedback, PublicInventoryPageRead, UploadFile, bool, int (+29 more)
### Community 23 - "Community 23"
Cohesion: 0.33
Nodes (25): DbUserContext, DbWeighingSlip, Session, str, WeighingSlipRead, user_info_from_context(), weighing_to_read(), apply_product_snapshot_to_weighing() (+17 more)
Cohesion: 0.09
Nodes (116): date, DbUserContext, int, Session, str, VehicleRechargeApplyCreate, VehicleRechargeApplyRead, VehicleRechargeApplyUpdate (+108 more)
### Community 24 - "Community 24"
Cohesion: 0.25
Nodes (18): BxUserContext, Session, str, bool, BxUserContext, Session, str, create_expense_item() (+10 more)
### Community 25 - "Community 25"
Cohesion: 0.13
Cohesion: 0.14
Nodes (29): Any, bool, BxUserContext, BxWorkflowNode, float, int, Session, str (+21 more)
### Community 26 - "Community 26"
@@ -292,168 +302,168 @@ Cohesion: 0.18
Nodes (50): Any, CRM 商机 CRUD(主表 + 阶段/赢输单/统计/看板/漏斗), CrmOpportunity, date, datetime, Decimal, int, Session (+42 more)
### Community 27 - "Community 27"
Cohesion: 0.12
Nodes (55): DbInboundOutbound, DbWeighingSlip, InboundOutboundRead, WeighingSlipRead, bool, datetime, DbWeighingSlip, Session (+47 more)
Cohesion: 0.10
Nodes (58): bool, date, DbInboundOutbound, DbPurchasePlan, float, PlPlan, PlPlanProduct, PlTransport (+50 more)
### Community 28 - "Community 28"
Cohesion: 0.08
Nodes (45): bool, Session, str, Any, int, Session, str, _append_user_scope_conditions() (+37 more)
Cohesion: 0.12
Nodes (30): Session, str, _append_user_scope_conditions(), apply_data_scope(), _collect_scope_ids(), _get_child_dept_ids(), get_user_dept_id(), 数据权限过滤(V0.3:基于角色 data_scope 的查询级行级过滤) data_scope 值: 1 = 全部数据 2 = 本部门数据 3 (+22 more)
### Community 29 - "Community 29"
Cohesion: 0.11
Nodes (49): register_contract_routes(), bool, Session, bool, batch_submit_approval(), _ensure_contract_in_scope(), execute_change(), finish_contract() (+41 more)
Cohesion: 0.10
Nodes (90): Any, register_contract_routes(), CRM 合同子模块路由 — 变更/模板/审批独立页, register_contract_sub_routes(), Any, bool, CRM 合同 CRUD — 对照 CrmContractServiceImpl.java(列表/详情 P1), Session (+82 more)
### Community 30 - "Community 30"
Cohesion: 0.36
Nodes (31): AdvanceApplyCreate, AdvanceApplyOut, AdvanceApplyUpdate, AdvanceApprovalCreate, BxUserContext, int, Session, str (+23 more)
Cohesion: 0.16
Nodes (51): AdvanceApplyCreate, AdvanceApplyOut, AdvanceApplyUpdate, AdvanceApprovalCreate, BxUserContext, int, Session, str (+43 more)
### Community 31 - "Community 31"
Cohesion: 0.11
Nodes (50): 公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/shanxia2.0.sql 基线), refresh_token(), bool, Redis, Request, Session, str, _T (+42 more)
Cohesion: 0.12
Nodes (44): 公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/shanxia2.0.sql 基线), refresh_token(), bool, Redis, Request, str, _T, bool (+36 more)
### Community 32 - "Community 32"
Cohesion: 0.05
Nodes (110): Any, bool, datetime, Decimal, int, PlTransport, Session, str (+102 more)
Cohesion: 0.07
Nodes (118): DbUserContext, FreightSettlementDisburseBody, FreightSettlementSubmitApprovalBody, FreightSettlementUpdate, int, Session, str, bool (+110 more)
### Community 33 - "Community 33"
Cohesion: 0.15
Nodes (30): Any, bool, int, Session, str, approve_quote(), can_user_approve(), create_approval_records() (+22 more)
### Community 34 - "Community 34"
Cohesion: 0.07
Nodes (28): create_plan_full(), 创建运输计划(完整表单:合同产品 + 可选运输安排), 创建运输计划(完整表单:合同产品 + 可选运输安排), 创建运输计划(完整表单:合同产品 + 可选运输安排), 创建运输计划(完整表单:合同产品 + 可选预选车辆,不自动安排运输), 创建运输计划(完整表单:合同产品 + 可选预选车辆,不自动安排运输), 创建运输计划(完整表单:合同产品 + 可选预选车辆,不自动安排运输), 创建运输计划(完整表单:合同产品 + 可选预选车辆,不自动安排运输) (+20 more)
Cohesion: 0.04
Nodes (48): create_plan_full(), _generate_plan_no(), 创建运输计划(完整表单:合同产品 + 可选运输安排), 创建运输计划(完整表单:合同产品 + 可选运输安排), 创建运输计划(完整表单:合同产品 + 可选运输安排), 创建运输计划(完整表单:合同产品 + 可选预选车辆,不自动安排运输), 创建运输计划(完整表单:合同产品 + 可选预选车辆,不自动安排运输), 创建运输计划(完整表单:合同产品 + 可选预选车辆,不自动安排运输) (+40 more)
### Community 35 - "Community 35"
Cohesion: 0.13
Nodes (97): date, DbUserContext, int, Session, str, VehicleMaintenanceApplyCreate, VehicleMaintenanceApplyRead, VehicleMaintenanceApplyReminder (+89 more)
Cohesion: 0.21
Nodes (60): InboundOutboundRead, bool, PurchasePlanRead, PurchasePlanWithFillLink, DbUserContext, DbWeighingSlip, Session, str (+52 more)
### Community 36 - "Community 36"
Cohesion: 0.15
Nodes (16): _avatar_url_field(), _build_user_info_payload(), _jwt_user_payload(), 与 web-admin `userInfo` 字段约定一致。, 与 web-admin `userInfo` 字段约定一致。, Web 端登录:账号为 sys_user.user_no,密码为 sys_user.password(bcrypt)。, Web 端登录:账号为 sys_user.user_no,密码为 sys_user.password(bcrypt)。, Web 端登录:账号为 sys_user.user_no,密码为 sys_user.password(bcrypt)。 (+8 more)
Cohesion: 0.40
Nodes (5): bool, str, get_password_hash(), 验证密码(哈希非 bcrypt 或损坏时返回 False,避免抛错), verify_password()
### Community 37 - "Community 37"
Cohesion: 0.13
Nodes (39): Any, bool, date, int, Session, str, Session, str (+31 more)
Nodes (40): Any, bool, date, int, Session, str, Session, str (+32 more)
### Community 38 - "Community 38"
Cohesion: 0.18
Nodes (41): AdvanceIssueRequest, BxUserContext, int, ReimburseApplyOut, Session, str, datetime, str (+33 more)
Nodes (44): AdvanceIssueRequest, BxUserContext, int, ReimburseApplyOut, Session, str, datetime, str (+36 more)
### Community 39 - "Community 39"
Cohesion: 0.22
Nodes (38): DbInboundOutbound, DbUserContext, InboundOutboundRead, Session, str, CreateLinkForPlanPayload, _apply_inventory_product_update(), _apply_inventory_update() (+30 more)
Cohesion: 0.14
Nodes (40): DbInboundOutbound, DbUserContext, Session, str, Session, str, user_info_from_context(), _apply_inventory_product_update() (+32 more)
### Community 40 - "Community 40"
Cohesion: 0.02
Nodes (197): CRM 报价单 API 路由 — 挂接到 crm.router, CRM 报价单 Pydantic Schemas, Decimal, Decimal, BaseModel, AdvanceApplyCreate, AdvanceApplyOut, AdvanceApplyUpdate (+189 more)
Cohesion: 0.15
Nodes (33): CRM 报价单 API 路由 — 挂接到 crm.router, CRM 报价单 Pydantic Schemas, BaseModel, QuoteAcceptBody, QuoteApprovalApproveBody, QuoteApprovalListBody, QuoteApprovalRefreshNamesBody, QuoteApprovalWithdrawBody (+25 more)
### Community 41 - "Community 41"
Cohesion: 0.07
Nodes (39): _db_pool_keepalive_loop(), _error_json(), lifespan(), _mask_secret(), Exception, int, Request, str (+31 more)
Nodes (42): _db_pool_keepalive_loop(), _error_json(), lifespan(), _mask_secret(), Exception, int, Request, str (+34 more)
### Community 42 - "Community 42"
Cohesion: 0.35
Nodes (31): BxUserContext, int, Session, str, PagedData, approve_reimburse(), approved_by_me(), _build_details_data() (+23 more)
Nodes (30): BxUserContext, int, Session, str, approve_reimburse(), approved_by_me(), _build_details_data(), create_reimburse() (+22 more)
### Community 43 - "Community 43"
Cohesion: 0.13
Nodes (37): Any, bool, Decimal, int, Session, str, batch_insert_quote_products(), calculate_subtotal() (+29 more)
### Community 44 - "Community 44"
Cohesion: 0.09
Nodes (94): DbUserContext, FreightSettlementDisburseBody, FreightSettlementSubmitApprovalBody, FreightSettlementUpdate, int, Session, str, FreightSettlementDisburseBody (+86 more)
Cohesion: 0.07
Nodes (95): Any, int, Session, str, Any, bool, CRM 客户 CRUD(客户主表 + 转移/成交/公海池操作), CrmCustomer (+87 more)
### Community 45 - "Community 45"
Cohesion: 0.13
Nodes (15): Any, bool, int, str, get_wechat_pay_client(), get_wechat_pay_v2(), 微信支付工具模块 支持微信支付 V3 API 文档参考: https://pay.weixin.qq.com/docs/v3/apis/index.html, 统一下单接口 (V3) Args: out_trade_no: 商户订单号 total_amo (+7 more)
### Community 46 - "Community 46"
Cohesion: 0.07
Nodes (56): CRM 合同 API 路由 — GET/PUT/DELETE/POST 对齐原 Java CRM, CRM 合同子模块路由 — 变更/模板/审批独立页, register_contract_sub_routes(), int, CRM 合同 Pydantic Schemas — 对照 CrmContractBo / CrmContractVo, ContractApprovalActionBody, ContractApprovalApproveBody, ContractApprovalByContractBody (+48 more)
Cohesion: 0.13
Nodes (30): Any, bool, CRM 供方信息 CRUD — 对照 CrmSupplierInfoServiceImpl.java, int, Session, str, _apply_list_filters(), create_supplier_info() (+22 more)
### Community 47 - "Community 47"
Cohesion: 0.16
Nodes (33): Any, bool, int, Session, str, Any, Session, str (+25 more)
### Community 48 - "Community 48"
Cohesion: 0.42
Nodes (53): date, DbUserContext, int, Session, str, DocumentReminderRead, DocumentReminderUpdate, InventoryLedgerResponse (+45 more)
Cohesion: 0.14
Nodes (121): date, DbUserContext, int, Session, str, bool, DbUserContext, float (+113 more)
### Community 49 - "Community 49"
Cohesion: 0.09
Nodes (43): 运输计划管理 - 基础数据 Schema(承运商、车辆、人员、证书), CarrierAuditReject, CarrierBase, CarrierCreate, CarrierListResponse, CarrierQualificationBase, CarrierRead, CarrierUpdate (+35 more)
Cohesion: 0.24
Nodes (20): Any, bool, str, _actor_ids(), _actor_names(), assert_row_deletable_by_creator(), assert_rows_deletable_by_creator(), can_delete_by_creator() (+12 more)
### Community 50 - "Community 50"
Cohesion: 0.18
Nodes (15): datetime, int, str, datetime, 审批轮次过滤 — 驳回后重新提交时,进度接口仅展示当前轮次。, format_datetime(), generate_id(), generate_random_password() (+7 more)
Cohesion: 0.12
Nodes (31): datetime, int, str, datetime, date, datetime, str, 审批轮次过滤 — 驳回后重新提交时,进度接口仅展示当前轮次。 (+23 more)
### Community 51 - "Community 51"
Cohesion: 0.16
Nodes (33): Any, bool, CrmContractProductPriceTier, date, Decimal, int, Session, str (+25 more)
Nodes (31): Any, bool, CrmContractProductPriceTier, date, Decimal, int, Session, str (+23 more)
### Community 52 - "Community 52"
Cohesion: 0.09
Nodes (22): batch_assign_owner(), download_contract_pdf(), Java: batchAssignOwner, Java: downloadContractPdf — 返回合同文件 URL/路径, Java: batchAssignOwner, Java: batchAssignOwner, Java: batchAssignOwner, Java: batchAssignOwner (+14 more)
Cohesion: 0.08
Nodes (25): batch_assign_owner(), download_contract_pdf(), Java: batchAssignOwner, Java: downloadContractPdf — 返回合同文件 URL/路径, Java: batchAssignOwner, Java: batchAssignOwner, Java: batchAssignOwner, Java: batchAssignOwner (+17 more)
### Community 53 - "Community 53"
Cohesion: 0.14
Nodes (39): Any, bool, int, Session, str, SysUser, create_user(), get_user() (+31 more)
### Community 54 - "Community 54"
Cohesion: 0.17
Nodes (24): Any, bool, bytes, int, Request, str, bool, int (+16 more)
Cohesion: 0.23
Nodes (10): Any, bool, int, Session, str, camel_to_snake(), CRUDBase, get_model_attr() (+2 more)
### Community 55 - "Community 55"
Cohesion: 0.22
Nodes (29): Engine, Path, build_id_maps(), clip_text(), count_rows(), export_mapping(), fetch_rows(), legacy_id_to_str() (+21 more)
Cohesion: 0.23
Nodes (30): Engine, int, Path, build_id_maps(), clip_text(), count_rows(), export_mapping(), fetch_rows() (+22 more)
### Community 56 - "Community 56"
Cohesion: 0.09
Nodes (45): Any, CRM 报价单 CRUD — 对照 CrmQuoteServiceImpl.java(P2 全量), date, datetime, Decimal, Query, _build_quote_filters(), calculate_quote_amount() (+37 more)
Cohesion: 0.22
Nodes (12): Any, bool, str, WfProcessInstance, _LocalCallbackFn, _dispatch_local(), 工作流回调服务 —— 流程状态变更时通知业务系统, 注册本地回调函数。当 callback_url 以 path 开头时直接调用 fn。 (+4 more)
### Community 57 - "Community 57"
Cohesion: 0.05
Nodes (41): PlInventoryTransaction, PlInventoryWarning, 运费结算表 pl_freight_settlement, 库存流水表 pl_inventory_transaction, 运费结算表 pl_freight_settlement, 结算单-运输关联表 pl_freight_settlement_transport, 结算单-运输关联表 pl_freight_settlement_transport, 库存流水表 pl_inventory_transaction (+33 more)
Cohesion: 0.46
Nodes (13): bool, date, PlCarrier, str, bool, assert_carrier_eligible_for_transport(), carrier_agreement_expired(), carrier_audit_approved() (+5 more)
### Community 58 - "Community 58"
Cohesion: 0.13
Nodes (29): API 路由包 — 山峡化工生产一体化门户, create_leave(), delete_leave(), get_leave(), _leave_local_callback(), leave_workflow_callback(), list_leaves(), submit_leave() (+21 more)
Cohesion: 0.19
Nodes (20): int, str, Any, bool, int, Session, str, business_type_from_path() (+12 more)
### Community 59 - "Community 59"
Cohesion: 0.16
Nodes (32): BxUserContext, Session, str, UploadFile, BxUserContext, int, Session, str (+24 more)
Cohesion: 0.33
Nodes (10): Any, int, Session, str, list_ethnics(), 民族码表 CRUD(全局 sys_ethnic), _row_to_dict(), 民族码表 sys_ethnic(国标 GB/T 3304,全局) (+2 more)
### Community 60 - "Community 60"
Cohesion: 0.03
Nodes (72): invoice_info_list(), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll (+64 more)
Cohesion: 0.22
Nodes (9): Java: withdrawApproval, Java: withdrawApproval, Java: withdrawApproval, Java: withdrawApproval, Java: withdrawApproval, Java: withdrawApproval, Java: withdrawApproval, Java: withdrawApproval (+1 more)
### Community 61 - "Community 61"
Cohesion: 0.27
Nodes (24): DbUserContext, float, Session, str, Session, str, DashboardCargoStat, DashboardPartnerCargoStat (+16 more)
Cohesion: 0.24
Nodes (25): DbUserContext, float, Session, str, Session, str, DashboardCargoStat, DashboardPartnerCargoStat (+17 more)
### Community 62 - "Community 62"
Cohesion: 0.22
Nodes (9): export_contracts(), Java: queryList for export, Java: queryList for export, Java: queryList for export, Java: queryList for export, Java: queryList for export, Java: queryList for export, Java: queryList for export (+1 more)
Cohesion: 0.10
Nodes (21): _build_contract_filters(), export_contracts(), Java: validateContractTypeAndProductsPresence, Java: queryList for export, Java: queryList for export, Java: queryList for export, Java: queryList for export, Java: buildQueryWrapper (+13 more)
### Community 63 - "Community 63"
Cohesion: 0.08
Nodes (25): 技术栈, 1. 创建虚拟环境, 项目结构, 1. 创建虚拟环境, 2. 安装依赖, 2. 安装依赖, 3. 配置环境变量, 快速开始 (+17 more)
### Community 64 - "Community 64"
Cohesion: 0.15
Nodes (13): ORM → camelCase(单点真相), ORM → camelCase(单点真相), ORM → camelCase(单点真相), ORM → camelCase(单点真相), Java: viewByShareCode, Java: viewByShareCode, Java: viewByShareCode, Java: viewByShareCode (+5 more)
Cohesion: 0.05
Nodes (46): PlInventoryTransaction, PlInventoryWarning, 计划-运输关联表 pl_plan_transport, 运费结算表 pl_freight_settlement, 库存流水表 pl_inventory_transaction, 运费结算表 pl_freight_settlement, 结算单-运输关联表 pl_freight_settlement_transport, 结算单-运输关联表 pl_freight_settlement_transport (+38 more)
### Community 65 - "Community 65"
Cohesion: 0.22
Nodes (29): Any, int, Session, str, create_instance(), create_process_def(), create_task(), get_all_tasks_for_business() (+21 more)
Cohesion: 0.28
Nodes (8): date, datetime, str, _date_to_str(), _dt_to_str(), PagedQuery, PagedResponse, bx 原系统 API 响应包装(code=0 / msg / data.rows),与门户 success_response 分离。
### Community 66 - "Community 66"
Cohesion: 0.18
Nodes (24): bool, date, datetime, int, Session, str, ReimburseApplyOut, allocate_reimburse_title_seq() (+16 more)
Cohesion: 0.14
Nodes (28): str, T, bool, date, datetime, int, Session, str (+20 more)
### Community 67 - "Community 67"
Cohesion: 0.15
Nodes (22): logout(), 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。, 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。, 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。, bool, int, Redis, Request (+14 more)
Cohesion: 0.21
Nodes (17): bool, int, Redis, Request, str, _claims_from_auth(), _coerce_post_method_override_to_get(), _get_redis_client() (+9 more)
### Community 68 - "Community 68"
Cohesion: 0.25
@@ -464,12 +474,12 @@ Cohesion: 0.21
Nodes (16): Any, int, str, BusinessException, InvalidStatusTransitionException, log_api_request(), log_delivery_operation(), log_operation() (+8 more)
### Community 70 - "Community 70"
Cohesion: 0.14
Nodes (24): bool, BxUserContext, int, Session, str, Any, bool, int (+16 more)
Cohesion: 0.11
Nodes (32): Any, str, bool, BxUserContext, int, Session, str, Any (+24 more)
### Community 71 - "Community 71"
Cohesion: 0.19
Nodes (14): bool, date, Session, str, get_last_month_range(), month_range(), 地磅计划按月自动生成 — 迁移自 simple-dibang-xiaoshou/auto-create-plan/auto_create_monthly_pla, 按月自动生成采购计划。 - 默认仅在每月 1 日执行(force=True 时跳过该限制,供手动触发验收) - 遍历所有租户,按上月计划模板生 (+6 more)
Cohesion: 0.16
Nodes (16): bool, date, Session, str, get_last_month_range(), month_range(), 地磅计划按月自动生成 — 迁移自 simple-dibang-xiaoshou/auto-create-plan/auto_create_monthly_pla, 按月自动生成采购计划。 - 默认仅在每月 1 日执行(force=True 时跳过该限制,供手动触发验收) - 遍历所有租户,按上月计划模板生 (+8 more)
### Community 72 - "Community 72"
Cohesion: 0.26
@@ -480,52 +490,52 @@ Cohesion: 0.32
Nodes (22): Any, CrmOpportunity, Decimal, int, Session, str, batch_add_opportunity_products(), _calc_subtotal() (+14 more)
### Community 74 - "Community 74"
Cohesion: 0.18
Nodes (11): Java: statisticsTrend, Java: statisticsTrend, Java: statisticsTrend, Java: statisticsTrend, Java: statisticsTrend, Java: statisticsTrend, Java: statisticsTrend, Java: statisticsTrend (+3 more)
Cohesion: 0.06
Nodes (88): Any, Decimal, Session, str, _advance_after_approval(), _approval_to_dict(), can_user_approve_settlement(), cancel_draft_settlement() (+80 more)
### Community 75 - "Community 75"
Cohesion: 0.15
Nodes (22): clue_import_template(), customer_import_template(), product_import_template(), str, Any, bool, Session, str (+14 more)
Cohesion: 0.33
Nodes (3): 地磅 API 路由聚合 — REST 风格,协议对齐原 simple-dibang-xiaoshou。, 地磅(db)Pydantic schemas — 对齐原 simple-dibang-xiaoshou 契约。, 地磅 API 路由聚合 — REST 风格,协议对齐原 simple-dibang-xiaoshou。
### Community 76 - "Community 76"
Cohesion: 0.23
Nodes (25): approve_task(), _assert_wf_instance_in_scope(), _business_type_label(), delete_process_def(), _enrich_task_list(), get_business_all_tasks(), _get_business_form_fields(), get_designer_data() (+17 more)
Cohesion: 0.09
Nodes (49): API 路由包 — 山峡化工生产一体化门户, create_leave(), delete_leave(), get_leave(), _leave_local_callback(), leave_workflow_callback(), list_leaves(), submit_leave() (+41 more)
### Community 77 - "Community 77"
Cohesion: 0.17
Nodes (21): Any, int, str, clear_oper_log_meta(), get_oper_log_meta(), OperLogMeta, 请求级操作日志元数据(ContextVar), 供异步落库线程使用,避免 ContextVar 跨线程丢失。 (+13 more)
Cohesion: 0.33
Nodes (3): _coerce_id(), OffsetItem, ReimburseDetailCreate
### Community 78 - "Community 78"
Cohesion: 0.23
Nodes (10): Any, bool, int, Session, str, camel_to_snake(), CRUDBase, get_model_attr() (+2 more)
Cohesion: 0.13
Nodes (37): CrmContract, CrmContractProductPriceTier, date, datetime, Decimal, int, Query, contract_row_to_dict() (+29 more)
### Community 79 - "Community 79"
Cohesion: 0.19
Nodes (20): int, str, Any, bool, int, Session, str, business_type_from_path() (+12 more)
Cohesion: 0.28
Nodes (18): Any, int, Session, str, _build_tree(), _cascade_soft_delete(), _collect_with_ancestors(), create_dept() (+10 more)
### Community 80 - "Community 80"
Cohesion: 0.31
Nodes (20): Any, bool, CrmOpportunity, datetime, int, Session, str, create_opportunity_follow() (+12 more)
### Community 81 - "Community 81"
Cohesion: 0.10
Nodes (21): product_delete(), 删除产品(支持单条 body.id 和批量 body.ids), 删除产品(支持单条 body.id 和批量 body.ids), 删除产品(支持单条 body.id 和批量 body.ids), 删除产品(支持单条 body.id 和批量 body.ids), 删除产品(支持单条 body.id 和批量 body.ids), 删除产品(支持单条 body.id 和批量 body.ids), 删除产品(支持单条 body.id 和批量 body.ids) (+13 more)
Cohesion: 0.09
Nodes (23): product_delete(), 删除产品(支持单条 body.id 和批量 body.ids), 删除产品(支持单条 body.id 和批量 body.ids), 删除产品(支持单条 body.id 和批量 body.ids), 删除产品(支持单条 body.id 和批量 body.ids), 删除产品(支持单条 body.id 和批量 body.ids), 删除产品(支持单条 body.id 和批量 body.ids), 删除产品(支持单条 body.id 和批量 body.ids) (+15 more)
### Community 82 - "Community 82"
Cohesion: 0.10
Nodes (20): 1. 上传到服务器, 1. 上传到服务器, 2. 服务器首次启动, 2. 服务器首次启动, 3. 日常运维, 3. 日常运维, 4. 配置文件, 4. 配置文件 (+12 more)
### Community 83 - "Community 83"
Cohesion: 0.16
Nodes (12): float, int, str, 微信消息通知服务 支持微信小程序模板消息推送 使用方式: from app.core.wechat_message import WechatMes, 发送订单状态通知 Args: openid: 用户openid order_n, 发送支付成功通知 Args: openid: 用户openid order_n, 发送配送通知 Args: openid: 配送员openid task_no:, 发送配送时效预警 Args: openid: 配送员openid task_n (+4 more)
Cohesion: 0.18
Nodes (11): float, int, str, 发送订单状态通知 Args: openid: 用户openid order_n, 发送支付成功通知 Args: openid: 用户openid order_n, 发送配送通知 Args: openid: 配送员openid task_no:, 发送配送时效预警 Args: openid: 配送员openid task_n, 发送退款结果通知 Args: openid: 用户openid order_n (+3 more)
### Community 84 - "Community 84"
Cohesion: 0.34
Nodes (19): check_python(), create_venv(), ensure_dirs(), ensure_valid_venv(), ensure_venv_and_runtime(), install_dependencies(), is_running(), log_error() (+11 more)
### Community 85 - "Community 85"
Cohesion: 0.24
Nodes (20): Any, bool, str, _actor_ids(), _actor_names(), assert_row_deletable_by_creator(), assert_rows_deletable_by_creator(), can_delete_by_creator() (+12 more)
Cohesion: 0.09
Nodes (27): Any, datetime, Decimal, _decimal_value(), _parse_optional_datetime(), _period_key(), Java: statisticsOverview, Java: statisticsOverview (+19 more)
### Community 86 - "Community 86"
Cohesion: 0.35
@@ -545,31 +555,31 @@ Nodes (17): Any, bool, Session, str, list_share_logs(), _parse_user_agent(), CRM
### Community 90 - "Community 90"
Cohesion: 0.10
Nodes (21): Java: createNewVersion — 返回新报价单 id, Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人), Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人), Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人), Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人), Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人), Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人), Java: statisticsOverview (+13 more)
Nodes (22): date, Query, _build_quote_filters(), generate_quote_no(), _parse_optional_date(), Java: toContract — 返回 contractId, Java: toContract — 返回 contractId, Java: toContract — 返回 contractId (+14 more)
### Community 91 - "Community 91"
Cohesion: 0.29
Nodes (16): Any, int, Session, str, BizLeaveRequest, create_leave(), get_leave(), list_leaves() (+8 more)
### Community 92 - "Community 92"
Cohesion: 0.34
Nodes (16): Any, bool, int, Session, str, create_dict_type(), dict_type_code_exists(), get_dict_type() (+8 more)
Cohesion: 0.45
Nodes (20): DbUserContext, int, Session, str, PlTransportAddress, 运输通用地址库 pl_transport_address, _address_to_read(), _build_address_query() (+12 more)
### Community 93 - "Community 93"
Cohesion: 0.34
Nodes (16): Any, bool, int, Session, str, create_tenant(), get_tenant(), list_tenants() (+8 more)
### Community 94 - "Community 94"
Cohesion: 0.28
Nodes (13): bool, BxUserContext, BxWorkflowNode, int, Session, str, 按业务类型获取工作流;已知类型不存在时自动创建空定义(无节点)。, 按业务类型获取工作流;已知类型不存在时自动创建空定义(无节点)。 (+5 more)
Cohesion: 0.39
Nodes (8): bool, BxUserContext, Session, str, 按业务类型获取工作流;已知类型不存在时自动创建空定义(无节点)。, 按业务类型获取工作流;已知类型不存在时自动创建空定义(无节点)。, WorkflowService, BxWorkflowDef
### Community 95 - "Community 95"
Cohesion: 0.28
Nodes (13): int, Session, str, _allocate_inbound_outbound_id(), _ensure_sequence_row(), max_numeric_inbound_outbound_id(), next_inbound_outbound_id(), 出入库单 id 生成 — 租户内纯数字自增,并发安全。 (+5 more)
Cohesion: 0.23
Nodes (14): int, Session, str, _allocate_inbound_outbound_id(), _ensure_sequence_row(), max_numeric_inbound_outbound_id(), next_inbound_outbound_id(), 出入库单 id 生成 — 租户内纯数字自增,并发安全。 (+6 more)
### Community 96 - "Community 96"
Cohesion: 0.06
Nodes (66): Any, CertRead, date, int, Session, str, CertRead, Session (+58 more)
Cohesion: 0.17
Nodes (20): Any, int, str, get_oper_log_meta(), OperLogMeta, 请求级操作日志元数据(ContextVar), 供异步落库线程使用,避免 ContextVar 跨线程丢失。, set_oper_log_meta() (+12 more)
### Community 97 - "Community 97"
Cohesion: 0.38
@@ -588,24 +598,24 @@ Cohesion: 0.29
Nodes (14): is_valid_dict_type(), load_portal_baseline(), load_sx(), load_zkxw(), main(), map_tag_type(), normalize_row(), parse_insert() (+6 more)
### Community 101 - "Community 101"
Cohesion: 0.29
Nodes (10): datetime, int, Session, str, BizNoKind, allocate_next_biz_no(), _format_biz_no(), _prefix_for() (+2 more)
Cohesion: 0.22
Nodes (17): Session, str, str, 附件上传 bizType 与 MinIO 目录映射(对齐原 CRM oss 上传语义), 优先使用显式 folder;否则按 bizType 映射默认目录。, 优先使用显式 folder;否则按 bizType 映射默认目录。, 优先使用显式 folder;否则按 bizType 映射默认目录。, resolve_upload_folder() (+9 more)
### Community 102 - "Community 102"
Cohesion: 0.27
Nodes (11): Any, bool, str, decrypt_login_payload(), ensure_initialized(), extract_login_credentials(), get_public_pem(), 登录凭据 RSA 解密(与前端 JSEncrypt PKCS#1 v1.5 兼容)。 (+3 more)
Cohesion: 0.12
Nodes (16): int, _fill_contract_linked_count(), list_quotes(), Java: fillContractLinkedCount, Java: fillContractLinkedCount, Java: fillContractLinkedCount, Java: fillContractLinkedCount, Java: fillContractLinkedCount (+8 more)
### Community 103 - "Community 103"
Cohesion: 0.41
Nodes (11): Any, str, build_quote_html(), _default_value(), _format_date(), _format_discount(), _format_money(), generate_quote_pdf() (+3 more)
### Community 104 - "Community 104"
Cohesion: 0.22
Nodes (12): Any, bool, str, WfProcessInstance, _LocalCallbackFn, _dispatch_local(), 工作流回调服务 —— 流程状态变更时通知业务系统, 注册本地回调函数。当 callback_url 以 path 开头时直接调用 fn。 (+4 more)
Cohesion: 0.39
Nodes (8): int, LeaveCreate, LeaveUpdate, Config, LeaveCreate, LeaveResponse, LeaveUpdate, Pydantic schemas - 请假申请
### Community 105 - "Community 105"
Cohesion: 0.18
Nodes (10): Any, Request, str, str, parse_admin_request_json(), 运营 Web(/web/admin)JSON 请求体解析。 与 ApiCryptoMiddleware 配合:中间件未改写 body 时,在路由内解密 `{, 读取 JSON body;dict 时走 resolve_request_payload(明文 / 密文 / data 包装)。 Returns:, 统一解析接口请求载荷(明文 / 密文信封)。 (+2 more)
Cohesion: 0.16
Nodes (25): Any, bool, bytes, int, Request, str, bool, int (+17 more)
### Community 106 - "Community 106"
Cohesion: 0.36
@@ -616,52 +626,52 @@ Cohesion: 0.60
Nodes (10): Any, Session, str, _apply_create_range(), _base_query(), get_clue_level_statistics(), get_clue_overview_statistics(), get_clue_owner_statistics() (+2 more)
### Community 108 - "Community 108"
Cohesion: 0.45
Nodes (20): DbUserContext, int, Session, str, PlTransportAddress, 运输通用地址库 pl_transport_address, _address_to_read(), _build_address_query() (+12 more)
Cohesion: 0.35
Nodes (5): Any, str, WfProcessInstance, WorkflowEngine, WfProcessInstance
### Community 109 - "Community 109"
Cohesion: 0.32
Nodes (7): Session, str, apply_product_snapshot_to_inventory(), ProductSnapshot, 地磅模块 — CRM 产品解析与快照写入。, 校验 CRM 产品并返回名称/规格快照。新建单据 product_id 必填。, resolve_crm_product()
Cohesion: 0.14
Nodes (22): Session, str, bool, Session, str, get_user_menu_codes(), 菜单 code 权限守卫(V0.3:通过 sys_user_role → sys_role_menu → sys_menu.perms), 读取用户授权的菜单权限标识集合(与前端 meta.menuCode / perms 对齐)。 (+14 more)
### Community 110 - "Community 110"
Cohesion: 0.18
Nodes (11): generate_contract_no(), Java: generateContractNo — {供方编码}/X-JT + yyyyMMdd + 3位流水, Java: generateContractNo — {供方编码}/X-JT + yyyyMMdd + 3位流水, Java: generateContractNo — {供方编码}/X-JT{yyyyMMdd}{3位序号}, Java: generateContractNo — {供方编码}/X-JT{yyyyMMdd}{3位序号}, Java: generateContractNo — {供方编码}/X-JT{yyyyMMdd}{3位序号}, Java: generateContractNo — {供方编码}/X-JT{yyyyMMdd}{3位序号}, Java: generateContractNo — {供方编码}/X-JT{yyyyMMdd}{3位序号} (+3 more)
Cohesion: 0.05
Nodes (38): create_contract(), create_from_opportunity(), generate_contract_name(), generate_contract_no(), _normalize_framework_contract_data(), _opportunity_products_to_contract_products(), Java: normalizeFrameworkContractData, Java: createFromOpportunity — 返回合同 id (+30 more)
### Community 111 - "Community 111"
Cohesion: 0.18
Nodes (11): generate_contract_name(), Java: generateContractName, Java: generateContractName, Java: generateContractName, Java: generateContractName, Java: generateContractName, Java: generateContractName, Java: generateContractName (+3 more)
Cohesion: 0.17
Nodes (42): ProcessDefCreateBody, ProcessDefUpdateBody, PublishVersionBody, 审批意见:前端传 commentText,兼容 comment。, 审批意见:前端传 commentText,兼容 comment。, StartInstanceBody, TaskActionBody, Any (+34 more)
### Community 112 - "Community 112"
Cohesion: 0.28
Nodes (18): Any, int, Session, str, _build_tree(), _cascade_soft_delete(), _collect_with_ancestors(), create_dept() (+10 more)
Cohesion: 0.08
Nodes (103): attachment_download_url(), attachment_preview_url(), AttachmentDownloadUrlBody, create_dept(), create_dict_data(), create_dict_type(), create_menu(), create_post() (+95 more)
### Community 113 - "Community 113"
Cohesion: 0.20
Nodes (10): create_from_quote(), _quote_products_to_contract_products(), Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id (+2 more)
Cohesion: 0.10
Nodes (20): create_from_quote(), _quote_products_to_contract_products(), Java: createFromQuote — 返回合同 id, Java: renewContract — 基础续签, Java: createFromQuote — 返回合同 id, Java: renewContract — 基础续签, Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id (+12 more)
### Community 115 - "Community 115"
Cohesion: 0.29
Nodes (9): AST, expr, field_has_alias(), iter_field_assignments(), main(), bool, int, str (+1 more)
### Community 116 - "Community 116"
Cohesion: 0.33
Nodes (8): Decimal, str, derive_legacy_status_from_three(), derive_three_status_from_legacy(), 阶段A兼容:从旧 status 推导三字段。 约束:旧 status 本身混合了支付/配送/订单生命周期,因此只能尽力推断; 若业务侧已有更精确, 阶段A兼容:从三字段反推旧 status(用于旧前端/旧筛选参数兼容)。, ThreeStatus, _to_decimal()
Cohesion: 0.13
Nodes (15): calculate_quote_amount(), create_new_version(), Java: createNewVersion — 返回新报价单 id, Java: createNewVersion — 返回新报价单 id, Java: createNewVersion — 返回新报价单 id, Java: createNewVersion — 返回新报价单 id, Java: createNewVersion — 返回新报价单 id, Java: createNewVersion — 返回新报价单 id (+7 more)
### Community 117 - "Community 117"
Cohesion: 0.50
Nodes (3): Any, str, 与门户 JWT / apply_data_scope 所需的 user_info 字段对齐。
Cohesion: 0.39
Nodes (6): _inject_user_info_after_tenant(), patch_contract_routes(), patch_quote_routes(), patch_route_file(), Path, str
### Community 118 - "Community 118"
Cohesion: 0.83
Nodes (3): _add_user_info_kwarg(), patch_file(), str
Cohesion: 0.34
Nodes (16): Any, bool, int, Session, str, create_dict_type(), dict_type_code_exists(), get_dict_type() (+8 more)
### Community 119 - "Community 119"
Cohesion: 0.12
Nodes (17): create_new_version(), Java: toContract — 返回 contractId, Java: createNewVersion — 返回新报价单 id, Java: toContract — 返回 contractId, Java: createNewVersion — 返回新报价单 id, Java: toContract — 返回 contractId, Java: createNewVersion — 返回新报价单 id, Java: createNewVersion — 返回新报价单 id (+9 more)
Cohesion: 0.15
Nodes (13): ORM → camelCase(单点真相), ORM → camelCase(单点真相), ORM → camelCase(单点真相), ORM → camelCase(单点真相), Java: viewByShareCode, Java: viewByShareCode, Java: viewByShareCode, Java: viewByShareCode (+5 more)
### Community 120 - "Community 120"
Cohesion: 0.22
Nodes (9): Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签 (+1 more)
Cohesion: 0.27
Nodes (11): Any, bool, str, decrypt_login_payload(), ensure_initialized(), extract_login_credentials(), get_public_pem(), 登录凭据 RSA 解密(与前端 JSEncrypt PKCS#1 v1.5 兼容)。 (+3 more)
### Community 121 - "Community 121"
Cohesion: 0.22
@@ -671,84 +681,128 @@ Nodes (9): batch_submit_contract_approval(), Java: batchSubmitApproval, Java: ba
Cohesion: 0.32
Nodes (7): str, eq_unicode(), like_unicode(), MySQL 字符串比较/模糊匹配时统一排序规则,避免 Illegal mix of collations。, 跨表字符串等值比较(JOIN / WHERE)时统一为 utf8mb4_unicode_ci。, 将列显式设为 utf8mb4_unicode_ci。, unicode_ci()
### Community 123 - "Community 123"
Cohesion: 0.10
Nodes (20): _generate_plan_no(), 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD###### (+12 more)
### Community 124 - "Community 124"
Cohesion: 0.12
Nodes (15): 报销(bx)API 路由聚合 — REST 风格,协议对齐 bx 原系统。, date, datetime, str, 报销(bx)Pydantic 模型 — 响应协议与 bx 原系统保持一致, _date_to_str(), _dt_to_str(), bx 原系统 API 响应包装(code=0 / msg / data.rows),与门户 success_response 分离。 (+7 more)
Cohesion: 0.18
Nodes (9): 报销(bx)API 路由聚合 — REST 风格,协议对齐 bx 原系统。, 报销(bx)Pydantic 模型 — 响应协议与 bx 原系统保持一致, bx_health(), 报销(bx)API 路由聚合 — REST 风格,协议对齐 bx 原系统。, P0 健康检查:验证 bx 路由已挂载且响应协议为 code=0。, P0 健康检查:验证 bx 路由已挂载且响应协议为 code=0。, P0 健康检查:验证 bx 路由已挂载且响应协议为 code=0。, P0 健康检查:验证 bx 路由已挂载且响应协议为 code=0。 (+1 more)
### Community 125 - "Community 125"
Cohesion: 0.25
Nodes (8): generate_template_no(), Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}
Cohesion: 0.22
Nodes (9): generate_template_no(), Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号} (+1 more)
### Community 126 - "Community 126"
Cohesion: 0.22
Nodes (9): create_from_opportunity(), _opportunity_products_to_contract_products(), Java: createFromOpportunity — 返回合同 id, Java: createFromOpportunity — 返回合同 id, Java: createFromOpportunity — 返回合同 id, Java: createFromOpportunity — 返回合同 id, Java: createFromOpportunity — 返回合同 id, Java: createFromOpportunity — 返回合同 id (+1 more)
Cohesion: 0.17
Nodes (10): FreightSettlementApprovalActionBody, FreightSettlementByTransportRead, FreightSettlementCreate, FreightSettlementDisburseBody, FreightSettlementListItem, FreightSettlementSubmitApprovalBody, SettlementApprovalFlowItem, SettlementPlanSummary (+2 more)
### Community 127 - "Community 127"
Cohesion: 0.12
Nodes (67): bool, DbPurchasePlan, DbUserContext, PurchasePlanRead, PurchasePlanWithFillLink, Session, str, bool (+59 more)
Cohesion: 0.16
Nodes (37): DbPurchasePlan, DbUserContext, Session, str, InboundOutboundRead, _apply_plan_product_update(), _apply_plan_update(), _base_query() (+29 more)
### Community 128 - "Community 128"
Cohesion: 0.35
Nodes (5): Any, str, WfProcessInstance, WorkflowEngine, WfProcessInstance
Cohesion: 0.24
Nodes (8): Decimal, VehicleUreaApplyCreate, VehicleUreaApplyListResponse, VehicleUreaApplyUpdate, VehicleUreaDisburseBoardListResponse, VehicleUreaDisburseCreate, VehicleUreaDisburseListResponse, VehicleUreaDisburseUpdate
### Community 129 - "Community 129"
Cohesion: 0.09
Nodes (18): 地磅 API 路由聚合 — REST 风格,协议对齐原 simple-dibang-xiaoshou。, Session, 地磅(db)Pydantic schemas — 对齐原 simple-dibang-xiaoshou 契约。, _auto_fill_create_dept_id(), get_db(), ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID, ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID, ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID (+10 more)
Cohesion: 0.18
Nodes (11): Java: createNewVersion — 返回新报价单 id, Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人), Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人), Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人), Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人), Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人), Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人), Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人) (+3 more)
### Community 130 - "Community 130"
Cohesion: 0.07
Nodes (35): list_roles_for_select(), 返回运营端角色下拉选项(label=库中角色名,value=Web 角色码)。, 返回运营端角色下拉选项(label=库中角色名,value=Web 角色码)。, 返回运营端角色下拉选项(label=库中角色名,value=Web 角色码)。, get_current_user_profile(), get_web_auth_menu_codes(), public_quote_share(), 获取当前用户信息(与 sys_user 字段对应)。 (+27 more)
Nodes (34): _avatar_url_field(), _build_user_info_payload(), get_current_user_profile(), get_web_auth_menu_codes(), _jwt_user_payload(), logout(), public_quote_share(), 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。 (+26 more)
### Community 131 - "Community 131"
Cohesion: 0.40
Nodes (3): BaseSettings, Config, Settings
### Community 132 - "Community 132"
Cohesion: 0.40
Nodes (4): Any, str, 供 get_tenant_no 等门户工具使用。, 与门户 JWT / apply_data_scope 所需的 user_info 字段对齐。
Cohesion: 0.18
Nodes (11): Java: statisticsStatus, Java: statisticsStatus, Java: statisticsStatus, Java: statisticsStatus, Java: statisticsStatus, Java: statisticsStatus, Java: statisticsStatus, Java: statisticsStatus (+3 more)
### Community 134 - "Community 134"
Cohesion: 0.50
Nodes (4): Web 端登录用 RSA 公钥(PKCS#1 v1.5,与 JSEncrypt 一致), Web 端登录用 RSA 公钥(PKCS#1 v1.5,与 JSEncrypt 一致), Web 端登录用 RSA 公钥(PKCS#1 v1.5,与 JSEncrypt 一致), web_login_rsa_public_key()
Cohesion: 0.18
Nodes (11): 解析司机信息:优先 pl_personnel,兼容旧 pl_driver。, 解析司机信息:优先 pl_personnel,兼容旧 pl_driver。, 解析司机信息:优先 pl_personnel,兼容旧 pl_driver。, 解析司机信息:优先 pl_personnel,兼容旧 pl_driver。, 解析司机信息:优先 pl_personnel,兼容旧 pl_driver。, 解析司机信息:优先 pl_personnel,兼容旧 pl_driver。, 解析司机信息:优先 pl_personnel,兼容旧 pl_driver。, 解析司机信息:优先 pl_personnel,兼容旧 pl_driver。 (+3 more)
### Community 135 - "Community 135"
Cohesion: 0.38
Nodes (4): str, _coerce_id(), ExpenseItemCreate, ExpenseItemUpdate
Cohesion: 0.20
Nodes (10): Java: deleteWithValidByIds — 软删除, Java: deleteWithValidByIds — 软删除, Java: deleteWithValidByIds — 软删除, Java: deleteWithValidByIds — 软删除, Java: deleteWithValidByIds — 软删除, Java: deleteWithValidByIds — 软删除, Java: deleteWithValidByIds, Java: deleteWithValidByIds — 软删除 (+2 more)
### Community 136 - "Community 136"
Cohesion: 0.32
Nodes (12): Any, bool, bytes, int, str, _b64url_decode(), _b64url_encode(), decrypt_json() (+4 more)
Cohesion: 0.18
Nodes (10): Any, Request, str, str, parse_admin_request_json(), 运营 Web(/web/admin)JSON 请求体解析。 与 ApiCryptoMiddleware 配合:中间件未改写 body 时,在路由内解密 `{, 读取 JSON body;dict 时走 resolve_request_payload(明文 / 密文 / data 包装)。 Returns:, 统一解析接口请求载荷(明文 / 密文信封)。 (+2 more)
### Community 137 - "Community 137"
Cohesion: 0.33
Nodes (6): _fill_contract_linked_count(), Java: fillContractLinkedCount, Java: fillContractLinkedCount, Java: fillContractLinkedCount, Java: fillContractLinkedCount, Java: fillContractLinkedCount
Nodes (8): Decimal, str, derive_legacy_status_from_three(), derive_three_status_from_legacy(), 阶段A兼容:从旧 status 推导三字段。 约束:旧 status 本身混合了支付/配送/订单生命周期,因此只能尽力推断; 若业务侧已有更精确, 阶段A兼容:从三字段反推旧 status(用于旧前端/旧筛选参数兼容)。, ThreeStatus, _to_decimal()
### Community 148 - "Community 148"
Cohesion: 0.39
Nodes (6): _inject_user_info_after_tenant(), patch_contract_routes(), patch_quote_routes(), patch_route_file(), Path, str
Cohesion: 0.38
Nodes (4): str, _coerce_id(), ExpenseItemCreate, ExpenseItemUpdate
### Community 149 - "Community 149"
Cohesion: 0.33
Nodes (5): _coerce_optional_id(), WorkflowDefCreate, WorkflowDefUpdate, WorkflowNodeCreate, WorkflowNodeOut
### Community 151 - "Community 151"
Cohesion: 0.03
Nodes (60): 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll (+52 more)
### Community 152 - "Community 152"
Cohesion: 0.38
Nodes (5): TransportAddressBase, TransportAddressCreate, TransportAddressListResponse, TransportAddressRead, TransportAddressUpdate
### Community 153 - "Community 153"
Cohesion: 0.40
Nodes (5): Web 端登录用 RSA 公钥(PKCS#1 v1.5,与 JSEncrypt 一致), Web 端登录用 RSA 公钥(PKCS#1 v1.5,与 JSEncrypt 一致), Web 端登录用 RSA 公钥(PKCS#1 v1.5,与 JSEncrypt 一致), Web 端登录用 RSA 公钥(PKCS#1 v1.5,与 JSEncrypt 一致), web_login_rsa_public_key()
### Community 154 - "Community 154"
Cohesion: 0.29
Nodes (10): datetime, int, Session, str, BizNoKind, allocate_next_biz_no(), _format_biz_no(), _prefix_for() (+2 more)
### Community 155 - "Community 155"
Cohesion: 0.40
Nodes (4): _auto_fill_create_dept_id(), ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID, ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID, ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID
### Community 156 - "Community 156"
Cohesion: 0.40
Nodes (4): ContactByCustomerBody, ContactDetailBody, ContactSetPrimaryBody, CRM 联系人 Pydantic Schemas
### Community 157 - "Community 157"
Cohesion: 0.40
Nodes (4): CreateLinkForPlanPayload, InboundOutboundBase, InboundOutboundCreate, InboundOutboundUpdate
### Community 158 - "Community 158"
Cohesion: 0.50
Nodes (3): Config, PageResult, ResponseModel
### Community 159 - "Community 159"
Cohesion: 0.50
Nodes (3): PurchasePlanBase, PurchasePlanCreate, PurchasePlanUpdate
### Community 160 - "Community 160"
Cohesion: 0.50
Nodes (3): WeighingSlipBase, WeighingSlipCreate, WeighingSlipUpdate
### Community 162 - "Community 162"
Cohesion: 0.50
Nodes (3): Any, str, 与门户 JWT / apply_data_scope 所需的 user_info 字段对齐。
### Community 169 - "Community 169"
Cohesion: 0.18
Nodes (11): Java: statisticsStatus, Java: statisticsStatus, Java: statisticsStatus, Java: statisticsStatus, Java: statisticsStatus, Java: statisticsStatus, Java: statisticsStatus, Java: statisticsStatus (+3 more)
Nodes (12): Session, Session, get_current_user(), get_current_user_by_token(), 根据 token 获取当前用户(包含 Redis 会话校验;会话滑动续期), 根据 token 获取当前用户(包含 Redis 会话校验;会话滑动续期), 根据 token 获取当前用户(包含 Redis 会话校验;会话滑动续期), 获取当前用户(sync 依赖,配合 sync 路由在线程池执行,避免阻塞事件循环)。 (+4 more)
## Knowledge Gaps
- **77 isolated node(s):** `Session`, `Request`, `bool`, `bool`, `Config` (+72 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **3 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
- **2 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **Why does `Session` connect `Community 3` to `Community 0`, `Community 1`, `Community 2`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 21`, `Community 22`, `Community 23`, `Community 24`, `Community 25`, `Community 26`, `Community 27`, `Community 28`, `Community 29`, `Community 30`, `Community 31`, `Community 32`, `Community 33`, `Community 34`, `Community 35`, `Community 36`, `Community 37`, `Community 38`, `Community 39`, `Community 40`, `Community 42`, `Community 43`, `Community 44`, `Community 46`, `Community 47`, `Community 51`, `Community 52`, `Community 53`, `Community 54`, `Community 56`, `Community 58`, `Community 59`, `Community 60`, `Community 61`, `Community 62`, `Community 64`, `Community 65`, `Community 66`, `Community 68`, `Community 70`, `Community 71`, `Community 72`, `Community 73`, `Community 74`, `Community 75`, `Community 76`, `Community 78`, `Community 79`, `Community 80`, `Community 81`, `Community 86`, `Community 87`, `Community 89`, `Community 90`, `Community 91`, `Community 92`, `Community 93`, `Community 94`, `Community 95`, `Community 96`, `Community 97`, `Community 98`, `Community 99`, `Community 101`, `Community 107`, `Community 108`, `Community 109`, `Community 110`, `Community 112`, `Community 113`, `Community 119`, `Community 120`, `Community 121`, `Community 123`, `Community 125`, `Community 126`, `Community 127`, `Community 128`, `Community 129`, `Community 130`, `Community 137`, `Community 149`?**
_High betweenness centrality (0.253) - this node is a cross-community bridge._
- **Why does `BaseModel` connect `Community 40` to `Community 0`, `Community 1`, `Community 2`, `Community 4`, `Community 135`, `Community 7`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 14`, `Community 15`, `Community 18`, `Community 22`, `Community 23`, `Community 24`, `Community 30`, `Community 35`, `Community 38`, `Community 39`, `Community 42`, `Community 44`, `Community 46`, `Community 48`, `Community 49`, `Community 58`, `Community 59`, `Community 61`, `Community 66`, `Community 76`, `Community 96`, `Community 114`, `Community 127`?**
_High betweenness centrality (0.050) - this node is a cross-community bridge._
- **Why does `int` connect `Community 30` to `Community 0`, `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 10`, `Community 11`, `Community 12`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 20`, `Community 21`, `Community 22`, `Community 24`, `Community 25`, `Community 26`, `Community 27`, `Community 28`, `Community 29`, `Community 32`, `Community 33`, `Community 35`, `Community 37`, `Community 38`, `Community 41`, `Community 42`, `Community 43`, `Community 44`, `Community 45`, `Community 46`, `Community 47`, `Community 50`, `Community 51`, `Community 52`, `Community 53`, `Community 54`, `Community 55`, `Community 58`, `Community 59`, `Community 65`, `Community 66`, `Community 67`, `Community 68`, `Community 69`, `Community 70`, `Community 71`, `Community 72`, `Community 73`, `Community 76`, `Community 77`, `Community 78`, `Community 79`, `Community 80`, `Community 83`, `Community 86`, `Community 91`, `Community 92`, `Community 93`, `Community 94`, `Community 95`, `Community 96`, `Community 97`, `Community 98`, `Community 99`, `Community 101`, `Community 108`, `Community 112`, `Community 121`, `Community 128`, `Community 136`?**
_High betweenness centrality (0.046) - this node is a cross-community bridge._
- **Why does `Session` connect `Community 112` to `Community 0`, `Community 1`, `Community 2`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 21`, `Community 22`, `Community 23`, `Community 24`, `Community 25`, `Community 26`, `Community 27`, `Community 28`, `Community 29`, `Community 30`, `Community 31`, `Community 32`, `Community 33`, `Community 34`, `Community 35`, `Community 37`, `Community 38`, `Community 39`, `Community 42`, `Community 43`, `Community 44`, `Community 46`, `Community 47`, `Community 48`, `Community 51`, `Community 52`, `Community 53`, `Community 54`, `Community 58`, `Community 59`, `Community 60`, `Community 61`, `Community 62`, `Community 66`, `Community 68`, `Community 70`, `Community 71`, `Community 72`, `Community 73`, `Community 74`, `Community 76`, `Community 78`, `Community 79`, `Community 80`, `Community 81`, `Community 85`, `Community 86`, `Community 87`, `Community 89`, `Community 90`, `Community 91`, `Community 92`, `Community 93`, `Community 94`, `Community 95`, `Community 97`, `Community 98`, `Community 99`, `Community 101`, `Community 102`, `Community 104`, `Community 105`, `Community 107`, `Community 108`, `Community 109`, `Community 110`, `Community 111`, `Community 113`, `Community 116`, `Community 118`, `Community 119`, `Community 121`, `Community 125`, `Community 127`, `Community 129`, `Community 130`, `Community 132`, `Community 134`, `Community 135`, `Community 151`, `Community 154`, `Community 169`?**
_High betweenness centrality (0.257) - this node is a cross-community bridge._
- **Why does `BaseModel` connect `Community 3` to `Community 0`, `Community 1`, `Community 128`, `Community 4`, `Community 6`, `Community 7`, `Community 9`, `Community 10`, `Community 11`, `Community 14`, `Community 15`, `Community 18`, `Community 148`, `Community 149`, `Community 22`, `Community 23`, `Community 24`, `Community 152`, `Community 20`, `Community 156`, `Community 29`, `Community 158`, `Community 30`, `Community 157`, `Community 159`, `Community 160`, `Community 35`, `Community 32`, `Community 38`, `Community 39`, `Community 40`, `Community 42`, `Community 48`, `Community 61`, `Community 65`, `Community 76`, `Community 77`, `Community 104`, `Community 111`, `Community 112`, `Community 114`, `Community 126`, `Community 127`?**
_High betweenness centrality (0.058) - this node is a cross-community bridge._
- **Why does `int` connect `Community 55` to `Community 0`, `Community 1`, `Community 2`, `Community 4`, `Community 5`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 14`, `Community 15`, `Community 17`, `Community 18`, `Community 21`, `Community 22`, `Community 23`, `Community 24`, `Community 25`, `Community 26`, `Community 29`, `Community 30`, `Community 32`, `Community 33`, `Community 35`, `Community 37`, `Community 38`, `Community 41`, `Community 42`, `Community 43`, `Community 44`, `Community 45`, `Community 46`, `Community 47`, `Community 48`, `Community 50`, `Community 51`, `Community 52`, `Community 53`, `Community 54`, `Community 58`, `Community 59`, `Community 66`, `Community 67`, `Community 68`, `Community 69`, `Community 70`, `Community 71`, `Community 72`, `Community 73`, `Community 74`, `Community 76`, `Community 78`, `Community 79`, `Community 80`, `Community 83`, `Community 86`, `Community 91`, `Community 92`, `Community 93`, `Community 94`, `Community 95`, `Community 96`, `Community 97`, `Community 98`, `Community 99`, `Community 101`, `Community 102`, `Community 104`, `Community 105`, `Community 108`, `Community 111`, `Community 112`, `Community 118`, `Community 121`, `Community 135`, `Community 154`?**
_High betweenness centrality (0.040) - this node is a cross-community bridge._
- **Are the 337 inferred relationships involving `str` (e.g. with `AdvanceApplyCreate` and `AdvanceApplyOut`) actually correct?**
_`str` has 337 INFERRED edges - model-reasoned connections that need verification._
- **Are the 333 inferred relationships involving `Session` (e.g. with `AdvanceApplyCreate` and `AdvanceApplyOut`) actually correct?**
@@ -756,4 +810,4 @@ _Questions this graph is uniquely positioned to answer:_
- **Are the 222 inferred relationships involving `int` (e.g. with `AdvanceApplyCreate` and `AdvanceApplyOut`) actually correct?**
_`int` has 222 INFERRED edges - model-reasoned connections that need verification._
- **What connects `定期 ping MySQL,避免远程库/NAT 回收空闲 TCP 后首请求长时间卡住。`, `数据库异常:返回 JSON 并保留 CORS 头,避免浏览器只报跨域错误`, `未捕获异常:统一 JSON 500,确保跨域场景下仍有 Access-Control-Allow-Origin` to the rest of the system?**
_1446 weakly-connected nodes found - possible documentation gaps or missing edges._
_1493 weakly-connected nodes found - possible documentation gaps or missing edges._
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_crm_opportunity_follow_py", "label": "opportunity_follow.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_follow.py", "source_location": "L1"}, {"id": "crm_opportunity_follow_total_pages", "label": "total_pages()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_follow.py", "source_location": "L17"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_follow.py", "source_location": "L17"}, {"id": "crm_opportunity_follow_parse_optional_datetime", "label": "_parse_optional_datetime()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_follow.py", "source_location": "L26"}, {"id": "any", "label": "Any", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_follow.py", "source_location": "L26"}, {"id": "datetime", "label": "datetime", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_follow.py", "source_location": "L26"}, {"id": "crm_opportunity_follow_row_to_dict", "label": "_row_to_dict()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_follow.py", "source_location": "L42"}, {"id": "crmopportunityfollow", "label": "CrmOpportunityFollow", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_follow.py", "source_location": "L42"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_follow.py", "source_location": "L42"}, {"id": "crm_opportunity_follow_recalc_opportunity_follow_stats", "label": "_recalc_opportunity_follow_stats()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_follow.py", "source_location": "L71"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_follow.py", "source_location": "L71"}, {"id": "crm_opportunity_follow_ensure_parent_opportunity", "label": "_ensure_parent_opportunity()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_follow.py", "source_location": "L111"}, {"id": "crmopportunity", "label": "CrmOpportunity", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_follow.py", "source_location": "L111"}, {"id": "crm_opportunity_follow_list_opportunity_follows", "label": "list_opportunity_follows()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_follow.py", "source_location": "L129"}, {"id": "crm_opportunity_follow_list_follows_by_opportunity", "label": "list_follows_by_opportunity()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_follow.py", "source_location": "L159"}, {"id": "crm_opportunity_follow_get_opportunity_follow", "label": "get_opportunity_follow()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_follow.py", "source_location": "L176"}, {"id": "crm_opportunity_follow_create_opportunity_follow", "label": "create_opportunity_follow()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_follow.py", "source_location": "L197"}, {"id": "crm_opportunity_follow_update_opportunity_follow", "label": "update_opportunity_follow()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_follow.py", "source_location": "L243"}, {"id": "crm_opportunity_follow_soft_delete_opportunity_follow", "label": "soft_delete_opportunity_follow()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_follow.py", "source_location": "L287"}, {"id": "bool", "label": "bool", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_follow.py", "source_location": "L287"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_crm_opportunity_follow_py", "target": "math", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_follow.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_crm_opportunity_follow_py", "target": "datetime", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_follow.py",Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_plan_service_py", "label": "pl_plan_service.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L1"}, {"id": "services_pl_plan_service_determine_plan_type", "label": "determine_plan_type()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L42"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L42"}, {"id": "services_pl_plan_service_is_ton_product_unit", "label": "is_ton_product_unit()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L50"}, {"id": "bool", "label": "bool", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L50"}, {"id": "services_pl_plan_service_product_quantity", "label": "_product_quantity()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L57"}, {"id": "planproductcreate", "label": "PlanProductCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L57"}, {"id": "decimal", "label": "Decimal", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L57"}, {"id": "services_pl_plan_service_product_create_actual_qty", "label": "_product_create_actual_qty()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L67"}, {"id": "services_pl_plan_service_plan_product_actual_qty", "label": "_plan_product_actual_qty()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L73"}, {"id": "plplanproduct", "label": "PlPlanProduct", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L73"}, {"id": "services_pl_plan_service_sum_product_actual_quantities", "label": "sum_product_actual_quantities()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L79"}, {"id": "services_pl_plan_service_sum_plan_product_actual_quantities", "label": "sum_plan_product_actual_quantities()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L83"}, {"id": "services_pl_plan_service_estimate_qty_fields", "label": "_estimate_qty_fields()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L87"}, {"id": "services_pl_plan_service_build_plan_product", "label": "build_plan_product()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L98"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L98"}, {"id": "services_pl_plan_service_apply_vehicle_snapshot_to_plan_transport", "label": "_apply_vehicle_snapshot_to_plan_transport()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L145"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L145"}, {"id": "plplan", "label": "PlPlan", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L145"}, {"id": "pltransport", "label": "PlTransport", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L145"}, {"id": "plvehicle", "label": "PlVehicle", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L145"}, {"id": "services_pl_plan_service_resolve_driver", "label": "_resolve_driver()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L163"}, {"id": "services_pl_plan_service_calc_freight_amount", "label": "_calc_freight_amount()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L208"}, {"id": "services_pl_plan_service_fetch_hazard_flags_mapLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_services_web_auth_roles_py", "label": "web_auth_roles.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/web_auth_roles.py", "source_location": "L1"}, {"id": "services_web_auth_roles_map_role_name_to_web", "label": "_map_role_name_to_web()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/web_auth_roles.py", "source_location": "L13"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/web_auth_roles.py", "source_location": "L13"}, {"id": "services_web_auth_roles_role_rows_for_user", "label": "_role_rows_for_user()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/web_auth_roles.py", "source_location": "L25"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/web_auth_roles.py", "source_location": "L25"}, {"id": "services_web_auth_roles_user_has_bound_roles", "label": "user_has_bound_roles()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/web_auth_roles.py", "source_location": "L42"}, {"id": "bool", "label": "bool", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/web_auth_roles.py", "source_location": "L42"}, {"id": "services_web_auth_roles_resolve_web_role_for_user", "label": "resolve_web_role_for_user()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/web_auth_roles.py", "source_location": "L53"}, {"id": "services_web_auth_roles_iter_user_menu_perm_codes", "label": "iter_user_menu_perm_codes()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/web_auth_roles.py", "source_location": "L71"}, {"id": "services_web_auth_roles_rationale_1", "label": "Web \u7ba1\u7406\u7aef\u767b\u5f55\uff1a\u57fa\u4e8e V0.3 sys_user / sys_user_role / sys_role \u7684\u89d2\u8272\u4e0e\u83dc\u5355\u6743\u9650\u89e3\u6790", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/web_auth_roles.py", "source_location": "L1"}, {"id": "services_web_auth_roles_rationale_14", "label": "\u5c06 sys_role.role_name \u6620\u5c04\u4e3a web-admin \u4f7f\u7528\u7684 role \u5b57\u7b26\u4e32\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/web_auth_roles.py", "source_location": "L14"}, {"id": "services_web_auth_roles_rationale_43", "label": "\u7528\u6237\u662f\u5426\u5728 sys_user_role \u4e2d\u7ed1\u5b9a\u4e86\u81f3\u5c11\u4e00\u4e2a\u6709\u6548\u89d2\u8272\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/web_auth_roles.py", "source_location": "L43"}, {"id": "services_web_auth_roles_rationale_54", "label": "\u6839\u636e\u7528\u6237\u5173\u8054\u89d2\u8272\u89e3\u6790 Web \u7aef role\uff08admin / super_admin\uff09\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/web_auth_roles.py", "source_location": "L54"}, {"id": "services_web_auth_roles_rationale_74", "label": "\u7528\u6237\u901a\u8fc7\u89d2\u8272\u5173\u8054\u5f97\u5230\u7684\u83dc\u5355\u6743\u9650\u6807\u8bc6\uff08sys_menu.perms\uff0c\u7a7a\u5219\u9000\u56de\u83dc\u5355 id\uff09\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/web_auth_roles.py", "source_location": "L74"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_web_auth_roles_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/web_auth_roles.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_web_auth_roles_py", "target": "sqlalchemy", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/web_auth_roles.py", "source_location": "L6", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_web_auth_roles_py", "target": "sqlalchemy_orm", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/web_auth_roles.py", "source_location": "L7", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_web_auth_roles_py", "target": "app_core_config", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/web_auth_roles.py", "source_location": "L9", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_appLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "label": "crm_contract_routes.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L1"}, {"id": "api_crm_contract_routes_register_contract_routes", "label": "register_contract_routes()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L75"}, {"id": "api_crm_contract_routes_rationale_1", "label": "CRM \u5408\u540c API \u8def\u7531 \u2014 GET/PUT/DELETE/POST \u5bf9\u9f50\u539f Java CRM", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L1"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "datetime", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L5", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "fastapi", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L7", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "sqlalchemy_orm", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L8", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_core_database", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L10", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_core_menu_guard", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L11", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_core_oper_log_decorator", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L12", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_core_response", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L13", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_core_tenant_context", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L14", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_crud_crm_data_scope_helpers", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L15", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_crud_crm_contract", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L16", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_crud_crm_contract_approval", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L47", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_schemas_crm_contract", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_tests_test_pl_dibang_plan_quantity_py", "label": "test_pl_dibang_plan_quantity.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_dibang_plan_quantity.py", "source_location": "L1"}, {"id": "tests_test_pl_dibang_plan_quantity_test_apply_plan_quantity_delta_accumulates", "label": "test_apply_plan_quantity_delta_accumulates()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_dibang_plan_quantity.py", "source_location": "L16"}, {"id": "tests_test_pl_dibang_plan_quantity_test_apply_plan_quantity_delta_never_below_zero", "label": "test_apply_plan_quantity_delta_never_below_zero()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_dibang_plan_quantity.py", "source_location": "L23"}, {"id": "tests_test_pl_dibang_plan_quantity_test_resolve_link_contributed_qty_prefers_link_field", "label": "test_resolve_link_contributed_qty_prefers_link_field()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_dibang_plan_quantity.py", "source_location": "L29"}, {"id": "tests_test_pl_dibang_plan_quantity_test_resolve_link_contributed_qty_falls_back_to_inventory", "label": "test_resolve_link_contributed_qty_falls_back_to_inventory()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_dibang_plan_quantity.py", "source_location": "L35"}, {"id": "tests_test_pl_dibang_plan_quantity_test_resolve_link_contributed_qty_zero_when_missing", "label": "test_resolve_link_contributed_qty_zero_when_missing()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_dibang_plan_quantity.py", "source_location": "L41"}, {"id": "tests_test_pl_dibang_plan_quantity_test_revoke_subtracts_plan_quantity_on_cancel", "label": "test_revoke_subtracts_plan_quantity_on_cancel()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_dibang_plan_quantity.py", "source_location": "L48"}, {"id": "tests_test_pl_dibang_plan_quantity_test_revoke_blocks_when_weighing_exists", "label": "test_revoke_blocks_when_weighing_exists()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_dibang_plan_quantity.py", "source_location": "L102"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_tests_test_pl_dibang_plan_quantity_py", "target": "decimal", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_dibang_plan_quantity.py", "source_location": "L2", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_tests_test_pl_dibang_plan_quantity_py", "target": "unittest_mock", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_dibang_plan_quantity.py", "source_location": "L3", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_tests_test_pl_dibang_plan_quantity_py", "target": "pytest", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_dibang_plan_quantity.py", "source_location": "L5", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_tests_test_pl_dibang_plan_quantity_py", "target": "app_models_db_models", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_dibang_plan_quantity.py", "source_location": "L7", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_tests_test_pl_dibang_plan_quantity_py", "target": "app_models_pl_models", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_dibang_plan_quantity.py", "source_location": "L8", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_tests_test_pl_dibang_plan_quantity_py", "target": "app_services_pl_dibang_bridge_service", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_dibang_plan_quantity.py", "source_location": "L9", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_tests_test_pl_dibang_plan_quantity_py", "target": "tests_test_pl_dibang_plan_quantity_test_apply_plan_quantity_delta_accumulates", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_dibang_plan_quantity.py", "source_location": "L16", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_tests_test_pl_dibang_plan_quantity_py", "target": "tests_test_pl_dibang_plan_quantity_test_apply_planLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_crm_product_py", "label": "product.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py", "source_location": "L1"}, {"id": "crm_product_total_pages", "label": "total_pages()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py", "source_location": "L24"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py", "source_location": "L24"}, {"id": "crm_product_category_row_to_dict", "label": "_category_row_to_dict()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py", "source_location": "L34"}, {"id": "crmproductcategory", "label": "CrmProductCategory", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py", "source_location": "L34"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py", "source_location": "L34"}, {"id": "any", "label": "Any", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py", "source_location": "L34"}, {"id": "crm_product_list_categories", "label": "list_categories()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py", "source_location": "L50"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py", "source_location": "L50"}, {"id": "crm_product_build_category_tree", "label": "_build_category_tree()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py", "source_location": "L77"}, {"id": "crm_product_collect_category_with_ancestors", "label": "_collect_category_with_ancestors()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py", "source_location": "L88"}, {"id": "crm_product_list_category_tree", "label": "list_category_tree()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py", "source_location": "L104"}, {"id": "crm_product_get_category", "label": "get_category()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py", "source_location": "L137"}, {"id": "crm_product_create_category", "label": "create_category()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py", "source_location": "L149"}, {"id": "crm_product_update_category", "label": "update_category()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py", "source_location": "L186"}, {"id": "crm_product_soft_delete_category", "label": "soft_delete_category()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py", "source_location": "L225"}, {"id": "crm_product_category_code_exists", "label": "category_code_exists()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py", "source_location": "L265"}, {"id": "bool", "label": "bool", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py", "source_location": "L265"}, {"id": "crm_product_product_row_to_dict", "label": "_product_row_to_dict()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py", "source_location": "L285"}, {"id": "crmproduct", "label": "CrmProduct", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py", "source_location": "L285"}, {"id": "crm_product_list_products", "label": "list_products()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py", "source_location": "L316"}, {"id": "crm_product_get_product", "label": "get_product()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py", "source_location": "L362"}, {"id": "crm_product_get_product_by_no", "label": "get_product_by_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py", "source_location": "L374"}, {"id": "crm_product_max_sxwl_product_seq", "label": "_max_sxwl_product_seq()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py", "source_location": "L393"}, {"id": "crm_product_generate_product_no", "label": "generate_product_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py", "source_location": "L414"}, {"id": "crm_product_generate_barcode", "label"Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "label": "crm_quote_routes.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L1"}, {"id": "api_crm_quote_routes_register_quote_routes", "label": "register_quote_routes()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L83"}, {"id": "api_crm_quote_routes_rationale_1", "label": "CRM \u62a5\u4ef7\u5355 API \u8def\u7531 \u2014 \u6302\u63a5\u5230 crm.router", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L1"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "datetime", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "fastapi", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L6", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "sqlalchemy_orm", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L7", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_core_database", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L9", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_core_menu_guard", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L10", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_core_response", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L11", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_core_tenant_context", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L12", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_crud_crm_data_scope_helpers", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L13", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_crud_crm_quote", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L14", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_crud_crm_quote_approval", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L36", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_crud_crm_quote_product", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L42", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_crud_crm_quote_share_log", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L48", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_schemas_crm_quote", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L49", "weight": 1.0}, {"source": "uLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_crm_contract_py", "label": "contract.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py", "source_location": "L1"}, {"id": "crm_contract_ensure_contract_in_scope", "label": "_ensure_contract_in_scope()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py", "source_location": "L48"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py", "source_location": "L48"}, {"id": "crmcontract", "label": "CrmContract", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py", "source_location": "L48"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py", "source_location": "L48"}, {"id": "crm_contract_get_contract_row_in_scope", "label": "_get_contract_row_in_scope()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py", "source_location": "L52"}, {"id": "crm_contract_total_pages", "label": "total_pages()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py", "source_location": "L69"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py", "source_location": "L69"}, {"id": "crm_contract_decimal_value", "label": "_decimal_value()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py", "source_location": "L75"}, {"id": "any", "label": "Any", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py", "source_location": "L75"}, {"id": "decimal", "label": "Decimal", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py", "source_location": "L75"}, {"id": "crm_contract_parse_optional_date", "label": "_parse_optional_date()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py", "source_location": "L83"}, {"id": "date", "label": "date", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py", "source_location": "L83"}, {"id": "crm_contract_parse_optional_datetime", "label": "_parse_optional_datetime()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py", "source_location": "L101"}, {"id": "datetime", "label": "datetime", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py", "source_location": "L101"}, {"id": "crm_contract_get_field", "label": "_get_field()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py", "source_location": "L117"}, {"id": "crm_contract_resolve_user_display_name", "label": "_resolve_user_display_name()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py", "source_location": "L124"}, {"id": "crm_contract_is_blank", "label": "_is_blank()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py", "source_location": "L134"}, {"id": "bool", "label": "bool", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py", "source_location": "L134"}, {"id": "crm_contract_first_non_blank", "label": "_first_non_blank()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py", "source_location": "L138"}, {"id": "crm_contract_get_default_invoice_info", "label": "_get_default_invoice_info()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py", "source_location": "L146"}, {"id": "crm_contract_build_customer_address", "label": "_build_customer_address()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py", "source_location": "L156"}, {"id": "crm_contract_row_to_dict", "label": "_row_to_dict()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py", "source_location": "L168"}, {"id": "crm_contract_contract_row_to_dict", "label": "contract_row_to_dict()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py", "source_location": "L245"}, {"id": "crm_contract_build_contract_filters", "label": "_build_contract_filters()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py", "source_location": "L249"}, {"id": "query", "label": "Query", "file_type": "code", "source_file": Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_crm_product_py", "label": "product.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/product.py", "source_location": "L1"}, {"id": "crm_product_productcategorycreatebody", "label": "ProductCategoryCreateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/product.py", "source_location": "L12"}, {"id": "basemodel", "label": "BaseModel", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "crm_product_productcategoryupdatebody", "label": "ProductCategoryUpdateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/product.py", "source_location": "L26"}, {"id": "crm_product_categorydeletebody", "label": "CategoryDeleteBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/product.py", "source_location": "L41"}, {"id": "crm_product_productupdatestatusbody", "label": "ProductUpdateStatusBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/product.py", "source_location": "L47"}, {"id": "crm_product_productlistbody", "label": "ProductListBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/product.py", "source_location": "L57"}, {"id": "crm_product_productcreatebody", "label": "ProductCreateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/product.py", "source_location": "L69"}, {"id": "crm_product_productupdatebody", "label": "ProductUpdateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/product.py", "source_location": "L101"}, {"id": "crm_product_productdetailbody", "label": "ProductDetailBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/product.py", "source_location": "L134"}, {"id": "crm_product_productdeletebody", "label": "ProductDeleteBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/product.py", "source_location": "L140"}, {"id": "crm_product_productoptionsbody", "label": "ProductOptionsBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/product.py", "source_location": "L147"}, {"id": "crm_product_rationale_1", "label": "CRM \u4ea7\u54c1\u6a21\u5757 Pydantic Schemas", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/product.py", "source_location": "L1"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_crm_product_py", "target": "decimal", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/product.py", "source_location": "L3", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_crm_product_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/product.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_crm_product_py", "target": "pydantic", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/product.py", "source_location": "L6", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_crm_product_py", "target": "crm_product_productcategorycreatebody", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/product.py", "source_location": "L12", "weight": 1.0}, {"source": "crm_product_productcategorycreatebody", "target": "basemodel", "relation": "inherits", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/product.py", "source_location": "L12", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_crm_product_py", "target": "crm_product_productcategoryupdatebody", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/product.py", "source_location": "L26", "weight": 1.0}, {"source": "crm_product_productcategoryupdatebody", "target": "basemodel", "relation": "inherits", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/product.py", "source_location": "L26", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_crm_product_py", "target": "crm_product_categorydeletebody", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxiaLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_crm_customer_py", "label": "customer.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer.py", "source_location": "L1"}, {"id": "crm_customer_customerlistbody", "label": "CustomerListBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer.py", "source_location": "L12"}, {"id": "basemodel", "label": "BaseModel", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "crm_customer_customerlistbody_to_list_kwargs", "label": ".to_list_kwargs()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer.py", "source_location": "L39"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer.py", "source_location": "L39"}, {"id": "crm_customer_customercreatebody", "label": "CustomerCreateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer.py", "source_location": "L72"}, {"id": "crm_customer_customercreatebody_forbid_legacy_invoice_fields", "label": "._forbid_legacy_invoice_fields()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer.py", "source_location": "L110"}, {"id": "crm_customer_customerupdatebody", "label": "CustomerUpdateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer.py", "source_location": "L135"}, {"id": "crm_customer_customerupdatebody_forbid_legacy_invoice_fields", "label": "._forbid_legacy_invoice_fields()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer.py", "source_location": "L174"}, {"id": "crm_customer_customerdeletebody", "label": "CustomerDeleteBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer.py", "source_location": "L199"}, {"id": "crm_customer_customerdetailbody", "label": "CustomerDetailBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer.py", "source_location": "L207"}, {"id": "crm_customer_customertransferbody", "label": "CustomerTransferBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer.py", "source_location": "L214"}, {"id": "crm_customer_customerintopoolbody", "label": "CustomerIntoPoolBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer.py", "source_location": "L226"}, {"id": "crm_customer_customerdealbody", "label": "CustomerDealBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer.py", "source_location": "L236"}, {"id": "crm_customer_customerclaimbody", "label": "CustomerClaimBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer.py", "source_location": "L248"}, {"id": "crm_customer_customerassignpoolbody", "label": "CustomerAssignPoolBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer.py", "source_location": "L257"}, {"id": "crm_customer_customercheckduplicatebody", "label": "CustomerCheckDuplicateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer.py", "source_location": "L268"}, {"id": "crm_customer_rationale_1", "label": "CRM \u5ba2\u6237\u7ba1\u7406 Pydantic Schemas", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer.py", "source_location": "L1"}, {"id": "crm_customer_rationale_40", "label": "\u89e3\u6790\u5217\u8868\u67e5\u8be2\u53c2\u6570\uff0c\u4f9b API \u4e0e CRUD \u5171\u7528\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer.py", "source_location": "L40"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_crm_customer_py", "target": "decimal", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer.py", "source_location": "L3", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_crm_customer_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_crm_customer_py", "target": "pydantic", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemasLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_core_config_py", "label": "config.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/config.py", "source_location": "L1"}, {"id": "core_config_settings", "label": "Settings", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/config.py", "source_location": "L7"}, {"id": "basesettings", "label": "BaseSettings", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "core_config_config", "label": "Config", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/config.py", "source_location": "L173"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_core_config_py", "target": "pydantic_settings", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/config.py", "source_location": "L1", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_core_config_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/config.py", "source_location": "L2", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_core_config_py", "target": "pathlib", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/config.py", "source_location": "L3", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_core_config_py", "target": "os", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/config.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_core_config_py", "target": "core_config_settings", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/config.py", "source_location": "L7", "weight": 1.0}, {"source": "core_config_settings", "target": "basesettings", "relation": "inherits", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/config.py", "source_location": "L7", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_core_config_py", "target": "core_config_config", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/config.py", "source_location": "L173", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_public_py", "label": "public.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/public.py", "source_location": "L1"}, {"id": "api_public_avatar_url_field", "label": "_avatar_url_field()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/public.py", "source_location": "L39"}, {"id": "sysuser", "label": "SysUser", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/public.py", "source_location": "L39"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/public.py", "source_location": "L39"}, {"id": "api_public_build_user_info_payload", "label": "_build_user_info_payload()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/public.py", "source_location": "L47"}, {"id": "api_public_jwt_user_payload", "label": "_jwt_user_payload()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/public.py", "source_location": "L63"}, {"id": "api_public_web_login_rsa_public_key", "label": "web_login_rsa_public_key()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/public.py", "source_location": "L76"}, {"id": "api_public_web_login", "label": "web_login()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/public.py", "source_location": "L83"}, {"id": "request", "label": "Request", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/public.py", "source_location": "L83"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/public.py", "source_location": "L83"}, {"id": "api_public_refresh_token", "label": "refresh_token()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/public.py", "source_location": "L141"}, {"id": "api_public_logout", "label": "logout()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/public.py", "source_location": "L203"}, {"id": "api_public_get_current_user_profile", "label": "get_current_user_profile()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/public.py", "source_location": "L222"}, {"id": "api_public_get_web_auth_menu_codes", "label": "get_web_auth_menu_codes()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/public.py", "source_location": "L255"}, {"id": "api_public_public_quote_share", "label": "public_quote_share()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/public.py", "source_location": "L304"}, {"id": "api_public_rationale_1", "label": "\u516c\u5171\u63a5\u53e3\uff08Web \u767b\u5f55\u3001\u5237\u65b0 Token\u3001\u767b\u51fa\uff1b\u7528\u6237\u6a21\u578b\u5bf9\u9f50 docs/sql/shanxia2.0.sql \u57fa\u7ebf\uff09", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/public.py", "source_location": "L1"}, {"id": "api_public_rationale_48", "label": "\u4e0e web-admin `userInfo` \u5b57\u6bb5\u7ea6\u5b9a\u4e00\u81f4\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/public.py", "source_location": "L48"}, {"id": "api_public_rationale_77", "label": "Web \u7aef\u767b\u5f55\u7528 RSA \u516c\u94a5\uff08PKCS#1 v1.5\uff0c\u4e0e JSEncrypt \u4e00\u81f4\uff09", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/public.py", "source_location": "L77"}, {"id": "api_public_rationale_88", "label": "Web \u7aef\u767b\u5f55\uff1a\u8d26\u53f7\u4e3a sys_user.user_no\uff0c\u5bc6\u7801\u4e3a sys_user.password\uff08bcrypt\uff09\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/public.py", "source_location": "L88"}, {"id": "api_public_rationale_204", "label": "\u767b\u51fa\uff1a\u540a\u9500 Redis \u4f1a\u8bdd\uff08\u82e5\u5df2\u914d\u7f6e REDIS_HOST\uff09\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/public.py", "source_location": "L204"}, {"id": "api_public_rationale_223", "label": "\u83b7\u53d6\u5f53\u524d\u7528\u6237\u4fe1\u606f\uff08\u4e0e sys_user \u5b57\u6bb5\u5bf9\u5e94\uff09\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/public.py", "source_location": "L223"}, {"id": "api_public_rationale_256", "label": "\u83b7\u53d6\u5f53\u524d\u7528\u6237\u83dc\u5355\u6743\u9650\u6807\u8bc6\uff08sys_menu.perms\uff0c\u7ecf\u89d2\u8272\u7ee7\u627f\uff09\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/public.py", "source_location": "L256"}, {"id": "api_public_rationale_310", "label": "\Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_models_pl_models_py", "label": "pl_models.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L1"}, {"id": "models_pl_models_plcarrier", "label": "PlCarrier", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L28"}, {"id": "base", "label": "Base", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "models_pl_models_plvehicle", "label": "PlVehicle", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L57"}, {"id": "models_pl_models_plpersonnel", "label": "PlPersonnel", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L83"}, {"id": "models_pl_models_pltransportaddress", "label": "PlTransportAddress", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L105"}, {"id": "models_pl_models_plcert", "label": "PlCert", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L129"}, {"id": "models_pl_models_pldriver", "label": "PlDriver", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L155"}, {"id": "models_pl_models_plvehiclepersonnel", "label": "PlVehiclePersonnel", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L181"}, {"id": "models_pl_models_plvehicledriver", "label": "PlVehicleDriver", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L202"}, {"id": "models_pl_models_plplan", "label": "PlPlan", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L227"}, {"id": "models_pl_models_plplanproduct", "label": "PlPlanProduct", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L326"}, {"id": "models_pl_models_plplanchange", "label": "PlPlanChange", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L382"}, {"id": "models_pl_models_pltransport", "label": "PlTransport", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L409"}, {"id": "models_pl_models_pltransportdblink", "label": "PlTransportDbLink", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L471"}, {"id": "models_pl_models_plplantransport", "label": "PlPlanTransport", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L494"}, {"id": "models_pl_models_pltransportsitelink", "label": "PlTransportSiteLink", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L513"}, {"id": "models_pl_models_pltransportapproval", "label": "PlTransportApproval", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L546"}, {"id": "models_pl_models_plfreightsettlement", "label": "PlFreightSettlement", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L575"}, {"id": "models_pl_models_plfreightsettlementapproval", "label": "PlFreightSettlementApproval", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L607"}, {"id": "models_pl_models_plfreightsettlementtransport", "label": "PlFreightSettlementTransport", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L634"}, {"id": "models_pl_models_plremittancevoucher", "label": "PlRemittanceVoucher", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L654"}, {"id": "models_pl_models_plinventory", "label": "PlInventory", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L681"}, {"id": "models_pl_models_plinventorytransaction", "label": "PlInventoryTransaction", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L713"}, {"id": "models_pl_models_plinventorywarning", "label": "PlInventoryWarning", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_py", "label": "crm.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L1"}, {"id": "api_crm_category_tree", "label": "category_tree()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L327"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L327"}, {"id": "api_crm_customer_list", "label": "customer_list()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L358"}, {"id": "customerlistbody", "label": "CustomerListBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L358"}, {"id": "api_crm_customer_detail", "label": "customer_detail()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L392"}, {"id": "api_crm_customer_create", "label": "customer_create()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L419"}, {"id": "customercreatebody", "label": "CustomerCreateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L419"}, {"id": "api_crm_customer_update", "label": "customer_update()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L449"}, {"id": "customerupdatebody", "label": "CustomerUpdateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L449"}, {"id": "api_crm_customer_delete", "label": "customer_delete()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L476"}, {"id": "customerdeletebody", "label": "CustomerDeleteBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L476"}, {"id": "api_crm_customer_generate_no", "label": "customer_generate_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L504"}, {"id": "api_crm_category_add", "label": "category_add()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L520"}, {"id": "productcategorycreatebody", "label": "ProductCategoryCreateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L520"}, {"id": "api_crm_category_update", "label": "category_update()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L539"}, {"id": "productcategoryupdatebody", "label": "ProductCategoryUpdateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L539"}, {"id": "api_crm_category_delete", "label": "category_delete()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L559"}, {"id": "categorydeletebody", "label": "CategoryDeleteBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L559"}, {"id": "api_crm_product_list", "label": "product_list()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L579"}, {"id": "productlistbody", "label": "ProductListBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L579"}, {"id": "api_crm_product_detail", "label": "product_detail()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L615"}, {"id": "productdetailbody", "label": "ProductDetailBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L615"}, {"id": "api_crm_product_add", "label": "product_add()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L636"}, {"id": "productcreatebody", "label": "ProductCreateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L636"}, {"id": "api_crm_product_update", "label": "product_update()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L660"}, {"id": "productupdatebody", "label": "ProductUpdateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_dibang_bridge_service_py", "label": "pl_dibang_bridge_service.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L1"}, {"id": "services_pl_dibang_bridge_service_dibanginventorylinkresult", "label": "DibangInventoryLinkResult", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L27"}, {"id": "services_pl_dibang_bridge_service_resolve_partner_company", "label": "resolve_partner_company()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L36"}, {"id": "plplan", "label": "PlPlan", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L36"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L36"}, {"id": "services_pl_dibang_bridge_service_normalize_to_date", "label": "_normalize_to_date()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L49"}, {"id": "date", "label": "date", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L49"}, {"id": "datetime", "label": "datetime", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L49"}, {"id": "services_pl_dibang_bridge_service_normalize_plan_date", "label": "_normalize_plan_date()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L61"}, {"id": "services_pl_dibang_bridge_service_resolve_dibang_date", "label": "_resolve_dibang_date()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L73"}, {"id": "services_pl_dibang_bridge_service_month_range", "label": "_month_range()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L84"}, {"id": "services_pl_dibang_bridge_service_plan_type_to_business_type", "label": "_plan_type_to_business_type()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L91"}, {"id": "services_pl_dibang_bridge_service_product_quantity", "label": "_product_quantity()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L99"}, {"id": "plplanproduct", "label": "PlPlanProduct", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L99"}, {"id": "float", "label": "float", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L99"}, {"id": "services_pl_dibang_bridge_service_apply_plan_quantity_delta", "label": "_apply_plan_quantity_delta()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L106"}, {"id": "dbpurchaseplan", "label": "DbPurchasePlan", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L106"}, {"id": "services_pl_dibang_bridge_service_resolve_link_contributed_qty", "label": "_resolve_link_contributed_qty()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L117"}, {"id": "pltransportdblink", "label": "PlTransportDbLink", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L117"}, {"id": "dbinboundoutbound", "label": "DbInboundOutbound", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L117"}, {"id": "services_pl_dibang_bridge_service_subtract_plan_contribution", "label": "_subtract_plan_contribution()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L128"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_locaLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "label": "crm_quote_routes.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L1"}, {"id": "api_crm_quote_routes_register_quote_routes", "label": "register_quote_routes()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L83"}, {"id": "api_crm_quote_routes_rationale_1", "label": "CRM \u62a5\u4ef7\u5355 API \u8def\u7531 \u2014 \u6302\u63a5\u5230 crm.router", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L1"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "datetime", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "fastapi", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L6", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "sqlalchemy_orm", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L7", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_core_database", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L9", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_core_menu_guard", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L10", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_core_response", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L11", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_core_tenant_context", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L12", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_crud_crm_data_scope_helpers", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L13", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_crud_crm_quote", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L14", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_crud_crm_quote_approval", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L36", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_crud_crm_quote_product", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L42", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_crud_crm_quote_share_log", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L48", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_quote_routes_py", "target": "app_schemas_crm_quote", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py", "source_location": "L49", "weight": 1.0}, {"source": "uLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_dibang_bridge_service_py", "label": "pl_dibang_bridge_service.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L1"}, {"id": "services_pl_dibang_bridge_service_dibanginventorylinkresult", "label": "DibangInventoryLinkResult", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L27"}, {"id": "services_pl_dibang_bridge_service_resolve_partner_company", "label": "resolve_partner_company()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L36"}, {"id": "plplan", "label": "PlPlan", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L36"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L36"}, {"id": "services_pl_dibang_bridge_service_normalize_to_date", "label": "_normalize_to_date()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L49"}, {"id": "date", "label": "date", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L49"}, {"id": "datetime", "label": "datetime", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L49"}, {"id": "services_pl_dibang_bridge_service_normalize_plan_date", "label": "_normalize_plan_date()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L61"}, {"id": "services_pl_dibang_bridge_service_resolve_dibang_date", "label": "_resolve_dibang_date()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L73"}, {"id": "services_pl_dibang_bridge_service_month_range", "label": "_month_range()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L84"}, {"id": "services_pl_dibang_bridge_service_plan_type_to_business_type", "label": "_plan_type_to_business_type()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L91"}, {"id": "services_pl_dibang_bridge_service_product_quantity", "label": "_product_quantity()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L99"}, {"id": "plplanproduct", "label": "PlPlanProduct", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L99"}, {"id": "float", "label": "float", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L99"}, {"id": "services_pl_dibang_bridge_service_generate_batch_no", "label": "_generate_batch_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L106"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L106"}, {"id": "services_pl_dibang_bridge_service_find_monthly_db_plan", "label": "find_monthly_db_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L126"}, {"id": "dbpurchaseplan", "label": "DbPurchasePlan", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L126"}, {"id": "services_pl_dibang_bridge_service_create_db_plan", "label": "_create_db_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L155"}, {"id": "services_pl_dibang_bridge_service_create_db_inventory", "label": "_create_db_inventory()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L197"}, {"id": "dbinboundoutbound", "label": "DbInboundOutbound", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py", "source_location": "L197"}, Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_py", "label": "crm.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L1"}, {"id": "api_crm_category_tree", "label": "category_tree()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L327"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L327"}, {"id": "api_crm_customer_list", "label": "customer_list()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L358"}, {"id": "customerlistbody", "label": "CustomerListBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L358"}, {"id": "api_crm_customer_detail", "label": "customer_detail()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L392"}, {"id": "api_crm_customer_create", "label": "customer_create()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L419"}, {"id": "customercreatebody", "label": "CustomerCreateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L419"}, {"id": "api_crm_customer_update", "label": "customer_update()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L449"}, {"id": "customerupdatebody", "label": "CustomerUpdateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L449"}, {"id": "api_crm_customer_delete", "label": "customer_delete()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L476"}, {"id": "customerdeletebody", "label": "CustomerDeleteBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L476"}, {"id": "api_crm_customer_generate_no", "label": "customer_generate_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L504"}, {"id": "api_crm_category_add", "label": "category_add()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L520"}, {"id": "productcategorycreatebody", "label": "ProductCategoryCreateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L520"}, {"id": "api_crm_category_update", "label": "category_update()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L540"}, {"id": "productcategoryupdatebody", "label": "ProductCategoryUpdateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L540"}, {"id": "api_crm_category_delete", "label": "category_delete()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L560"}, {"id": "categorydeletebody", "label": "CategoryDeleteBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L560"}, {"id": "api_crm_product_list", "label": "product_list()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L580"}, {"id": "productlistbody", "label": "ProductListBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L580"}, {"id": "api_crm_product_detail", "label": "product_detail()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L616"}, {"id": "productdetailbody", "label": "ProductDetailBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L616"}, {"id": "api_crm_product_add", "label": "product_add()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L637"}, {"id": "productcreatebody", "label": "ProductCreateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L637"}, {"id": "api_crm_product_update", "label": "product_update()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L661"}, {"id": "productupdatebody", "label": "ProductUpdateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.Line truncated
+1 -1
View File
@@ -1 +1 @@
{"/Users/zlwu/work/00code/shanxia2.0/backend/start-prod.sh":{"size":7357,"mtime_ns":1782639355476060453,"hash":"ca89869446be74a2bf2367b3b28f448580220859a39a84ad9613ac5fe35a8433"},"/Users/zlwu/work/00code/shanxia2.0/backend/start.sh":{"size":9548,"mtime_ns":1780114661446983244,"hash":"4d632863e8a0cd81194b491d83a0218a4157435ae812dec76a6f6197b4d48e57"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/__init__.py":{"size":21,"mtime_ns":1780113982281973984,"hash":"60a04ff08fd97084e83ea809fd13feda6d2848be20ebf309e31fc1dad7523230"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/main.py":{"size":6280,"mtime_ns":1782790852770448643,"hash":"8f9da799977bee7134d87a26ff8434ac5ef8eabb0b7368dd537eee49ad7fd00b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py":{"size":2207,"mtime_ns":1783208067881286091,"hash":"313e6da0e91d743ce891dd283458c79b50b24c4d909dc96ca7b3fd9d2bfa5013"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/auth.py":{"size":7457,"mtime_ns":1782790841500516545,"hash":"34bac56e1c2cb6215f4b5fa2dc32d77d4b4abca51428819eebf4afd269cc51cf"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_crypto.py":{"size":1870,"mtime_ns":1780108930724444498,"hash":"c04b748aea86890a5971f749523c7b17dbad1ddd6f9f7f07a5679d159c984e33"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/login_crypto.py":{"size":3858,"mtime_ns":1780108930725155710,"hash":"b82d51afabf2293a9cb1fb2e5f994c383948e7f47ea95fe674686b4f3f3342b1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/config.py":{"size":6698,"mtime_ns":1782790876685112837,"hash":"9d955a6baa70f78ddfcf29407d9711915a04e59a7f616254d06d25d25e8d0327"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/wechat_pay.py":{"size":23585,"mtime_ns":1780108930725768130,"hash":"3d52265ce65d21e7103f94afa1e8547aee0823b5ea6d65c50317436ac81fb4fc"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py":{"size":11971,"mtime_ns":1782701669198208175,"hash":"0b84c32828939e6102bb3647691fa8f04b1f68e0e0e9f28127374cc44e19a11c"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/database.py":{"size":1510,"mtime_ns":1782790847265601586,"hash":"8bc9c8ae62e2be2c6920036582ba448dfcd1711c41cebb62274ff6bc6ee14b0b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/security.py":{"size":728,"mtime_ns":1780108930725470004,"hash":"df0f01115064f7b8de1c81dbe9a6db39360c236610b68c33b3234a51d7f0025b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/request_payload.py":{"size":2842,"mtime_ns":1780108930725350836,"hash":"e9937151530ab1417ef297aef59111977725cf90be2a8fa76c00a0618789aba0"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/__init__.py":{"size":61,"mtime_ns":1780108930724312414,"hash":"3dd1842ee59b0c804176b03b4b8a323bd4a252b7feb1bbc32db64f3a8928570e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/response.py":{"size":1824,"mtime_ns":1780108930725412462,"hash":"f1739352d65ea8287d76e282a59553aa8d63c97e35764c64ac378aa5bd9c347e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/menu_guard.py":{"size":1611,"mtime_ns":1780108930725223002,"hash":"3e26077288c3620a4780038b891a326206b45bffe00aca545bdae30172487235"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/db_collation.py":{"size":548,"mtime_ns":1780108930725025043,"hash":"0c680e25a18244ca839a7de6ed1f4313b33bc8da118d89c463aee1b6af00c6f9"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/context.py":{"size":421,"mtime_ns":1780108930724824458,"hash":"83a10aee67162374993c10a30bf8bb8bd9c4b35cf48f42c832415ccebd91660f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/utils.py":{"size":1498,"mtime_ns":1780108930725607296,"hash":"09323693661c1d4560458b86a954128ccff82bbaf6522da0b44f24da99ddf9ea"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/storage.py":{"size":14141,"mtime_ns":1782855136810943216,"hash":"3ae2c413c10898a8adb3d6773870bafbfbc18c739c10e0e8b4f765b4b5ca6c08"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/order_status_compat.py":{"size":2828,"mtime_ns":1780108930725284669,"hash":"5349e5a0745ee6734c4a995337779307adefc46f453906171988a77de2081540"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/exceptions.py":{"size":4946,"mtime_ns":1780108930725084335,"hash":"79c997d964bca1a4f12f2b92fb1f181fdb9e2b3cf72eae39f4cc164d23794fd7"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/data_scope.py":{"size":8043,"mtime_ns":1782648925694296304,"hash":"626dff86937e5cacec634ecbc61cd53d3763d760223a1d4ec7cd11f64e59a220"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/admin_request_json.py":{"size":2669,"mtime_ns":1780108930724378914,"hash":"4f68b3cf84291e7bddad6ecf4bb4d29ca5e1ec246a9f7f9c4cf5909d0ec1174a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_crypto_middleware.py":{"size":10876,"mtime_ns":1782790001926627098,"hash":"b39f128b6612589ea6ac5babd30752e9585b3af07b82bb1ad53c9e3c0d90816f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/wechat_message.py":{"size":11194,"mtime_ns":1780108930725684255,"hash":"5086070deb14d99d1a69e419b2299ec4Line truncated
{"/Users/zlwu/work/00code/shanxia2.0/backend/start-prod.sh":{"size":7357,"mtime_ns":1782639355476060453,"hash":"ca89869446be74a2bf2367b3b28f448580220859a39a84ad9613ac5fe35a8433"},"/Users/zlwu/work/00code/shanxia2.0/backend/start.sh":{"size":9548,"mtime_ns":1780114661446983244,"hash":"4d632863e8a0cd81194b491d83a0218a4157435ae812dec76a6f6197b4d48e57"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/__init__.py":{"size":21,"mtime_ns":1780113982281973984,"hash":"60a04ff08fd97084e83ea809fd13feda6d2848be20ebf309e31fc1dad7523230"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/main.py":{"size":6280,"mtime_ns":1782790852770448643,"hash":"8f9da799977bee7134d87a26ff8434ac5ef8eabb0b7368dd537eee49ad7fd00b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py":{"size":2207,"mtime_ns":1783208067881286091,"hash":"313e6da0e91d743ce891dd283458c79b50b24c4d909dc96ca7b3fd9d2bfa5013"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/auth.py":{"size":7457,"mtime_ns":1782790841500516545,"hash":"34bac56e1c2cb6215f4b5fa2dc32d77d4b4abca51428819eebf4afd269cc51cf"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_crypto.py":{"size":1870,"mtime_ns":1780108930724444498,"hash":"c04b748aea86890a5971f749523c7b17dbad1ddd6f9f7f07a5679d159c984e33"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/login_crypto.py":{"size":3858,"mtime_ns":1780108930725155710,"hash":"b82d51afabf2293a9cb1fb2e5f994c383948e7f47ea95fe674686b4f3f3342b1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/config.py":{"size":6716,"mtime_ns":1783761682571576393,"hash":"bbe64081c9790941eb2fa8f9c18ee39daf151068b3503f1fdb74c2cb9f7b5b45"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/wechat_pay.py":{"size":23585,"mtime_ns":1780108930725768130,"hash":"3d52265ce65d21e7103f94afa1e8547aee0823b5ea6d65c50317436ac81fb4fc"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py":{"size":11971,"mtime_ns":1782701669198208175,"hash":"0b84c32828939e6102bb3647691fa8f04b1f68e0e0e9f28127374cc44e19a11c"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/database.py":{"size":1510,"mtime_ns":1782790847265601586,"hash":"8bc9c8ae62e2be2c6920036582ba448dfcd1711c41cebb62274ff6bc6ee14b0b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/security.py":{"size":728,"mtime_ns":1780108930725470004,"hash":"df0f01115064f7b8de1c81dbe9a6db39360c236610b68c33b3234a51d7f0025b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/request_payload.py":{"size":2842,"mtime_ns":1780108930725350836,"hash":"e9937151530ab1417ef297aef59111977725cf90be2a8fa76c00a0618789aba0"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/__init__.py":{"size":61,"mtime_ns":1780108930724312414,"hash":"3dd1842ee59b0c804176b03b4b8a323bd4a252b7feb1bbc32db64f3a8928570e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/response.py":{"size":1824,"mtime_ns":1780108930725412462,"hash":"f1739352d65ea8287d76e282a59553aa8d63c97e35764c64ac378aa5bd9c347e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/menu_guard.py":{"size":1611,"mtime_ns":1780108930725223002,"hash":"3e26077288c3620a4780038b891a326206b45bffe00aca545bdae30172487235"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/db_collation.py":{"size":548,"mtime_ns":1780108930725025043,"hash":"0c680e25a18244ca839a7de6ed1f4313b33bc8da118d89c463aee1b6af00c6f9"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/context.py":{"size":421,"mtime_ns":1780108930724824458,"hash":"83a10aee67162374993c10a30bf8bb8bd9c4b35cf48f42c832415ccebd91660f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/utils.py":{"size":1498,"mtime_ns":1780108930725607296,"hash":"09323693661c1d4560458b86a954128ccff82bbaf6522da0b44f24da99ddf9ea"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/storage.py":{"size":14141,"mtime_ns":1782855136810943216,"hash":"3ae2c413c10898a8adb3d6773870bafbfbc18c739c10e0e8b4f765b4b5ca6c08"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/order_status_compat.py":{"size":2828,"mtime_ns":1780108930725284669,"hash":"5349e5a0745ee6734c4a995337779307adefc46f453906171988a77de2081540"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/exceptions.py":{"size":4946,"mtime_ns":1780108930725084335,"hash":"79c997d964bca1a4f12f2b92fb1f181fdb9e2b3cf72eae39f4cc164d23794fd7"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/data_scope.py":{"size":8043,"mtime_ns":1782648925694296304,"hash":"626dff86937e5cacec634ecbc61cd53d3763d760223a1d4ec7cd11f64e59a220"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/admin_request_json.py":{"size":2669,"mtime_ns":1780108930724378914,"hash":"4f68b3cf84291e7bddad6ecf4bb4d29ca5e1ec246a9f7f9c4cf5909d0ec1174a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_crypto_middleware.py":{"size":10876,"mtime_ns":1782790001926627098,"hash":"b39f128b6612589ea6ac5babd30752e9585b3af07b82bb1ad53c9e3c0d90816f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/wechat_message.py":{"size":11194,"mtime_ns":1780108930725684255,"hash":"5086070deb14d99d1a69e419b2299ec4Line truncated
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,136 @@
"""地磅月计划量运输联动累加/回减单元测试"""
from decimal import Decimal
from unittest.mock import MagicMock, patch
import pytest
from app.models.db_models import DbInboundOutbound, DbPurchasePlan
from app.models.pl_models import PlTransportDbLink
from app.services.pl_dibang_bridge_service import (
_apply_plan_quantity_delta,
_resolve_link_contributed_qty,
revoke_regeneratable_dibang_for_transport,
)
def test_apply_plan_quantity_delta_accumulates():
db_plan = DbPurchasePlan(planned_quantity=30.0)
_apply_plan_quantity_delta(db_plan, 31, user_id="u1")
assert db_plan.planned_quantity == 61.0
assert db_plan.update_by == "u1"
def test_apply_plan_quantity_delta_never_below_zero():
db_plan = DbPurchasePlan(planned_quantity=30.0)
_apply_plan_quantity_delta(db_plan, -50, user_id="u1")
assert db_plan.planned_quantity == 0.0
def test_resolve_link_contributed_qty_prefers_link_field():
link = PlTransportDbLink(contributed_qty=Decimal("30"))
inventory = DbInboundOutbound(quantity=Decimal("99"))
assert _resolve_link_contributed_qty(link, inventory) == 30.0
def test_resolve_link_contributed_qty_falls_back_to_inventory():
link = PlTransportDbLink(contributed_qty=None)
inventory = DbInboundOutbound(quantity=Decimal("31"))
assert _resolve_link_contributed_qty(link, inventory) == 31.0
def test_resolve_link_contributed_qty_zero_when_missing():
link = PlTransportDbLink(contributed_qty=None)
assert _resolve_link_contributed_qty(link, None) == 0.0
@patch("app.services.pl_dibang_bridge_service._inventory_has_weighing_slip", return_value=False)
def test_revoke_subtracts_plan_quantity_on_cancel(_mock_weighing):
db_plan = DbPurchasePlan(id="plan-1", tenant_no="t1", planned_quantity=61.0, del_flag=0)
inventory = DbInboundOutbound(
id="inv-1",
tenant_no="t1",
del_flag=0,
status="待入库",
quantity=Decimal("30"),
batch_no="CRKDH0000001",
)
link = PlTransportDbLink(
id="link-1",
tenant_no="t1",
transport_id="tr-1",
db_inventory_id="inv-1",
db_purchase_plan_id="plan-1",
contributed_qty=Decimal("30"),
del_flag=0,
)
db = MagicMock()
link_query = MagicMock()
link_query.filter.return_value.all.return_value = [link]
inv_query = MagicMock()
inv_query.filter.return_value.first.return_value = inventory
plan_query = MagicMock()
plan_query.filter.return_value.first.return_value = db_plan
def query_router(model):
if model is PlTransportDbLink:
return link_query
if model is DbInboundOutbound:
return inv_query
if model is DbPurchasePlan:
return plan_query
return MagicMock()
db.query.side_effect = query_router
revoke_regeneratable_dibang_for_transport(
db,
tenant_no="t1",
transport_id="tr-1",
user_id="u1",
)
assert db_plan.planned_quantity == 31.0
assert inventory.del_flag == 1
assert link.del_flag == 1
@patch("app.services.pl_dibang_bridge_service._inventory_has_weighing_slip", return_value=True)
def test_revoke_blocks_when_weighing_exists(_mock_weighing):
inventory = DbInboundOutbound(
id="inv-1",
tenant_no="t1",
del_flag=0,
status="待入库",
batch_no="CRKDH0000001",
)
link = PlTransportDbLink(
id="link-1",
tenant_no="t1",
transport_id="tr-1",
db_inventory_id="inv-1",
db_purchase_plan_id="plan-1",
contributed_qty=Decimal("30"),
del_flag=0,
)
db = MagicMock()
link_query = MagicMock()
link_query.filter.return_value.all.return_value = [link]
inv_query = MagicMock()
inv_query.filter.return_value.first.return_value = inventory
db.query.side_effect = lambda model: link_query if model is PlTransportDbLink else inv_query
with pytest.raises(ValueError, match="已关联地磅"):
revoke_regeneratable_dibang_for_transport(
db,
tenant_no="t1",
transport_id="tr-1",
user_id="u1",
)
assert inventory.del_flag == 0
assert link.del_flag == 0
@@ -0,0 +1,3 @@
-- 运输联动贡献量(吨),用于地磅月计划量累加/回减
ALTER TABLE pl_transport_db_link
ADD COLUMN contributed_qty DECIMAL(15,3) NULL COMMENT '本次联动贡献量(吨)' AFTER db_inventory_id;
@@ -0,0 +1,19 @@
-- 修复商机 follow_count 与跟进记录数不一致(历史数据回填)
-- 原因:创建跟进时 recalc 发生在 flush 之前,导致冗余字段长期少 1
UPDATE crm_opportunity o
LEFT JOIN (
SELECT opportunity_id, COUNT(*) AS cnt
FROM crm_opportunity_follow
WHERE del_flag = 0
GROUP BY opportunity_id
) f ON f.opportunity_id = o.id
SET o.follow_count = IFNULL(f.cnt, 0),
o.last_time = (
SELECT MAX(COALESCE(f2.follow_time, f2.create_time))
FROM crm_opportunity_follow f2
WHERE f2.opportunity_id = o.id
AND f2.del_flag = 0
)
WHERE o.del_flag = 0
AND IFNULL(o.follow_count, 0) <> IFNULL(f.cnt, 0);
+1
View File
@@ -356,6 +356,7 @@ CREATE TABLE `pl_transport_db_link` (
`plan_product_id` varchar(64) NOT NULL COMMENT '计划产品行ID',
`db_purchase_plan_id` varchar(64) NOT NULL COMMENT '地磅计划ID',
`db_inventory_id` varchar(64) NOT NULL COMMENT '出入库单ID',
`contributed_qty` decimal(15,3) DEFAULT NULL COMMENT '本次联动贡献量(吨)',
`tenant_no` varchar(64) NOT NULL DEFAULT 'shanxia2.0' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
@@ -0,0 +1,275 @@
# 地磅月计划量运输联动累加与回减 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 运输计划勾选「生成地磅单」时,地磅月计划量按产品行累加;调整运输与取消计划作废旧联动时同步回减贡献量。
**Architecture:** 在 `pl_transport_db_link` 记录 `contributed_qty`;`revoke_regeneratable_dibang_for_transport` 统一回减;`_sync_transport_inventory_links` 统一累加。取消计划与调整运输复用现有 revoke 入口,无需新增 API。
**Tech Stack:** FastAPI + SQLAlchemy + pytest;MySQL 5.7
**OpenSpec:** `openspec/changes/pl-dibang-plan-quantity-accumulation/`
**Design:** `docs/superpowers/specs/2026-07-11-pl-dibang-plan-quantity-accumulation-design.md`
---
## 文件结构
| 操作 | 路径 | 职责 |
|------|------|------|
| Create | `docs/sql/20260711_pl_transport_db_link_contributed_qty.sql` | 增量 DDL |
| Modify | `docs/sql/pl_tables.sql` | 基线同步 `contributed_qty` |
| Modify | `backend/app/models/pl_models.py` | ORM `contributed_qty` |
| Modify | `backend/app/services/pl_dibang_bridge_service.py` | 累加/回减逻辑 |
| Create | `backend/tests/test_pl_dibang_plan_quantity.py` | 单元测试 |
---
### Task 1: 数据库与 ORM
**Files:**
- Create: `docs/sql/20260711_pl_transport_db_link_contributed_qty.sql`
- Modify: `docs/sql/pl_tables.sql`
- Modify: `backend/app/models/pl_models.py`
- [ ] **Step 1: 编写增量 SQL**
```sql
-- 运输联动贡献量(吨),用于月计划量累加/回减
ALTER TABLE pl_transport_db_link
ADD COLUMN contributed_qty DECIMAL(15,3) NULL COMMENT '本次联动贡献量(吨)' AFTER db_inventory_id;
```
- [ ] **Step 2: 更新 `pl_tables.sql`**
在 `pl_transport_db_link` 的 `db_inventory_id` 后增加:
```sql
`contributed_qty` decimal(15,3) DEFAULT NULL COMMENT '本次联动贡献量(吨)',
```
- [ ] **Step 3: ORM 字段**
在 `PlTransportDbLink` 增加:
```python
contributed_qty = Column(DECIMAL(15, 3), nullable=True, comment="本次联动贡献量(吨)")
```
- [ ] **Step 4: 本地执行 DDL**(开发库)
```bash
mysql -u ... -p ... < docs/sql/20260711_pl_transport_db_link_contributed_qty.sql
```
---
### Task 2: 桥接服务 — 辅助函数
**Files:**
- Modify: `backend/app/services/pl_dibang_bridge_service.py`
- [ ] **Step 1: 新增 `_apply_plan_quantity_delta`**
```python
def _apply_plan_quantity_delta(
db_plan: DbPurchasePlan,
delta: float,
*,
user_id: Optional[str],
) -> None:
current = float(db_plan.planned_quantity or 0)
db_plan.planned_quantity = max(0.0, current + float(delta))
db_plan.update_by = user_id
```
- [ ] **Step 2: 新增 `_resolve_link_contributed_qty`**
```python
def _resolve_link_contributed_qty(
link: PlTransportDbLink,
inventory: Optional[DbInboundOutbound],
) -> float:
if link.contributed_qty is not None:
return float(link.contributed_qty)
if inventory and inventory.quantity is not None:
return float(inventory.quantity)
return 0.0
```
- [ ] **Step 3: 新增 `_subtract_plan_contribution`**
在作废每条 link 前调用:
```python
def _subtract_plan_contribution(
db: Session,
*,
tenant_no: str,
link: PlTransportDbLink,
inventory: Optional[DbInboundOutbound],
user_id: Optional[str],
) -> None:
if not link.db_purchase_plan_id:
return
db_plan = (
db.query(DbPurchasePlan)
.filter(
DbPurchasePlan.id == link.db_purchase_plan_id,
DbPurchasePlan.tenant_no == tenant_no,
DbPurchasePlan.del_flag == 0,
)
.first()
)
if not db_plan:
return
qty = _resolve_link_contributed_qty(link, inventory)
if qty:
_apply_plan_quantity_delta(db_plan, -qty, user_id=user_id)
```
---
### Task 3: 桥接服务 — 累加(sync)
**Files:**
- Modify: `backend/app/services/pl_dibang_bridge_service.py`
- [ ] **Step 1: 在 `_sync_transport_inventory_links` 产品循环内,取得 `contrib`**
```python
contrib = _product_quantity(product)
```
- [ ] **Step 2: 复用月计划分支增加累加**
在 `if not db_plan: ... _create_db_plan(...)` 的 `else` 分支(或 `db_plan` 已存在时):
```python
_apply_plan_quantity_delta(db_plan, contrib, user_id=user_id)
```
注意:`_create_db_plan` 新建时 `planned_quantity` 已设为 `contrib`,**仅复用**时执行 `+=`。
- [ ] **Step 3: 创建 link 时写入 `contributed_qty`**
```python
link = PlTransportDbLink(
...
contributed_qty=Decimal(str(contrib)),
...
)
```
---
### Task 4: 桥接服务 — 回减(revoke)
**Files:**
- Modify: `backend/app/services/pl_dibang_bridge_service.py`
- [ ] **Step 1: 在 `revoke_regeneratable_dibang_for_transport` 第二段循环(实际作废)中,软删前调用回减**
在 `if inventory: inventory.del_flag = 1` **之前**:
```python
_subtract_plan_contribution(
db,
tenant_no=tenant_no,
link=link,
inventory=inventory,
user_id=user_id,
)
```
- [ ] **Step 2: 确认 `cancel_plan` 与 `arrange_transport` 调整分支无需改入口**(已调用 revoke)
---
### Task 5: 单元测试
**Files:**
- Create: `backend/tests/test_pl_dibang_plan_quantity.py`
- [ ] **Step 1: 编写测试(使用项目现有 pytest + DB session 夹具模式;若无夹具则用 mock Session 测纯函数)**
至少覆盖:
```python
def test_apply_plan_quantity_delta_never_below_zero():
...
def test_sync_creates_plan_with_contributed_qty():
# 无月计划 → planned_quantity = 30, link.contributed_qty = 30
...
def test_sync_reuses_plan_and_accumulates():
# 已有 30,再 +31 → 61
...
def test_revoke_subtracts_on_cancel():
# 61 - 30 → 31
...
def test_revoke_then_sync_on_rearrange():
# 30 回减后 +25 → 25
...
def test_revoke_blocks_when_weighing_exists():
# 月计划量不变
...
```
- [ ] **Step 2: 运行测试**
```bash
cd backend && source venv/bin/activate && pytest tests/test_pl_dibang_plan_quantity.py -v
```
Expected: 全部 PASS
---
### Task 6: 图谱与验收
- [ ] **Step 1: 更新知识图谱**
```bash
./scripts/update-graph.sh backend
```
- [ ] **Step 2: 手工验收清单**
| 步骤 | 操作 | 预期月计划量 |
|------|------|-------------|
| 1 | 计划 A 安排,B+C,30t,勾地磅 | 30 |
| 2 | 计划 B 安排,B+C,31t,勾地磅 | 61 |
| 3 | 取消计划 A | 31 |
| 4 | 计划 B 调整 31→25,勾地磅 | 25 |
| 5 | 对已完成/已关联磅单单据尝试取消 | 失败,量不变 |
---
## Spec 覆盖自检
| OpenSpec Scenario | Task |
|-------------------|------|
| 首次联动创建月计划 | Task 3 |
| 后续联动累加月计划 | Task 3 |
| 调整运输变更数量 | Task 4 + Task 3 |
| 调整运输取消勾选地磅 | Task 4 |
| 调整运输被阻断 | Task 4(现有门禁不变) |
| 取消计划回减月计划量 | Task 4 |
| 取消计划被阻断 | Task 4 |
| 取消后月计划量下限 | Task 2 `_apply_plan_quantity_delta` |
---
## 执行方式
Plan 已保存至 `docs/superpowers/plans/2026-07-11-pl-dibang-plan-quantity-accumulation.md`。
**1. Subagent-Driven(推荐)** — 每 Task 派发子代理,逐 Task 评审
**2. Inline Execution** — 本会话按 Task 顺序直接改代码
需要我开始实现吗?
@@ -0,0 +1,37 @@
# 地磅月计划量运输联动累加与回减 — 设计文档
> **日期**: 2026-07-11
> **状态**: 待评审
> **OpenSpec**: `openspec/changes/pl-dibang-plan-quantity-accumulation/`
> **前置**: `docs/superpowers/specs/2026-06-20-pl-arrange-dibang-design.md`
## 背景
用户期望:同月同往来单位、同产品的多次运输计划勾选「生成地磅单」时,地磅月计划量应累加;取消或调整运输时应回减已贡献的量。
## 已确认决策
| # | 决策 |
|---|------|
| D1 | 用 `pl_transport_db_link.contributed_qty` 记录单次贡献(吨) |
| D2 | 回减集中在 `revoke_regeneratable_dibang_for_transport`(覆盖调整运输 + 取消计划) |
| D3 | 累加在 `_sync_transport_inventory_links`(新建或复用月计划后 `+=`) |
| D4 | `planned_quantity` 回减下限为 0,不删除月计划行 |
| D5 | 已关联磅单/不可作废出入库单时整单阻断,月计划量不变 |
## 验收场景(摘自 OpenSpec)
| # | 场景 | 预期 |
|---|------|------|
| V1 | A(30) 首次联动 | 月计划量 = 30 |
| V2 | B(31) 同维度再联动 | 月计划量 = 61 |
| V3 | 取消 A | 月计划量 = 31 |
| V4 | 调整 A 30→25 | 月计划量 = 25 |
| V5 | 调整不勾地磅 | 月计划量回减,不新建联动 |
| V6 | 已关联磅单时取消/调整 | 操作失败,月计划量不变 |
## 参考实现位置
- `backend/app/services/pl_dibang_bridge_service.py`
- `backend/app/api/pl/transport.py` → `arrange_transport`
- `backend/app/api/pl/plan.py` → `cancel_plan`
+66 -61
View File
@@ -60,8 +60,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/config.py": {
"mtime": 1782790876.685113,
"ast_hash": "fdb7d2c4aadb0c0ad3f36f003a881896",
"mtime": 1783761682.5715764,
"ast_hash": "770866e1055881cf96183a1c31027d43",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/wechat_pay.py": {
@@ -210,8 +210,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py": {
"mtime": 1783253795.1009343,
"ast_hash": "a0804629d38dcca19b2001cf91ec0673",
"mtime": 1783777743.341288,
"ast_hash": "c922ece8f7295870b9b64f61ee6ddae9",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py": {
@@ -430,13 +430,13 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/product.py": {
"mtime": 1782853111.9225569,
"ast_hash": "2ed01611fcb3ad2c1dde82506fb74439",
"mtime": 1783820526.2041585,
"ast_hash": "40fb32a388517af9a50e6f2906d39c74",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer.py": {
"mtime": 1780713560.8434699,
"ast_hash": "639b5ea6917acab50233b93149d96f3b",
"mtime": 1783823672.041317,
"ast_hash": "39e0e4d75f5e6d344f495bd1de13083e",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/statistics.py": {
@@ -445,7 +445,7 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/contract.py": {
"mtime": 1783760882.0429533,
"mtime": 1783823649.766528,
"ast_hash": "6553f54ca1fd19086c170095f402a628",
"semantic_hash": ""
},
@@ -610,8 +610,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_follow.py": {
"mtime": 1782687669.5521226,
"ast_hash": "74c8c9a39659b8b97adc1b9751b59753",
"mtime": 1783821986.0404174,
"ast_hash": "1a340f1428e7208373ca4ae545e97c56",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/quote.py": {
@@ -680,8 +680,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py": {
"mtime": 1782853124.0849667,
"ast_hash": "c12e0b89c7915836a9c1eb49ca63ce5c",
"mtime": 1783820522.540309,
"ast_hash": "6ff985e8b6c4bc8518976f1a7f5ef63f",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/customer.py": {
@@ -695,8 +695,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py": {
"mtime": 1782687678.1298795,
"ast_hash": "3a03a88e2332b2cf4a88976a191e6583",
"mtime": 1783763318.7403605,
"ast_hash": "5ff35e415f26e1159e79395538aade07",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py": {
@@ -710,8 +710,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/public.py": {
"mtime": 1782790854.8675761,
"ast_hash": "352bfde90d23fdff82eb2189ab058fea",
"mtime": 1783761683.8630195,
"ast_hash": "aefd2bb5fdb09571cd33fa133d12ca55",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/monitor.py": {
@@ -725,8 +725,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py": {
"mtime": 1782853126.2804675,
"ast_hash": "006092a244ecff23fd16cbf142e69437",
"mtime": 1783819836.5052142,
"ast_hash": "185d877b19159d9d996122fea0bb4f17",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/__init__.py": {
@@ -740,8 +740,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py": {
"mtime": 1782650290.4937038,
"ast_hash": "d629c27a20d22a3d731cda0dbec0d6b4",
"mtime": 1783825326.8107653,
"ast_hash": "8f42ad854382af58e5ed9b50192c66a8",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py": {
@@ -750,8 +750,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py": {
"mtime": 1782699376.518852,
"ast_hash": "90194b08a79a8eec2bdf17de6b62d9fe",
"mtime": 1783763319.0708206,
"ast_hash": "44223920a99b6b0796a49232e8c93a8e",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/workflow.py": {
@@ -905,8 +905,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py": {
"mtime": 1783754600.6087332,
"ast_hash": "377caea561fa9566945d26f8867c0d17",
"mtime": 1783768766.0299537,
"ast_hash": "8dc07a801eaf0e590cd8dc3c2e0a0fc3",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/workflow_engine.py": {
@@ -925,8 +925,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py": {
"mtime": 1782227851.746783,
"ast_hash": "9f89467e28bdd7afb685442d68b62d40",
"mtime": 1783777749.4408944,
"ast_hash": "4b28a02e47270ec9d6d563abd6eb2626",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_urea_service.py": {
@@ -935,8 +935,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/web_auth_roles.py": {
"mtime": 1780201410.114958,
"ast_hash": "76817c400c5eb1caf47ebe6aa4ab01a4",
"mtime": 1783761681.6072063,
"ast_hash": "e9e8c0bde894350b65cff1aa3e4b3abd",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_freight_settlement_service.py": {
@@ -2460,8 +2460,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/composables/usePlToxicProductCertCheck.ts": {
"mtime": 1783751322.1412632,
"ast_hash": "83594f6faf7468199fb530a7cd29942c",
"mtime": 1783768767.5174649,
"ast_hash": "e9bc2c8ed7e5d39fbbb850bdf31a45ff",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/locales/index.ts": {
@@ -2955,8 +2955,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/CustomerSelector.vue": {
"mtime": 1782858531.0552773,
"ast_hash": "98771e4601b92a345402756d0f2bf9e1",
"mtime": 1783824595.3424296,
"ast_hash": "9ba7f888219ec2e8a26eb9cc6d9b6d27",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/OpportunitySelector.vue": {
@@ -3600,8 +3600,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/attachment-upload-field/index.vue": {
"mtime": 1783202853.2057269,
"ast_hash": "c8ee3840407e179852078e0e4553c865",
"mtime": 1783823902.3416996,
"ast_hash": "35a5dd3288c74ae0d70f85ec8bbad7db",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-freight-approval-dialog/index.vue": {
@@ -3720,8 +3720,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-arrange-dialog/index.vue": {
"mtime": 1783754107.3458776,
"ast_hash": "a12ea444a2a64e098d24ffe2a6ea6904",
"mtime": 1783769816.3338885,
"ast_hash": "2884ad0b2e6e897f084b283f3d03c993",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/contract-selector/index.vue": {
@@ -4295,8 +4295,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/pl/carrier/index.vue": {
"mtime": 1783176825.8335733,
"ast_hash": "f7db9387cf0c1f529ad1f3b26c34039b",
"mtime": 1783821190.6414018,
"ast_hash": "047b4cc96dc747d17f802d892ba4f424",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/pl/personnel/index.vue": {
@@ -4385,8 +4385,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/clue/detailDrawer.vue": {
"mtime": 1783753619.656547,
"ast_hash": "48cba2f8a723c016ebd5eed3c2110715",
"mtime": 1783821704.5264707,
"ast_hash": "5d701a69b6e84bc4ae55fd343df8fec5",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/clue/convertForm.vue": {
@@ -4400,8 +4400,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/clue/followForm.vue": {
"mtime": 1783167842.1795158,
"ast_hash": "af1803baa6eb8cae5af763ab8c9ddc48",
"mtime": 1783820990.01577,
"ast_hash": "c771ed36b518d5446110277d71b52440",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/clue/detail.vue": {
@@ -4450,8 +4450,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/opportunity/components/FollowDialog.vue": {
"mtime": 1783167842.0515602,
"ast_hash": "a4de29a9699bf3a7608a1fec2962ffc6",
"mtime": 1783821190.7793255,
"ast_hash": "66af79b6f89974751fbc7dd974cd3bd6",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/opportunity/components/BatchUpdateStageDialog.vue": {
@@ -4560,8 +4560,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/index.vue": {
"mtime": 1783753615.4545014,
"ast_hash": "94a65c4ccb9fe9e3e041e7c41525ab70",
"mtime": 1783825109.3807256,
"ast_hash": "fa9a6e35d8e03d26f7a87176918b8735",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/statistics.vue": {
@@ -4590,8 +4590,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteDetailDialog.vue": {
"mtime": 1782858541.3595047,
"ast_hash": "50b376b69b6db7ab1b205d1293c8618b",
"mtime": 1783825601.5186715,
"ast_hash": "d58a617eedcb033280f9a3fd5b533252",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/VersionHistoryDialog.vue": {
@@ -4605,8 +4605,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue": {
"mtime": 1783167832.2729018,
"ast_hash": "ed483352cff8b803943da7f79268c398",
"mtime": 1783824505.3333218,
"ast_hash": "affd16b36a130f3812e5bb2133747a7f",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/ShareDialog.vue": {
@@ -4635,8 +4635,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/product/index.vue": {
"mtime": 1783173429.157619,
"ast_hash": "458f348b8e1c5ea5d8703524b30db38b",
"mtime": 1783821190.641404,
"ast_hash": "a008811667abe7b2d68fa937e4fa2c56",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/customer/index.vue": {
@@ -4645,8 +4645,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/customer/FollowFormDialog.vue": {
"mtime": 1783167838.3380034,
"ast_hash": "43e2fbd04e833b0e8ee58336f6eda942",
"mtime": 1783821191.1033955,
"ast_hash": "8c88f603ed5275aefccc8a40c62a8794",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/customer/TransferDialog.vue": {
@@ -4655,8 +4655,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/customer/CustomerDetailDialog.vue": {
"mtime": 1783171251.6549275,
"ast_hash": "c9daeb848a9d04097221452225be8180",
"mtime": 1783823911.7927911,
"ast_hash": "291b100b9b5ec6f6bd8c6602434265b7",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/customer/DealDialog.vue": {
@@ -4665,8 +4665,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/customer/CustomerFormDialog.vue": {
"mtime": 1783167305.9037223,
"ast_hash": "8369e92c0364a12bd3ef232f496ee02d",
"mtime": 1783823672.333586,
"ast_hash": "0909afcc068c2bc42985e6e37837d176",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/customer/IntoPoolDialog.vue": {
@@ -5010,8 +5010,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/router/guards/beforeEach.ts": {
"mtime": 1783257592.081401,
"ast_hash": "c8dd5356651e9faa03e9247acde72322",
"mtime": 1783761696.732008,
"ast_hash": "ed40e0b8a735e7b96b4993b1f2a461c8",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/router/guards/afterEach.ts": {
@@ -7393,5 +7393,10 @@
"mtime": 1783760394.5188968,
"ast_hash": "2f8542187bc2678c3a779259a43288ee",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_dibang_plan_quantity.py": {
"mtime": 1783777759.8294604,
"ast_hash": "73869416708f10036beec5769af334bb",
"semantic_hash": ""
}
}
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-11
@@ -0,0 +1,69 @@
# 地磅月计划量运输联动累加与回减 — 设计
> **日期**: 2026-07-11
> **OpenSpec**: `openspec/changes/pl-dibang-plan-quantity-accumulation/`
> **前置**: `docs/superpowers/specs/2026-06-20-pl-arrange-dibang-design.md`
## 目标
当月同维度(租户 + 业务类型 + CRM 产品 + 规格 + 往来单位)的地磅月计划,`planned_quantity` 应等于所有**仍有效**的运输地磅联动贡献之和;取消或调整运输作废联动时同步回减。
## 核心决策
| # | 决策 | 说明 |
|---|------|------|
| D1 | **贡献量记账** | `pl_transport_db_link.contributed_qty`(吨)为单次联动写入量;回减以该字段为准 |
| D2 | **贡献量来源** | 创建 link 时取 `_product_quantity(product)`,优先级:`quantity` → `inbound_estimate_qty` → `outbound_estimate_qty` → `0` |
| D3 | **累加时机** | `_sync_transport_inventory_links` 在创建/复用月计划后 `+= contributed_qty` |
| D4 | **回减时机** | `revoke_regeneratable_dibang_for_transport` 在软删 link 前 `-= contributed_qty`;`planned_quantity = max(0, value)` |
| D5 | **调整运输** | 复用 arrange 流程:先 revoke(回减)→ 再 sync(累加新贡献);净效果等同「先减旧再加新」 |
| D6 | **取消计划** | `cancel_plan` 已调用 revoke;回减逻辑集中在 revoke,cancel 无需改入口 |
| D7 | **阻断不变** | 出入库单已关联磅单或状态不可作废时,整单阻断;月计划量不变 |
| D8 | **月计划保留** | 回减至 0 不删除 `db_purchase_plan` 行 |
## 数据流
```
安排运输 (generateDibangPlan=true)
→ 每产品行:
find_monthly_db_plan
├─ 无 → create plan, planned_qty = contrib
└─ 有 → planned_qty += contrib
create inventory + link(contributed_qty=contrib)
调整运输
→ revoke_regeneratable (每条 link: planned_qty -= contributed_qty)
→ sync_dibang (同上累加)
取消计划
→ revoke_regeneratable (同上回减)
```
## 数据库变更
```sql
ALTER TABLE pl_transport_db_link
ADD COLUMN contributed_qty DECIMAL(15,3) NULL COMMENT '本次联动贡献量(吨)' AFTER db_inventory_id;
```
历史数据:`contributed_qty` 为 NULL 时,回减 fallback 读取关联 `db_inbound_outbound.quantity`(作废前)。
## 影响面
| 文件 | 变更 |
|------|------|
| `docs/sql/pl_tables.sql` | 基线 DDL |
| `docs/sql/20260711_pl_transport_db_link_contributed_qty.sql` | 增量迁移 |
| `backend/app/models/pl_models.py` | ORM 字段 |
| `backend/app/services/pl_dibang_bridge_service.py` | 累加/回减辅助函数 |
| `backend/tests/test_pl_dibang_plan_quantity.py` | **新建** 单元测试 |
前端无需改动(行为对用户透明)。
## 风险与处理
| 风险 | 处理 |
|------|------|
| 历史 link 无 `contributed_qty` | 回减时 fallback `inventory.quantity` |
| 扣减后 `planned_quantity < executed_quantity/1000` | 不回写 `executed_quantity`;执行状态由 `weighing_logic` 按现有规则重算 |
| 手工创建的月计划基础量 | 运输联动仅在既有 `planned_quantity` 上累加/回减,不区分手工基数(本期接受) |
@@ -0,0 +1,29 @@
## Why
运输计划勾选「生成地磅单」联动地磅月计划时,当前实现仅在**首次创建**月计划时写入 `planned_quantity`,**复用**已有月计划时不更新计划量;取消计划或调整运输作废旧联动时也不回减。导致同月同往来单位、同产品多次运输后,月计划量与真实运输需求不一致。
## What Changes
- `pl_transport_db_link` 新增 `contributed_qty`(吨),记录单次运输联动对月计划的贡献量
- 安排运输勾选地磅时:新建月计划 `planned_quantity = 贡献量`;复用月计划 `planned_quantity += 贡献量`
- 调整运输、取消计划作废旧联动时:`planned_quantity -= contributed_qty`(下限 0)
- 调整运输重建联动时:先回减旧贡献、再按新产品数量累加新贡献
- 新增/补充单元测试覆盖累加、调整、取消三条路径
## Capabilities
| Capability | Spec |
|------------|------|
| 地磅月计划量运输联动累加与回减 | `specs/dibang-plan-quantity/spec.md` |
## Non-goals
- 不修改地磅磅单(`db_weighing_slip`)生成逻辑
- 不自动重算或回写 `executed_quantity`(仍由磅单业务逻辑维护)
- 不删除 `planned_quantity = 0` 的月计划记录
- 不改变「已关联磅单 / 已完成出入库单阻断作废」的现有门禁
## Dependencies
- 现有 `pl_dibang_bridge_service.py`、`POST /transport/order/arrange/{plan_id}`、`POST /transport/plan/{id}/cancel`
- 前置设计:`docs/superpowers/specs/2026-06-20-pl-arrange-dibang-design.md`
@@ -0,0 +1,84 @@
# Dibang Plan Quantity (地磅月计划量运输联动)
## ADDED Requirements
### Requirement: 运输联动地磅月计划量累加
当运输计划安排运输且勾选「生成地磅单」(`generateDibangPlan=true`)时,系统 SHALL 按产品行联动地磅月计划,并按运输计划产品数量更新月计划 `planned_quantity`(单位:吨)。
贡献量 SHALL 取自 `pl_plan_product`,优先级为:`quantity` → `inbound_estimate_qty` → `outbound_estimate_qty` → `0`。
月计划匹配维度 SHALL 与现有 `find_monthly_db_plan` 一致:同租户、同业务类型(入库/出库)、同 CRM `product_id`、同 `specification`、同 `customer_name`(往来单位),且计划有效期与运输计划地磅日期所在自然月相交。
#### Scenario: 首次联动创建月计划
- **GIVEN** 当月不存在匹配的地磅月计划
- **WHEN** 运输计划 A(往来单位 B、产品 C)勾选生成地磅单且产品数量为 30 吨
- **THEN** 系统 SHALL 创建地磅月计划
- **AND** `db_purchase_plan.planned_quantity` SHALL 为 `30`
- **AND** 系统 SHALL 创建出入库单与 `pl_transport_db_link`
- **AND** `pl_transport_db_link.contributed_qty` SHALL 为 `30`
#### Scenario: 后续联动累加月计划
- **GIVEN** 当月已存在匹配的地磅月计划且 `planned_quantity` 为 `30`
- **WHEN** 运输计划 B(同往来单位 B、同产品 C)勾选生成地磅单且产品数量为 31 吨
- **THEN** 系统 SHALL 复用该地磅月计划(不新建第二条同维度月计划)
- **AND** `db_purchase_plan.planned_quantity` SHALL 更新为 `61`
- **AND** 系统 SHALL 新建出入库单与 `pl_transport_db_link`,`contributed_qty` 为 `31`
---
### Requirement: 调整运输时地磅月计划量回减与重建
当用户对已安排运输的计划执行「调整运输」且该运输单此前存在地磅联动时,系统 SHALL 在重建联动前作废仍可作废的出入库单与关联,并回减对应月计划贡献量;若本次仍勾选生成地磅单,系统 SHALL 按当前计划产品数量重新累加贡献量。
#### Scenario: 调整运输变更数量
- **GIVEN** 运输计划 A 已联动地磅,月计划 `planned_quantity` 为 `30`,`pl_transport_db_link.contributed_qty` 为 `30`
- **WHEN** 用户对计划 A 执行调整运输,勾选生成地磅单,且产品数量变更为 `25` 吨
- **THEN** 系统 SHALL 作废旧出入库单与关联,并将月计划 `planned_quantity` 回减为 `0`
- **AND** 系统 SHALL 创建新出入库单与关联,`contributed_qty` 为 `25`
- **AND** 月计划 `planned_quantity` SHALL 为 `25`
#### Scenario: 调整运输取消勾选地磅
- **GIVEN** 运输计划 A 已联动地磅,月计划 `planned_quantity` 为 `30`
- **WHEN** 用户对计划 A 执行调整运输且**未**勾选生成地磅单
- **THEN** 系统 SHALL 作废旧出入库单与关联,并将月计划 `planned_quantity` 回减为 `0`
- **AND** 系统 SHALL NOT 创建新的地磅联动记录
#### Scenario: 调整运输被阻断
- **GIVEN** 运输计划 A 的联动出入库单已关联有效地磅磅单,或出入库单状态不在 `{待入库, 待出库, 待三方填写, 待处理}`
- **WHEN** 用户对计划 A 执行调整运输
- **THEN** 系统 SHALL 拒绝操作并提示需在地磅管理中先处理
- **AND** 月计划 `planned_quantity` SHALL 保持不变
---
### Requirement: 取消计划时地磅月计划量回减
当用户取消运输计划且该计划关联的运输单存在地磅联动时,系统 SHALL 作废仍可作废的出入库单与关联,并从对应地磅月计划回减贡献量。
#### Scenario: 取消计划回减月计划量
- **GIVEN** 运输计划 A(30 吨)与运输计划 B(31 吨)均已联动同一地磅月计划,`planned_quantity` 为 `61`
- **WHEN** 用户取消运输计划 A
- **THEN** 系统 SHALL 软删计划 A 的出入库单与 `pl_transport_db_link`
- **AND** 地磅月计划 `planned_quantity` SHALL 更新为 `31`
- **AND** 计划 A 的 `follow_status` SHALL 为 `cancelled`
#### Scenario: 取消计划被阻断
- **GIVEN** 运输计划 A 的联动出入库单已关联有效地磅磅单或状态不可作废
- **WHEN** 用户取消运输计划 A
- **THEN** 系统 SHALL 拒绝取消并提示需在地磅管理中先处理
- **AND** 月计划 `planned_quantity` SHALL 保持不变
#### Scenario: 取消后月计划量下限
- **GIVEN** 仅运输计划 A(30 吨)联动地磅月计划,`planned_quantity` 为 `30`
- **WHEN** 用户取消运输计划 A
- **THEN** 月计划 `planned_quantity` SHALL 为 `0`
- **AND** 系统 SHALL NOT 物理删除该 `db_purchase_plan` 记录
@@ -0,0 +1,23 @@
# Tasks: 地磅月计划量运输联动累加与回减
## 1. Schema 与迁移
- [x] 1.1 编写 `docs/sql/20260711_pl_transport_db_link_contributed_qty.sql`
- [x] 1.2 同步 `docs/sql/pl_tables.sql` 中 `pl_transport_db_link.contributed_qty`
- [x] 1.3 `backend/app/models/pl_models.py` 增加 `PlTransportDbLink.contributed_qty`
## 2. 桥接服务
- [x] 2.1 `pl_dibang_bridge_service.py` 新增 `_apply_plan_quantity_delta`、`_resolve_link_contributed_qty`
- [x] 2.2 `_sync_transport_inventory_links`:复用月计划时 `+= contributed_qty`;写入 link.contributed_qty
- [x] 2.3 `revoke_regeneratable_dibang_for_transport`:作废前 `-= contributed_qty`(下限 0)
## 3. 测试
- [x] 3.1 新建 `backend/tests/test_pl_dibang_plan_quantity.py`
- [x] 3.2 覆盖:累加、回减、阻断(本地 venv 损坏,已用 python3 手工验证核心路径)
## 4. 验收
- [ ] 4.1 本地 pytest 通过(需修复 venv 或 `pip install pytest`)
- [ ] 4.2 手工:A(30)+B(31)→61;取消 A→31;调整 A 30→25→净 25
+12 -5
View File
@@ -689,7 +689,6 @@
"687": "Community 687",
"688": "Community 688",
"689": "Community 689",
"690": "Community 690",
"691": "Community 691",
"692": "Community 692",
"693": "Community 693",
@@ -711,17 +710,21 @@
"709": "Community 709",
"710": "Community 710",
"711": "Community 711",
"712": "Community 712",
"713": "Community 713",
"714": "Community 714",
"715": "Community 715",
"716": "Community 716",
"717": "Community 717",
"718": "Community 718",
"719": "Community 719",
"721": "Community 721",
"722": "Community 722",
"723": "Community 723",
"724": "Community 724",
"725": "Community 725",
"726": "Community 726",
"727": "Community 727",
"728": "Community 728",
"729": "Community 729",
"730": "Community 730",
"731": "Community 731",
"732": "Community 732",
"733": "Community 733",
@@ -729,5 +732,9 @@
"735": "Community 735",
"736": "Community 736",
"737": "Community 737",
"739": "Community 739"
"738": "Community 738",
"739": "Community 739",
"740": "Community 740",
"741": "Community 741",
"743": "Community 743"
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+140 -6
View File
@@ -2,7 +2,7 @@
<el-dialog
title="选择客户"
v-model="dialogVisible"
width="1280px"
width="1500px"
align-center
class="dialog-form-layout"
modal-class="order-module-dialog-mask"
@@ -39,6 +39,7 @@
<div class="panel dialog-list-panel">
<div class="list-body list-wrap dialog-list-body-only">
<el-table
ref="tableRef"
v-loading="loading"
:data="customerList"
class="list-table"
@@ -48,7 +49,13 @@
:max-height="400"
>
<el-table-column type="index" label="序号" min-width="60" align="center" header-align="center" />
<el-table-column label="客户名称" prop="customerName" min-width="230" align="center" header-align="center" :show-overflow-tooltip="true" />
<el-table-column label="客户名称" prop="customerName" min-width="200" align="center" header-align="center" :show-overflow-tooltip="true" />
<el-table-column label="客户简称" prop="shortName" min-width="120" align="center" header-align="center" :show-overflow-tooltip="true">
<template #default="scope">
<span v-if="hasShortName(scope.row)">{{ scope.row.shortName }}</span>
<el-tag v-else type="warning" size="small">未填写</el-tag>
</template>
</el-table-column>
<el-table-column label="客户类型" prop="customerType" min-width="150" align="center" header-align="center">
<template #default="scope">
<DictTag dict-type="crm_customer_type" :value="scope.row.customerType" />
@@ -72,6 +79,13 @@
</template>
</el-table-column>
<el-table-column label="负责人" prop="ownerUserName" min-width="100" align="center" header-align="center" :show-overflow-tooltip="true" />
<el-table-column label="操作" fixed="right" min-width="120" align="center" header-align="center" class-name="col-actions">
<template #default="scope">
<el-button link type="primary" @click.stop="handleEditShortName(scope.row)">
<el-icon><Edit /></el-icon>{{ hasShortName(scope.row) ? '编辑简称' : '填写简称' }}
</el-button>
</template>
</el-table-column>
</el-table>
<ListPagination
@@ -90,6 +104,7 @@
<div class="selected-customer">
<el-descriptions :column="2" border>
<el-descriptions-item label="客户名称">{{ currentCustomer.customerName }}</el-descriptions-item>
<el-descriptions-item label="客户简称">{{ currentCustomer.shortName }}</el-descriptions-item>
<el-descriptions-item label="客户类型">
<DictTag dict-type="crm_customer_type" :value="currentCustomer.customerType" />
</el-descriptions-item>
@@ -110,7 +125,55 @@
<el-button class="btn-ghost" @click="handleClose">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button class="btn-main" type="primary" @click="handleConfirm" :disabled="!currentCustomer">
<el-button
class="btn-main"
type="primary"
@click="handleConfirm"
:disabled="!currentCustomer || !hasShortName(currentCustomer)"
>
<el-icon><Check /></el-icon>确定
</el-button>
</div>
</template>
</el-dialog>
<!-- 填写/编辑客户简称 -->
<el-dialog
v-model="shortNameDialogVisible"
:title="hasShortName(shortNameForm) ? '编辑客户简称' : '填写客户简称'"
width="480px"
align-center
class="dialog-form-layout"
modal-class="order-module-dialog-mask"
:close-on-click-modal="false"
append-to-body
>
<el-form
ref="shortNameFormRef"
:model="shortNameForm"
:rules="shortNameRules"
label-position="top"
class="crm-dialog-form"
>
<el-form-item label="客户名称">
<el-input :model-value="shortNameForm.customerName" disabled />
</el-form-item>
<el-form-item label="客户简称" prop="shortName">
<el-input
v-model="shortNameForm.shortName"
placeholder="请输入客户简称"
maxlength="100"
show-word-limit
clearable
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button class="btn-ghost" @click="shortNameDialogVisible = false">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button class="btn-main" type="primary" :loading="shortNameSaving" @click="handleSaveShortName">
<el-icon><Check /></el-icon>确定
</el-button>
</div>
@@ -122,10 +185,10 @@
import { useDictStore } from '@/store/modules/dict'
import DictTag from '@/components/DictTag/index.vue'
import ListPagination from '@/components/ListPagination/index.vue'
import { Check, Close, Refresh, Search } from '@element-plus/icons-vue'
import { Check, Close, Edit, Refresh, Search } from '@element-plus/icons-vue'
import { ref, watch } from 'vue'
import type { ElFormInstance } from 'element-plus'
import { fetchCustomerList } from '@/api/crm/customer'
import type { ElFormInstance, FormItemRule } from 'element-plus'
import { fetchCustomerList, fetchUpdateCustomer } from '@/api/crm/customer'
const dictStore = useDictStore()
@@ -167,6 +230,21 @@ const loading = ref(false)
const total = ref(0)
const currentCustomer = ref<any | null>(null)
const queryFormRef = ref<ElFormInstance>()
const tableRef = ref<any>()
const shortNameDialogVisible = ref(false)
const shortNameSaving = ref(false)
const shortNameFormRef = ref<ElFormInstance>()
const shortNameForm = ref({
id: '' as string | number,
customerName: '',
shortName: ''
})
const shortNameRules: Record<string, FormItemRule[]> = {
shortName: [{ required: true, message: '客户简称不能为空', trigger: 'blur' }]
}
const hasShortName = (row?: { shortName?: string } | null) => Boolean(String(row?.shortName || '').trim())
const getList = async () => {
loading.value = true
@@ -213,10 +291,66 @@ watchDebounced(
)
const handleCurrentChange = (row: any) => {
if (!row) {
currentCustomer.value = null
return
}
if (!hasShortName(row)) {
ElMessage.warning('该客户尚未填写简称,请先填写客户简称后再选择')
tableRef.value?.setCurrentRow(undefined)
currentCustomer.value = null
return
}
currentCustomer.value = row
}
const handleEditShortName = (row: any) => {
shortNameForm.value = {
id: row.id,
customerName: row.customerName || '',
shortName: row.shortName || ''
}
shortNameDialogVisible.value = true
nextTick(() => shortNameFormRef.value?.clearValidate())
}
const handleSaveShortName = async () => {
const valid = await shortNameFormRef.value?.validate().catch(() => false)
if (!valid) return
shortNameSaving.value = true
try {
const trimmedShortName = shortNameForm.value.shortName.trim()
await fetchUpdateCustomer({
id: shortNameForm.value.id,
shortName: trimmedShortName
})
ElMessage.success('客户简称保存成功')
const targetId = shortNameForm.value.id
const updatedRow = customerList.value.find((item) => item.id === targetId)
if (updatedRow) {
updatedRow.shortName = trimmedShortName
}
if (currentCustomer.value?.id === targetId) {
currentCustomer.value = { ...currentCustomer.value, shortName: trimmedShortName }
}
shortNameDialogVisible.value = false
} catch (error) {
console.error('保存客户简称失败:', error)
ElMessage.error('保存客户简称失败')
} finally {
shortNameSaving.value = false
}
}
const handleConfirm = () => {
if (!currentCustomer.value) return
if (!hasShortName(currentCustomer.value)) {
ElMessage.warning('该客户尚未填写简称,请先填写客户简称后再选择')
return
}
emit('confirm', currentCustomer.value)
handleClose()
}
@@ -54,6 +54,9 @@
</span>
</div>
</template>
<span v-if="getCategoryLabel(file)" class="attachment-preview__category">
{{ getCategoryLabel(file) }}
</span>
</div>
<button
v-if="compact && !disabled"
@@ -165,6 +168,8 @@ const props = withDefaults(
disabled?: boolean
/** 紧凑模式:小缩略图,只读时隐藏文件名与操作栏 */
compact?: boolean
/** 附件 URL → 分类名称,用于查看态在缩略图上展示分类标签 */
categoryLabels?: Record<string, string>
}>(),
{
modelValue: '',
@@ -173,7 +178,8 @@ const props = withDefaults(
accept: 'image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar',
hint: '支持图片、视频、PDF、Word、Excel等文件',
disabled: false,
compact: false
compact: false,
categoryLabels: () => ({})
}
)
@@ -269,6 +275,11 @@ function getFileExtension(fileName: string) {
return fileName.substring(lastDotIndex + 1).toUpperCase()
}
function getCategoryLabel(file: AttachmentItem) {
const label = props.categoryLabels?.[file.fileUrl]
return label ? String(label).trim() : ''
}
function canPreview(file: AttachmentItem) {
return Boolean(file.fileUrl) && (isImageFile(file) || isVideoFile(file) || isPdfFile(file))
}
@@ -420,6 +431,7 @@ defineExpose({ hasUploading })
}
.attachment-item {
flex: 0 0 auto;
width: 140px;
border: 1px solid #e2e8f0;
border-radius: 8px;
@@ -438,6 +450,7 @@ defineExpose({ hasUploading })
}
.attachment-preview {
position: relative;
width: 100%;
height: 96px;
display: flex;
@@ -490,6 +503,21 @@ defineExpose({ hasUploading })
color: #1e293b;
}
.attachment-preview__category {
position: absolute;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
padding: 2px 4px;
background: rgba(15, 23, 42, 0.62);
color: #fff;
font-size: 11px;
line-height: 1.3;
text-align: center;
word-break: keep-all;
}
.attachment-footer {
display: flex;
flex-direction: column;
@@ -565,7 +593,8 @@ defineExpose({ hasUploading })
}
:deep(.el-upload) {
display: block;
display: inline-flex;
flex: 0 0 auto;
}
.attachment-upload--compact {
@@ -204,7 +204,7 @@
调整运输时现场链接将轮换更新,旧现场链接将失效。
</template>
<template v-else-if="showDibangHint">
勾选后将按产品进行地磅单生成(当月已有则复用),并在地磅出入库管理中产生对应记录。
勾选后将按产品进行地磅单生成(当月已有则复用);入库单日期取送达时间,出库单日期取取货时间,未填时回退计划日期。
</template>
</p>
</div>
@@ -1,12 +1,12 @@
<template>
<div class="view-kv-table-cell">
<el-tag v-if="field.tagType" :type="field.tagType" size="small">{{
<el-tag v-if="field.tagType" :type="field.tagType" size="small" class="view-kv-tag">{{
formatViewValue(field.value)
}}</el-tag>
<ul v-else-if="field.items?.length" class="view-plain-list">
<li v-for="(item, idx) in field.items" :key="idx">{{ item }}</li>
</ul>
<span v-else :class="{ 'money-display': field.money, 'muted-display': isMuted }">{{
<span v-else :class="{ 'money-display': field.money, 'danger-display': field.danger, 'muted-display': isMuted }">{{
formatViewValue(field.value)
}}</span>
<el-button
@@ -49,7 +49,7 @@
const onFieldView = inject<((field: ViewField) => void) | undefined>(VIEW_KV_FIELD_VIEW_KEY)
const isMuted = computed(
() => !props.field.money && formatViewValue(props.field.value) === '—'
() => !props.field.money && !props.field.danger && formatViewValue(props.field.value) === '—'
)
const showViewBtn = computed(() => {
@@ -91,6 +91,10 @@
gap: 6px;
}
.view-kv-tag {
font-weight: 600;
}
.view-kv-field-link {
margin: 0;
padding: 0 4px;
@@ -112,4 +116,9 @@
color: var(--text-muted, #64748b);
font-weight: 600;
}
.danger-display {
color: #dc2626;
font-weight: 600;
}
</style>
@@ -18,6 +18,8 @@ export interface ViewField {
fullSpan?: boolean
tagType?: TagType
money?: boolean
/** 红色加粗强调展示(如结算方式等关键业务字段) */
danger?: boolean
/** 值后展示「查看」按钮,点击时 ViewKvTable 抛出 field-view */
viewTarget?: 'customer' | 'opportunity' | 'quote'
viewId?: string | number | null
@@ -141,14 +141,16 @@ export function isHazardCertReady(plan: { hazardCertReady?: number } | null | un
return Number(plan?.hazardCertReady ?? 0) === 1
}
/** 含危化品且尚未人工确认证书齐备时,阻断安排运输 */
/** 含危化品且尚未确认证书齐备(人工确认或全部所需证书已上传)时,阻断安排运输 */
export function shouldBlockTransportForHazardCert(
plan: { hazardCertReady?: number; products?: PlanProduct[] } | null | undefined,
products?: PlanProduct[]
) {
const prods = products ?? plan?.products
if (!planHasHazardProducts(prods)) return false
return !isHazardCertReady(plan)
if (isHazardCertReady(plan)) return false
if (!findMissingHazardCerts(prods ?? []).length) return false
return true
}
export function buildMissingHazardCertReadyMessage(
@@ -156,7 +158,10 @@ export function buildMissingHazardCertReadyMessage(
products?: PlanProduct[]
) {
if (!shouldBlockTransportForHazardCert(plan, products)) return ''
return '该计划危化品证书尚未确认齐备'
const prods = products ?? plan?.products ?? []
const missingCertsMsg = buildMissingHazardCertMessage(prods)
if (missingCertsMsg) return missingCertsMsg
return '该计划危化品证书尚未确认齐备,请点击「证书齐备」或补全所需证书'
}
function formatMissingCertLabels(product: PlanProduct): string {
+22 -2
View File
@@ -280,9 +280,29 @@ async function handleDynamicRoutes(
// 2. 获取菜单数据
const menuList = await menuProcessor.getMenuList()
// 3. 验证菜单数据
// 3. 无菜单权限(如未绑定角色):允许空菜单并完成初始化,跳转 403
if (!menuProcessor.validateMenuList(menuList)) {
throw new Error('获取菜单列表失败,请重新登录')
routeRegistry?.register([])
const menuStore = useMenuStore()
menuStore.setMenuList([])
menuStore.addRemoveRouteFns(routeRegistry?.getRemoveRouteFns() || [])
IframeRouteManager.getInstance().save()
useWorktabStore().validateWorktabs(router)
routeInitInProgress = false
closeLoading()
if (isStaticRoute(to.path)) {
next({
path: to.path,
query: to.query,
hash: to.hash,
replace: true
})
return
}
next({ path: '/403', replace: true })
return
}
// 4. 注册动态路由
@@ -193,8 +193,9 @@
if (c.mobile) parts.push(c.mobile)
if (c.status) {
parts.push(dictStore.getLabel('crm_clue_status', c.status))
} else if (c.isConverted === 1) {
parts.push('已转化')
}
if (c.isConverted === 1) parts.push('已转化')
return parts.join(' · ')
})
@@ -338,7 +339,6 @@
}
const handleInvalidSuccess = async () => {
ElMessage.success('标记无效成功')
await loadDetail()
emit('success')
}
@@ -350,7 +350,6 @@
}
const handleConvertSuccess = async () => {
ElMessage.success('转化成功')
await loadDetail()
emit('success')
}
+1 -1
View File
@@ -56,7 +56,7 @@
v-model="attachmentUrls"
folder="crm/clue/follow"
:limit="15"
hint="支持图片、视频、PDF、Word 等文件,单文件不超过 20MB,最多 15 个"
hint="支持图片、视频、PDF、Word 等文件,最多 15 个"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
@@ -14,8 +14,7 @@
ref="formRef"
:model="form"
:rules="rules"
label-position="right"
label-width="100px"
label-position="top"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
@@ -1381,7 +1381,11 @@ const detailBasicViewFields = computed<ViewField[]>(() => {
? []
: [{ label: '合同金额(元)', value: `¥${formatDetailAmount(c.amount)}`, money: true }]),
{ label: '交易主体', value: c.supplierName },
detailTagField('结算方式', 'crm_settlement_method', c.settlementMethod)
{
label: '结算方式',
value: c.settlementMethod ? dictStore.getLabel('crm_settlement_method', c.settlementMethod) : '—',
danger: true
}
];
});
+122 -20
View File
@@ -279,7 +279,7 @@
</el-table-column>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="合同编号" align="center" prop="contractNo" min-width="170" show-overflow-tooltip />
<el-table-column label="合同名称" prop="contractName" show-overflow-tooltip min-width="200">
<el-table-column label="合同名称" prop="contractName" show-overflow-tooltip min-width="280">
<template #default="scope">
<el-link type="primary" @click="handleView(scope.row)">{{ scope.row.contractName }}</el-link>
</template>
@@ -349,14 +349,6 @@
>
</template>
</el-table-column>
<el-table-column v-if="isColumnVisible('receivedAmount')" label="已回款" align="right" prop="receivedAmount" min-width="130">
<template #default="scope">
<span v-if="scope.row.receivedAmount && scope.row.receivedAmount > 0" class="text-success"
>¥{{ parseFloat(scope.row.receivedAmount).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}</span
>
<span v-else class="text-muted">¥0.00</span>
</template>
</el-table-column>
<el-table-column v-if="isColumnVisible('unreceivedAmount')" label="未回款" align="right" prop="unreceivedAmount" min-width="130">
<template #default="scope">
<span
@@ -366,14 +358,6 @@
>
</template>
</el-table-column>
<el-table-column v-if="isColumnVisible('receivedRate')" label="回款进度" align="center" min-width="120">
<template #default="scope">
<el-progress
:percentage="getReceivedRate(scope.row.contractAmount, scope.row.receivedAmount)"
:color="getProgressColor(getReceivedRate(scope.row.contractAmount, scope.row.receivedAmount))"
/>
</template>
</el-table-column>
<el-table-column v-if="isColumnVisible('invoiceAmount')" label="已开票金额" align="right" prop="invoiceAmount" min-width="130">
<template #default="scope">
<span
@@ -421,6 +405,22 @@
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column v-if="isColumnVisible('receivedAmount')" label="已回款" align="right" prop="receivedAmount" min-width="130">
<template #default="scope">
<span v-if="scope.row.receivedAmount && scope.row.receivedAmount > 0" class="text-success"
>¥{{ parseFloat(scope.row.receivedAmount).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}</span
>
<span v-else class="text-muted">¥0.00</span>
</template>
</el-table-column>
<el-table-column v-if="isColumnVisible('receivedRate')" label="回款进度" align="center" min-width="120">
<template #default="scope">
<el-progress
:percentage="getReceivedRate(scope.row.contractAmount, scope.row.receivedAmount)"
:color="getProgressColor(getReceivedRate(scope.row.contractAmount, scope.row.receivedAmount))"
/>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('ownerUserName')"
label="负责人"
@@ -438,7 +438,7 @@
show-overflow-tooltip
/>
<el-table-column v-if="isColumnVisible('createByName')" label="创建人" align="center" prop="createByName" min-width="100" />
<el-table-column v-if="isColumnVisible('createTime')" label="创建时间" align="center" prop="createTime" min-width="160" show-overflow-tooltip>
<el-table-column v-if="isColumnVisible('createTime')" label="创建时间" align="center" prop="createTime" min-width="140" show-overflow-tooltip>
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
@@ -581,9 +581,7 @@ const listColumnDefs = [
{ prop: 'renewalCount', label: '续签次数' },
{ prop: 'attachmentFiles', label: '合同附件' },
{ prop: 'amount', label: '合同金额(元)' },
{ prop: 'receivedAmount', label: '已回款' },
{ prop: 'unreceivedAmount', label: '未回款' },
{ prop: 'receivedRate', label: '回款进度' },
{ prop: 'invoiceAmount', label: '已开票金额' },
{ prop: 'uninvoiceAmount', label: '未开票金额' },
{ prop: 'status', label: '合同状态' },
@@ -595,6 +593,8 @@ const listColumnDefs = [
{ prop: 'partyA', label: '甲方' },
{ prop: 'partyB', label: '乙方' },
{ prop: 'settlementMethod', label: '结算方式' },
{ prop: 'receivedAmount', label: '已回款' },
{ prop: 'receivedRate', label: '回款进度' },
{ prop: 'ownerUserName', label: '负责人' },
{ prop: 'createDeptName', label: '创建部门' },
{ prop: 'createByName', label: '创建人' },
@@ -1342,4 +1342,106 @@ onMounted(async () => {
color: var(--mint-primary, #1ca851);
}
.attachment-list-cell {
display: flex;
flex-wrap: nowrap;
gap: 8px;
justify-content: flex-start;
align-items: flex-start;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
padding: 4px 0;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
.attachment-thumb-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
flex-shrink: 0;
width: 58px;
}
.attachment-preview-wrap {
position: relative;
width: 50px;
height: 50px;
flex-shrink: 0;
}
.attachment-thumb-image {
width: 50px;
height: 50px;
border-radius: 4px;
border: 1px solid #e2e8f0;
object-fit: cover;
display: block;
}
.attachment-file-icon {
width: 50px;
height: 50px;
border-radius: 4px;
border: 1px solid #e2e8f0;
background: #f8fafc;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: all 0.2s;
.el-icon {
font-size: 22px;
color: #1e293b;
}
&:hover {
border-color: #1ca851;
background: #e6f8ed;
.el-icon {
color: #1ca851;
}
}
}
.attachment-download-btn {
position: absolute;
right: -4px;
bottom: -4px;
width: 18px;
height: 18px;
border-radius: 50%;
background: #fff;
border: 1px solid #e2e8f0;
color: #1e293b;
cursor: pointer;
z-index: 2;
&:hover {
color: #1ca851;
border-color: #1ca851;
background: #e6f8ed;
}
}
.attachment-category-label {
display: block;
width: 100%;
font-size: 11px;
line-height: 1.3;
color: #1e293b;
text-align: center;
white-space: normal;
word-break: break-all;
}
}
</style>
@@ -15,20 +15,11 @@
append-to-body
>
<template #certificates>
<div class="cert-list">
<div v-if="customerInfo.businessLicenseImages" class="cert-block">
<div class="view-field-label">营业执照</div>
<AttachmentUploadField :model-value="customerInfo.businessLicenseImages" disabled />
</div>
<div v-if="customerInfo.businessPermitImages" class="cert-block">
<div class="view-field-label">经营许可证</div>
<AttachmentUploadField :model-value="customerInfo.businessPermitImages" disabled />
</div>
<div v-if="customerInfo.otherCertImages" class="cert-block">
<div class="view-field-label">其他证件</div>
<AttachmentUploadField :model-value="customerInfo.otherCertImages" disabled />
</div>
</div>
<AttachmentUploadField
:model-value="certificateDisplay.modelValue"
:category-labels="certificateDisplay.categoryLabels"
disabled
/>
</template>
<template #hero-actions>
@@ -547,6 +538,32 @@ const isToday = computed(() => {
return today === customerInfo.value.nextTime.split(' ')[0]
})
function appendCertUrls(raw: string | undefined, label: string, urls: string[], labels: Record<string, string>) {
if (!raw) return
String(raw)
.split(',')
.map((item) => item.trim())
.filter(Boolean)
.forEach((url) => {
urls.push(url)
labels[url] = label
})
}
function buildCertDisplay(customer: CustomerVO) {
const urls: string[] = []
const labels: Record<string, string> = {}
appendCertUrls(customer.businessLicenseImages, '营业执照', urls, labels)
appendCertUrls(customer.businessPermitImages, '经营许可证', urls, labels)
appendCertUrls(customer.otherCertImages, '其他证件', urls, labels)
return {
modelValue: urls.join(','),
categoryLabels: labels
}
}
const certificateDisplay = computed(() => buildCertDisplay(customerInfo.value))
const viewHeroMeta = computed(() => {
const c = customerInfo.value
const parts: string[] = []
@@ -993,18 +1010,6 @@ defineExpose({ open })
}
}
.cert-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.cert-block {
.view-field-label {
margin-bottom: 8px;
}
}
:deep(.el-timeline-item__timestamp) {
color: #1e293b;
font-size: 12px;
@@ -152,25 +152,25 @@
<el-divider content-position="left">证件信息</el-divider>
<div class="form-grid-4">
<el-form-item label="营业执照" prop="businessLicenseImages">
<el-form-item label="营业执照" prop="businessLicenseImages" class="form-field-span-2">
<AttachmentUploadField
ref="businessLicenseUploadRef"
v-model="form.businessLicenseImages"
:limit="5"
accept="image/*,.pdf"
folder="crm/customer/license"
hint="支持 JPG、PNG、PDF,单文件不超过 20MB,最多 5 个"
hint="支持 JPG、PNG、PDF,最多 5 个"
@update:model-value="handleCertFieldChange('businessLicenseImages')"
/>
</el-form-item>
<el-form-item label="经营许可证" prop="businessPermitImages">
<el-form-item label="经营许可证" prop="businessPermitImages" class="form-field-span-2">
<AttachmentUploadField
ref="businessPermitUploadRef"
v-model="form.businessPermitImages"
:limit="5"
accept="image/*,.pdf"
folder="crm/customer/permit"
hint="支持 JPG、PNG、PDF,单文件不超过 20MB,最多 5 个"
hint="支持 JPG、PNG、PDF,最多 5 个"
@update:model-value="handleCertFieldChange('businessPermitImages')"
/>
</el-form-item>
@@ -181,7 +181,7 @@
:limit="10"
accept="image/*,.pdf"
folder="crm/customer/cert"
hint="支持 JPG、PNG、PDF,单文件不超过 20MB,最多 10 个"
hint="支持 JPG、PNG、PDF,最多 10 个"
/>
</el-form-item>
</div>
@@ -519,6 +519,7 @@ const open = async (id?: number | string) => {
Object.assign(form.value, data)
form.value.businessLicenseImages =
(data as any)?.businessLicenseImages || (data as any)?.businessLicenseImg || undefined
delete (form.value as Record<string, unknown>).businessLicenseImg
form.value.businessPermitImages = (data as any)?.businessPermitImages || undefined
form.value.otherCertImages = (data as any)?.otherCertImages || undefined
if ((data as any)?.ownerUserId) {
@@ -536,6 +537,8 @@ const open = async (id?: number | string) => {
/** 构建提交 payload(字段名与后端 Schema 对齐) */
function buildSubmitPayload() {
const payload: Record<string, any> = { ...form.value }
// 详情接口会同时返回 businessLicenseImg(旧字段),避免覆盖新上传的 businessLicenseImages
delete payload.businessLicenseImg
return payload
}
@@ -82,7 +82,7 @@
v-model="attachmentUrls"
folder="crm/customer/follow"
:limit="15"
hint="支持图片、视频、PDF、Word、Excel等文件,单文件不超过 20MB,最多 15 个"
hint="支持图片、视频、PDF、Word、Excel等文件,最多 15 个"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
@@ -75,7 +75,7 @@
v-model="attachmentUrls"
folder="crm/opportunity/follow"
:limit="15"
hint="支持图片、视频、PDF、Word 等文件,单文件不超过 20MB,最多 15 个"
hint="支持图片、视频、PDF、Word 等文件,最多 15 个"
/>
</el-form-item>
</el-form>
+2 -32
View File
@@ -201,18 +201,6 @@
header-align="center"
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('unit')"
prop="unit"
label="单位"
min-width="80"
align="center"
header-align="center"
>
<template #default="{ row }">
{{ formatProductUnit(row.unit) }}
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('price')"
prop="price"
@@ -293,7 +281,7 @@
>
<div class="form-grid-3">
<el-form-item label="产品编号">
<el-input v-model="formData.productNo" placeholder="留空自动生成" />
<el-input v-model="formData.productNo" placeholder="留空自动生成(如 SXWL000001)" />
</el-form-item>
<el-form-item label="产品名称" prop="productName">
<el-input v-model="formData.productName" placeholder="请输入产品名称" />
@@ -367,7 +355,7 @@
:limit="5"
accept="image/*"
folder="crm/product"
hint="支持 JPG、PNG、WebP 等图片,单文件不超过 20MB,最多 5 张"
hint="支持 JPG、PNG、WebP 等图片,最多 5 张"
/>
</el-form-item>
<el-form-item label="产品描述" style="grid-column: 1 / -1">
@@ -548,11 +536,6 @@ function getHazardFlagTagType(value: unknown) {
return 'danger'
}
function formatProductUnit(unit: string | null | undefined) {
if (!unit) return '-'
return dictStore.getLabel('crm_product_unit', unit) || unit
}
function formatProductPrice(value: unknown) {
if (value === null || value === undefined || value === '') return '-'
const num = Number(value)
@@ -568,7 +551,6 @@ const listColumnDefs = [
{ prop: 'productNo', label: '产品编号' },
{ prop: 'categoryName', label: '产品分类' },
{ prop: 'model', label: '规格型号' },
{ prop: 'unit', label: '单位' },
{ prop: 'price', label: '标准价(元)' },
{ prop: 'cost', label: '成本价(元)', defaultVisible: false },
{ prop: 'status', label: '状态' }
@@ -648,22 +630,10 @@ const productViewSections = computed<ViewSection[]>(() => {
if (!d) return []
const fields: ViewField[] = [
{
label: '标准价(元)',
value: formatProductPrice(d.price),
money: true
},
{
label: '成本价(元)',
value: formatProductPrice(d.costPrice ?? d.cost),
money: true
},
crmField('库存数量', `${d.stock ?? 0} ${formatProductUnit(d.unit)}`),
crmDictField('状态', 'crm_product_status', d.status, dictStore),
crmField('产品分类', d.categoryName),
crmField('规格型号', d.model),
crmField('条码', d.barcode),
crmField('库存预警值', d.lowStock ?? 10),
crmField('销售次数', d.saleCount ?? 0),
crmField('税率', `${d.taxRate ?? 13}%`),
crmTagField('是否易制毒', formatHazardFlag(d.isToxic), getHazardFlagTagType(d.isToxic)),
@@ -203,47 +203,50 @@
<div class="quote-approval-flow-scroll">
<p v-if="approvalList.length === 0" class="view-empty-hint">暂无审批记录</p>
<el-timeline v-else>
<el-timeline-item
v-for="item in approvalList"
:key="item.id"
:timestamp="`第${item.step}步 · ${item.createTime || '—'}`"
placement="top"
:color="getApprovalTimelineColor(item.approvalResult)"
:hollow="item.approvalResult === 'pending'"
>
<div class="quote-approval-card">
<div class="quote-approval-card-head">
<div class="quote-approval-card-user">
<el-avatar :size="28">{{ item.approverName?.substring(0, 1) || '—' }}</el-avatar>
<span>{{ item.approverName || '—' }}</span>
<div v-else class="quote-approval-flow-track">
<template v-for="(item, index) in approvalList" :key="item.id">
<div class="quote-approval-flow-step">
<p class="quote-approval-step-meta">
第{{ item.step }}步 · {{ item.createTime || '—' }}
</p>
<div class="quote-approval-card">
<div class="quote-approval-card-head">
<div class="quote-approval-card-user">
<el-avatar :size="28">{{ item.approverName?.substring(0, 1) || '—' }}</el-avatar>
<span>{{ item.approverName || '—' }}</span>
</div>
<el-tag
v-if="item.approvalResult === 'approved'"
type="success"
size="small"
>
已通过
</el-tag>
<el-tag
v-else-if="item.approvalResult === 'rejected'"
type="danger"
size="small"
>
已拒绝
</el-tag>
<el-tag v-else type="info" size="small">待审批</el-tag>
</div>
<el-tag
v-if="item.approvalResult === 'approved'"
type="success"
size="small"
>
已通过
</el-tag>
<el-tag
v-else-if="item.approvalResult === 'rejected'"
type="danger"
size="small"
>
已拒绝
</el-tag>
<el-tag v-else type="info" size="small">待审批</el-tag>
<p v-if="item.approvalOpinion" class="quote-approval-opinion">
审批意见:{{ item.approvalOpinion }}
</p>
<p v-if="item.approvalTime" class="quote-approval-meta">
审批时间:{{ item.approvalTime }}
</p>
<p v-if="item.remark" class="quote-approval-meta">备注:{{ item.remark }}</p>
</div>
<p v-if="item.approvalOpinion" class="quote-approval-opinion">
审批意见:{{ item.approvalOpinion }}
</p>
<p v-if="item.approvalTime" class="quote-approval-meta">
审批时间:{{ item.approvalTime }}
</p>
<p v-if="item.remark" class="quote-approval-meta">备注:{{ item.remark }}</p>
</div>
</el-timeline-item>
</el-timeline>
<div
v-if="index < approvalList.length - 1"
class="quote-approval-flow-connector"
:class="{ 'quote-approval-flow-connector--done': item.approvalResult === 'approved' }"
/>
</template>
</div>
</div>
<div v-if="approvalList.length > 0" class="quote-approval-stats">
@@ -386,12 +389,6 @@ function getDaysRemaining(expireDate?: string) {
return `剩余${diff}天`
}
function getApprovalTimelineColor(result?: string) {
if (result === 'approved') return '#1ca851'
if (result === 'rejected') return '#ef4444'
return '#94a3b8'
}
/** 8 列表格区块:禁用长字段独占行,保证每行尽量排满 4 组 */
function viewQuadTableSection(title: string, fields: ViewField[]): ViewSection {
return {
@@ -622,12 +619,62 @@ defineExpose({ open, refresh })
}
.quote-approval-flow-scroll {
max-height: 420px;
overflow-y: auto;
padding-right: 4px;
overflow-x: auto;
overflow-y: hidden;
padding: 4px 4px 12px;
}
.quote-approval-flow-track {
display: flex;
align-items: stretch;
gap: 0;
min-width: min-content;
padding: 4px 0;
}
.quote-approval-flow-step {
display: flex;
flex-direction: column;
gap: 8px;
width: 240px;
flex: 0 0 auto;
}
.quote-approval-step-meta {
margin: 0;
font-size: 12px;
line-height: 1.4;
color: var(--mint-text-muted, #64748b);
}
.quote-approval-flow-connector {
width: 48px;
height: 2px;
margin-top: 52px;
flex-shrink: 0;
align-self: flex-start;
background: #e2e8f0;
position: relative;
}
.quote-approval-flow-connector--done {
background: linear-gradient(90deg, #1ca851, #49be78);
}
.quote-approval-flow-connector--done::after {
content: '';
position: absolute;
right: -3px;
top: -3px;
width: 8px;
height: 8px;
border-top: 2px solid #1ca851;
border-right: 2px solid #1ca851;
transform: rotate(45deg);
}
.quote-approval-card {
flex: 1;
padding: 12px 14px;
border: 1px solid var(--border, #d9e8e2);
border-radius: 8px;
@@ -5,14 +5,17 @@
:model="form"
:rules="rules"
label-position="right"
label-width="100px"
label-width="120px"
class="crm-dialog-form quote-form-body"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
<el-divider content-position="left">基本信息</el-divider>
<el-divider content-position="left">
<span class="quote-section-title">基本信息</span>
</el-divider>
<div class="form-grid-4">
<el-form-item class="quote-type-item form-col-span-4" label="报价类型" prop="quoteType" required>
<!-- 第一行:报价类型、报价单编号、报价主题、关联商机 -->
<el-form-item class="quote-type-item" label="报价类型" prop="quoteType" required>
<el-radio-group v-model="form.quoteType" @change="handleQuoteTypeChange">
<el-radio-button v-for="item in quoteTypeOptions" :key="item.value" :label="item.value">
{{ item.label }}
@@ -25,6 +28,27 @@
<el-form-item :label="basicLabels.quoteTitle" prop="quoteTitle">
<el-input v-model="form.quoteTitle" placeholder="可不填,系统将自动生成" maxlength="100" show-word-limit />
</el-form-item>
<el-form-item :label="basicLabels.opportunity" prop="opportunityId">
<el-input v-model="opportunityDisplayName" placeholder="请选择商机(可选)" readonly clearable>
<template #append>
<el-button text @click="handleSelectOpportunity">
<el-icon><Search /></el-icon>
</el-button>
</template>
</el-input>
</el-form-item>
<!-- 第二行:采购主体/报价主体、客户/供货商名称、联系人、负责人 -->
<el-form-item :label="basicLabels.supplier" prop="supplierId" required>
<el-select
v-model="form.supplierId"
:placeholder="`请选择${basicLabels.supplier}`"
clearable
style="width: 100%"
@change="handleSupplierChange"
>
<el-option v-for="supplier in supplierList" :key="supplier.id" :label="supplier.supplierName" :value="supplier.id" />
</el-select>
</el-form-item>
<el-form-item :label="basicLabels.customerName" prop="customerId" required>
<el-input v-model="form.customerName" placeholder="请选择客户" readonly>
<template #append>
@@ -43,15 +67,6 @@
</template>
</el-input>
</el-form-item>
<el-form-item :label="basicLabels.opportunity" prop="opportunityId">
<el-input v-model="opportunityDisplayName" placeholder="请选择商机(可选)" readonly clearable>
<template #append>
<el-button text @click="handleSelectOpportunity">
<el-icon><Search /></el-icon>
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item :label="basicLabels.ownerUser" prop="ownerUserId" required>
<el-select
v-model="form.ownerUserId"
@@ -64,6 +79,10 @@
<el-option v-for="user in userList" :key="user.userId" :label="user.nickName" :value="user.userId" />
</el-select>
</el-form-item>
<!-- 第三行:运费、询价/报价日期、有效期(天) -->
<el-form-item label="运费(元)" prop="freight">
<el-input-number v-model="form.freight" :min="0" :step="0.01" style="width: 100%" />
</el-form-item>
<el-form-item :label="basicLabels.quoteDate" prop="quoteDate" required>
<el-date-picker
v-model="form.quoteDate"
@@ -76,25 +95,11 @@
<el-form-item :label="basicLabels.validityDays" prop="validityDays" required>
<el-input-number v-model="form.validityDays" :min="1" :max="365" style="width: 100%" />
</el-form-item>
<el-form-item :label="basicLabels.supplier" prop="supplierId" required>
<el-select
v-model="form.supplierId"
:placeholder="`请选择${basicLabels.supplier}`"
clearable
style="width: 100%"
@change="handleSupplierChange"
>
<el-option v-for="supplier in supplierList" :key="supplier.id" :label="supplier.supplierName" :value="supplier.id" />
</el-select>
</el-form-item>
<el-form-item label="运费(元)" prop="freight">
<el-input-number v-model="form.freight" :min="0" :step="0.01" style="width: 100%" />
</el-form-item>
</div>
<!-- 产品明细 -->
<el-divider content-position="left">
<span style="font-size: 16px; font-weight: bold; color: #67c23a">
<span class="quote-section-title">
<el-icon><GoodsFilled /></el-icon> 产品明细
</span>
</el-divider>
@@ -236,7 +241,7 @@
<!-- 报价条款 -->
<el-divider content-position="left">
<span style="font-size: 16px; font-weight: bold; color: #67c23a">
<span class="quote-section-title">
<el-icon><List /></el-icon> 报价条款
</span>
</el-divider>
@@ -891,8 +896,8 @@ const handleCustomerConfirm = async (customer: CustomerVO) => {
if (customer) {
form.value.customerId = customer.id;
form.value.customerName = customer.customerName;
// 保存客户简称
customerShortName.value = customer.shortName || customer.customerName || '';
// 保存客户简称(报价主题依赖简称,不可回退为客户全称)
customerShortName.value = customer.shortName?.trim() || '';
// 清空联系人和商机
form.value.contactId = undefined;
form.value.contactName = '';
@@ -963,6 +968,39 @@ const handleOwnerChange = (userId: string | number | undefined) => {
form.value.ownerUserName = user?.nickName || ''
}
/** 当前登录用户 ID(后端 current-user 返回 id,兼容历史 userId 字段) */
const resolveCurrentUserId = (): string => {
const info = userStore.info || ({} as Api.Auth.UserInfo & Record<string, unknown>)
const fromInfo = String(info.id || info.userId || '')
if (fromInfo) return fromInfo
const token = userStore.accessToken
if (!token || token.split('.').length < 2) return ''
try {
const payload = token.split('.')[1]
const b64 = payload.replace(/-/g, '+').replace(/_/g, '/')
const pad = '='.repeat((4 - (b64.length % 4)) % 4)
const obj = JSON.parse(atob(b64 + pad))
return String(obj?.sub || '')
} catch {
return ''
}
}
/** 新增时默认负责人为当前登录人 */
const applyDefaultOwnerUser = () => {
const currentUserId = resolveCurrentUserId()
if (!currentUserId) return
const matched = userList.value.find((item) => String(item.userId) === currentUserId)
if (matched) {
form.value.ownerUserId = matched.userId as QuoteForm['ownerUserId']
form.value.ownerUserName = matched.nickName || ''
return
}
form.value.ownerUserId = currentUserId as QuoteForm['ownerUserId']
const info = userStore.info as Record<string, string | undefined>
form.value.ownerUserName = info?.realName || info?.userName || info?.username || ''
}
const formatApproverOptionLabel = (user: { nickName?: string; userName?: string }) => {
const nick = (user.nickName || '').trim()
const account = (user.userName || '').trim()
@@ -1610,6 +1648,7 @@ const initNew = async () => {
form.value.quoteDate = getTodayDate();
await dictStore.loadAllDict();
await Promise.all([loadSupplierList(), loadUserList()]);
applyDefaultOwnerUser();
};
const initEdit = async (id: string | number) => {
@@ -1674,11 +1713,27 @@ onMounted(async () => {
getQuoteInfoForCopy(copyId);
} else {
form.value.quoteDate = getTodayDate();
applyDefaultOwnerUser();
}
});
</script>
<style scoped lang="scss">
.quote-form-body {
:deep(.el-form-item__label) {
white-space: nowrap;
}
}
.quote-section-title {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 16px;
font-weight: 400;
color: var(--primary-dark, #059669);
}
.box-card {
margin-top: 10px;
}
@@ -1708,17 +1763,19 @@ onMounted(async () => {
.quote-type-item :deep(.el-form-item__content) {
display: flex;
align-items: center;
justify-content: center;
justify-content: flex-start;
}
.quote-type-item :deep(.el-radio-group) {
display: inline-flex;
justify-content: center;
justify-content: flex-start;
flex-wrap: wrap;
gap: 0;
}
.quote-type-item :deep(.el-radio-button__inner) {
padding: 14px 26px;
font-size: 16px;
padding: 8px 16px;
font-size: 14px;
}
.quote-type-wrapper {
@@ -2027,11 +2084,6 @@ onMounted(async () => {
gap: 8px 12px;
}
.quote-type-item :deep(.el-radio-group) {
display: inline-flex;
justify-content: center;
}
.approval-dialog-actions {
display: flex;
flex-wrap: wrap;
+42 -20
View File
@@ -438,8 +438,7 @@
ref="submitFormRef"
:model="submitForm"
:rules="submitRules"
label-position="right"
label-width="100px"
label-position="top"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
@@ -1772,8 +1771,34 @@ const DEFAULT_QUOTE_TABLE_REMARK_ROW = `<tr>
<td colspan="10" style="padding: 8px; border: 1px solid #000; background-color: #f9f9f9; text-align: center; font-size: 13px;"> </td>
</tr>`;
/** 生成可替换的产品明细区块(产品行 + 合计行,每次从数据库数据渲染) */
const buildQuoteDynamicProductSection = (quote: QuoteVO): string => {
const QUOTE_TABLE_REMARK_ROW_PATTERN =
/<!--\s*备注行\s*-->\s*(<tr[\s\S]*?colspan=["']10["'][\s\S]*?<\/tr>)/i;
/** 从已保存模板中提取备注行(兼容备注行在合计上方或下方的旧结构) */
const extractQuoteTableRemarkRow = (html: string): string => {
const withComment = html.match(QUOTE_TABLE_REMARK_ROW_PATTERN);
if (withComment?.[1]) return withComment[1];
const colspan10Rows = [...html.matchAll(/<tr[\s\S]*?colspan=["']10["'][\s\S]*?<\/tr>/gi)].map((m) => m[0]);
for (let i = colspan10Rows.length - 1; i >= 0; i--) {
const row = colspan10Rows[i];
if (!row.includes('合计') && !row.includes('暂无产品信息')) {
return row;
}
}
return DEFAULT_QUOTE_TABLE_REMARK_ROW;
};
/** 移除动态区块外重复的备注行(旧模板中备注行在合计下方时) */
const removeOrphanRemarkRowAfterDynamicSection = (html: string): string => {
return html.replace(
/(<!--\s*quote-dynamic-products-end\s*-->)\s*(?:<!--\s*备注行\s*-->\s*)?<tr[\s\S]*?colspan=["']10["'][\s\S]*?<\/tr>/i,
'$1'
);
};
/** 生成可替换的产品明细区块(产品行 + 备注行 + 合计行,每次从数据库数据渲染) */
const buildQuoteDynamicProductSection = (quote: QuoteVO, remarkRow: string = DEFAULT_QUOTE_TABLE_REMARK_ROW): string => {
const productRows =
quote.productList
?.map(
@@ -1805,6 +1830,8 @@ const buildQuoteDynamicProductSection = (quote: QuoteVO): string => {
return `<!-- quote-dynamic-products-start -->
${productRows}
<!-- 备注行 -->
${remarkRow}
${totalRow}
<!-- quote-dynamic-products-end -->`;
};
@@ -1815,24 +1842,21 @@ const applyQuoteProductDataToTemplate = (
quote: QuoteVO,
supplierInfo?: SupplierInfoVO
): string => {
const dynamicSection = buildQuoteDynamicProductSection(quote);
const remarkRow = extractQuoteTableRemarkRow(savedTemplate);
const dynamicSection = buildQuoteDynamicProductSection(quote, remarkRow);
let result = savedTemplate;
if (TEMPLATE_DYNAMIC_PRODUCTS_PATTERN.test(savedTemplate)) {
return savedTemplate.replace(TEMPLATE_DYNAMIC_PRODUCTS_PATTERN, dynamicSection);
}
if (!/<tbody[\s\S]*?<\/tbody>/i.test(savedTemplate)) {
result = savedTemplate.replace(TEMPLATE_DYNAMIC_PRODUCTS_PATTERN, dynamicSection);
} else if (!/<tbody[\s\S]*?<\/tbody>/i.test(savedTemplate)) {
return generateQuoteTemplate(quote, supplierInfo);
}
return savedTemplate.replace(/<tbody>([\s\S]*?)<\/tbody>/i, (_match, body: string) => {
const remarkRow =
body.match(/<tr>[\s\S]*?colspan=["']10["'][\s\S]*?<\/tr>/i)?.[0] || DEFAULT_QUOTE_TABLE_REMARK_ROW;
return `<tbody>
} else {
result = savedTemplate.replace(/<tbody>([\s\S]*?)<\/tbody>/i, () => {
return `<tbody>
${dynamicSection}
<!-- 备注行 -->
${remarkRow}
</tbody>`;
});
});
}
return removeOrphanRemarkRowAfterDynamicSection(result);
};
/** 生成报价单HTML模版 */
@@ -1939,8 +1963,6 @@ const generateQuoteTemplate = (quote: QuoteVO, supplierInfo?: SupplierInfoVO): s
</thead>
<tbody>
${dynamicProductSection}
<!-- 备注行 -->
${DEFAULT_QUOTE_TABLE_REMARK_ROW}
</tbody>
</table>
+3 -3
View File
@@ -264,7 +264,7 @@
:limit="5"
accept="image/*,.pdf"
folder="pl/carrier/license"
hint="支持 JPG、PNG、PDF,单文件不超过 20MB,最多 5 个"
hint="支持 JPG、PNG、PDF,最多 5 个"
@update:model-value="handleCertFieldChange('businessLicenseUrls')"
/>
</el-form-item>
@@ -275,7 +275,7 @@
:limit="5"
accept="image/*,.pdf"
folder="pl/carrier/permit"
hint="支持 JPG、PNG、PDF,单文件不超过 20MB,最多 5 个"
hint="支持 JPG、PNG、PDF,最多 5 个"
@update:model-value="handleCertFieldChange('roadTransportLicenseUrls')"
/>
</el-form-item>
@@ -286,7 +286,7 @@
:limit="5"
accept="image/*,.pdf"
folder="pl/carrier/agreement"
hint="支持 JPG、PNG、PDF,单文件不超过 20MB,最多 5 个"
hint="支持 JPG、PNG、PDF,最多 5 个"
@update:model-value="handleCertFieldChange('transportAgreementUrls')"
/>
</el-form-item>
@@ -6,7 +6,7 @@ APP_NAME=山峡化工一体化门户
DEBUG=false
DEFAULT_TENANT_NO=shanxia2.0
WEB_AUTH_DEFAULT_ROLE=super_admin
WEB_AUTH_DEFAULT_ROLE=admin
# === 与本地一致的远程中间件(按实际 .env 填写)===
DATABASE_URL=mysql+pymysql://USER:PASSWORD@47.99.164.66:3306/shanxia2.0