This commit is contained in:
zlwu committed 2026-06-29 11:15:39 +08:00
1 parent 493f8d6756
commit d84f4b944c
406 files changed
+58954 -10376

No files matched your search

+101
View File
@@ -0,0 +1,101 @@
# shanxia2.0 Backend 生产部署说明
## 1. 上传到服务器
将 `backend/` 目录上传到:
```
/iflytek/shanxia2.0/backend
```
**必须排除**(否则 Linux 上会报 `bad interpreter: No such file or directory`):
- `venv/`(**禁止上传本机 venv**,必须在服务器上用 `./start-prod.sh install` 重建)
- `logs/`、`__pycache__/`、`*.pyc`
- `graphify-out/`
- `uploads/`(若生产已有附件数据则保留服务器端目录)
- `.env`(开发配置,生产用 `.env.production`)
本地打包示例:
```bash
cd /path/to/shanxia2.0
tar czf shanxia2.0-backend.tar.gz \
--exclude='backend/venv' \
--exclude='backend/logs' \
--exclude='backend/__pycache__' \
--exclude='backend/graphify-out' \
--exclude='backend/.env' \
backend/
```
## 2. 服务器首次启动
```bash
cd /iflytek/shanxia2.0/backend
chmod +x start-prod.sh
# 首次:安装 Python 依赖(自动创建 venv)
./start-prod.sh install
# 启动(默认 127.0.0.1:9111,读取 .env.production)
./start-prod.sh start
```
## 3. 日常运维
```bash
./start-prod.sh status # 查看状态
./start-prod.sh logs # 最近日志
./start-prod.sh restart # 发版后重启
./start-prod.sh stop # 停止
```
健康检查:
```bash
curl -s http://127.0.0.1:9111/health
# {"status":"healthy"}
```
## 4. 配置文件
| 文件 | 用途 |
|---|---|
| `.env.production` | 生产环境变量(DB/Redis/MinIO/JWT/RSA 等) |
| `start-prod.sh` | 启动脚本(默认端口 **9111**) |
默认监听 `127.0.0.1:9111`,由 Nginx `:9911` 反代 `/api/*`,无需对公网开放 9111。
## 5. 与 Nginx 配合
Nginx 配置见仓库 `生产环境部署/nginx-shanxia2.0.conf`:
- 前端:`http://121.43.228.102:9911`
- API:`/api/` → `http://127.0.0.1:9111`
## 6. 常见问题
**Q: `bad interpreter: .../Users/.../venv/bin/python3: No such file or directory`**
A: 误把本机 Mac 的 `venv/` 上传到了 Linux。在服务器执行:
```bash
cd /iflytek/shanxia2.0/backend
rm -rf venv
./start-prod.sh install
./start-prod.sh start
```
新版 `start-prod.sh` 也会自动检测并重建无效 venv。
**Q: 启动失败,日志报 ModuleNotFoundError**
A: 执行 `./start-prod.sh install` 重新安装依赖。
**Q: 登录失败 / 公钥变化**
A: 确认 `.env.production` 中 `LOGIN_RSA_PRIVATE_KEY_PEM` 已配置且未被修改。
**Q: 数据库连接失败**
A: 检查服务器到 `47.99.164.66:3306` 网络与白名单。
**Q: 需要改端口**
A: `PORT=9111 ./start-prod.sh restart` 或修改脚本默认值。
+1 -1
View File
@@ -1 +1 @@
4800
17486
+9 -9
View File
@@ -211,7 +211,7 @@ async def delete_tenant(
db: Session = Depends(get_db),
):
try:
tenant_svc.soft_delete_tenant(db, tenant_id)
tenant_svc.soft_delete_tenant(db, tenant_id, user_info=_user)
return success_response(None, message="删除成功")
except ValueError as e:
return error_response(400, str(e))
@@ -401,7 +401,7 @@ async def delete_user(
db: Session = Depends(get_db),
):
try:
user_svc.soft_delete_user(db, user_id)
user_svc.soft_delete_user(db, user_id, user_info=_user)
return success_response(None, message="删除成功")
except ValueError as e:
return error_response(400, str(e))
@@ -513,7 +513,7 @@ async def delete_dict_type(
db: Session = Depends(get_db),
):
try:
dict_type_svc.soft_delete_dict_type(db, dict_type_id)
dict_type_svc.soft_delete_dict_type(db, dict_type_id, user_info=_user)
return success_response(None, message="删除成功")
except ValueError as e:
return error_response(400, str(e))
@@ -617,7 +617,7 @@ async def delete_dict_data(
db: Session = Depends(get_db),
):
try:
dict_data_svc.soft_delete_dict_data(db, data_id)
dict_data_svc.soft_delete_dict_data(db, data_id, user_info=_user)
return success_response(None, message="删除成功")
except ValueError as e:
return error_response(400, str(e))
@@ -760,7 +760,7 @@ async def delete_dept(
db: Session = Depends(get_db),
):
try:
dept_svc.soft_delete_dept(db, dept_id)
dept_svc.soft_delete_dept(db, dept_id, user_info=_user)
return success_response(None, message="删除成功")
except ValueError as e:
return error_response(400, str(e))
@@ -879,7 +879,7 @@ async def delete_site(
db: Session = Depends(get_db),
):
try:
site_svc.soft_delete_site(db, site_id)
site_svc.soft_delete_site(db, site_id, user_info=_user)
return success_response(None, message="删除成功")
except ValueError as e:
return error_response(400, str(e))
@@ -978,7 +978,7 @@ async def delete_post(
db: Session = Depends(get_db),
):
try:
post_svc.soft_delete_post(db, post_id)
post_svc.soft_delete_post(db, post_id, user_info=_user)
return success_response(None, message="删除成功")
except ValueError as e:
return error_response(400, str(e))
@@ -1138,7 +1138,7 @@ async def delete_menu(
db: Session = Depends(get_db),
):
try:
menu_svc.soft_delete_menu(db, menu_id)
menu_svc.soft_delete_menu(db, menu_id, user_info=_user)
return success_response(None, message="删除成功")
except ValueError as e:
return error_response(400, str(e))
@@ -1248,7 +1248,7 @@ async def delete_role_management(
db: Session = Depends(get_db),
):
try:
role_svc.soft_delete_role(db, role_id)
role_svc.soft_delete_role(db, role_id, user_info=_user)
return success_response(None, message="删除成功")
except ValueError as e:
return error_response(400, str(e))
+10
View File
@@ -12,6 +12,7 @@ from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.menu_guard import require_admin_or_super_admin
from app.core.oper_log_decorator import oper_log
from app.core.response import error_response, success_response
from app.core.tenant_context import get_tenant_no
from app.crud.crm.data_scope_helpers import (
@@ -414,6 +415,7 @@ async def customer_detail(
@router.post("/customer/add")
@oper_log(title="CRM客户管理", business_type=1, summary="新增客户【{customerName}】")
async def customer_create(
body: CustomerCreateBody,
_user: dict = Depends(require_admin_or_super_admin),
@@ -443,6 +445,7 @@ async def customer_create(
@router.post("/customer/update")
@oper_log(title="CRM客户管理", business_type=2, summary="修改客户【{customerName}】")
async def customer_update(
body: CustomerUpdateBody,
_user: dict = Depends(require_admin_or_super_admin),
@@ -469,6 +472,7 @@ async def customer_update(
@router.post("/customer/delete")
@oper_log(title="CRM客户管理", business_type=3, business_id_field="ids", summary="删除客户")
async def customer_delete(
body: CustomerDeleteBody,
_user: dict = Depends(require_admin_or_super_admin),
@@ -926,6 +930,7 @@ async def product_import_template(
@router.post("/customer/transfer")
@oper_log(title="CRM客户管理", business_type=2, business_id_field="customerId", summary="转移客户")
async def customer_transfer(
body: dict,
_user: dict = Depends(require_admin_or_super_admin),
@@ -951,6 +956,7 @@ async def customer_transfer(
@router.post("/customer/intoPool")
@oper_log(title="CRM客户管理", business_type=2, business_id_field="customerId", summary="客户放入公海")
async def customer_into_pool(
body: dict,
_user: dict = Depends(require_admin_or_super_admin),
@@ -975,6 +981,7 @@ async def customer_into_pool(
@router.post("/customer/deal/{customer_id}")
@oper_log(title="CRM客户管理", business_type=2, path_param="customer_id", summary="客户成交")
async def customer_deal(
customer_id: str,
body: dict = None,
@@ -1026,6 +1033,7 @@ async def customer_check_duplicate(
@router.post("/customer/export")
@oper_log(title="CRM客户管理", business_type=5, summary="导出客户列表")
async def customer_export(
body: CustomerListBody = None,
_user: dict = Depends(require_admin_or_super_admin),
@@ -1350,6 +1358,7 @@ async def pool_customer_list(
@router.post("/customer/pool/assign")
@oper_log(title="CRM客户管理", business_type=2, business_id_field="customerIds", summary="公海分配客户")
async def pool_customer_assign(
body: PoolAssignBody,
_user: dict = Depends(require_admin_or_super_admin),
@@ -1368,6 +1377,7 @@ async def pool_customer_assign(
@router.post("/customer/claim")
@oper_log(title="CRM客户管理", business_type=2, business_id_field="customerId", summary="认领公海客户")
async def pool_customer_claim(
body: PoolClaimBody,
_user: dict = Depends(require_admin_or_super_admin),
+6
View File
@@ -9,6 +9,7 @@ from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.menu_guard import require_admin_or_super_admin
from app.core.oper_log_decorator import oper_log
from app.core.response import error_response, success_response
from app.core.tenant_context import get_tenant_no
from app.crud.crm.data_scope_helpers import crm_user_info
@@ -156,6 +157,7 @@ def register_contract_routes(router):
return error_response(500, str(e))
@router.post("/contract")
@oper_log(title="CRM合同管理", business_type=1, summary="新增合同【{contractName}】")
async def contract_add(
body: ContractCreateBody,
_user: dict = Depends(require_admin_or_super_admin),
@@ -180,6 +182,7 @@ def register_contract_routes(router):
return error_response(500, str(e))
@router.put("/contract")
@oper_log(title="CRM合同管理", business_type=2, summary="修改合同【{contractName}】")
async def contract_edit(
body: ContractUpdateBody,
_user: dict = Depends(require_admin_or_super_admin),
@@ -208,6 +211,7 @@ def register_contract_routes(router):
return error_response(500, str(e))
@router.delete("/contract/{ids}")
@oper_log(title="CRM合同管理", business_type=3, path_param="ids", summary="删除合同")
async def contract_remove(
ids: str,
_user: dict = Depends(require_admin_or_super_admin),
@@ -337,6 +341,7 @@ def register_contract_routes(router):
return error_response(500, str(e))
@router.put("/contract/invalid")
@oper_log(title="CRM合同管理", business_type=2, summary="作废合同")
async def contract_invalid(
body: ContractInvalidBody,
_user: dict = Depends(require_admin_or_super_admin),
@@ -441,6 +446,7 @@ def register_contract_routes(router):
return error_response(500, str(e))
@router.post("/contract/export")
@oper_log(title="CRM合同管理", business_type=5, summary="导出合同列表")
async def contract_export(
body: ContractExportBody = None,
_user: dict = Depends(require_admin_or_super_admin),
+2
View File
@@ -10,6 +10,7 @@ from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.db_auth import DbUserContext, get_db_current_user, get_db_current_user_optional
from app.core.delete_guard import assert_row_deletable_by_creator, user_info_from_context
from app.core.tenant_context import get_tenant_no
from app.services.db_data_scope import apply_db_scope, assert_db_row_in_scope
from app.services.db.inventory_id import next_inbound_outbound_id
@@ -426,6 +427,7 @@ def delete_inventory(
) -> None:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
row = _get_inventory(db, tenant_no, id, user)
assert_row_deletable_by_creator(row, user_info_from_context(user))
row.del_flag = 1
row.update_by = user.id
db.commit()
+2
View File
@@ -9,6 +9,7 @@ from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.db_auth import DbUserContext, get_db_current_user
from app.core.delete_guard import assert_row_deletable_by_creator, user_info_from_context
from app.core.tenant_context import get_tenant_no
from app.core.utils import generate_uuid
from app.services.db_data_scope import apply_db_scope, assert_db_row_in_scope
@@ -295,6 +296,7 @@ def delete_plan(
) -> None:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
plan = _get_plan(db, tenant_no, id, user)
assert_row_deletable_by_creator(plan, user_info_from_context(user))
plan.del_flag = 1
plan.update_by = user.id
db.commit()
+2
View File
@@ -9,6 +9,7 @@ from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.db_auth import DbUserContext, get_db_current_user
from app.core.delete_guard import assert_row_deletable_by_creator, user_info_from_context
from app.core.tenant_context import get_tenant_no
from app.core.utils import generate_uuid
from app.services.db_data_scope import apply_db_scope, assert_db_row_in_scope
@@ -360,6 +361,7 @@ def delete_weighing(
) -> None:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
row = _get_weighing(db, tenant_no, id, user)
assert_row_deletable_by_creator(row, user_info_from_context(user))
row.is_voided = 1
row.update_by = user.id
db.commit()
+1 -1
View File
@@ -182,7 +182,7 @@ async def delete_leave(
return error_response(404, "请假记录不存在")
if leave["status"] == "pending":
return error_response(400, "审批中的请假单不能删除")
leave_crud.soft_delete_leave(db, leave_id, user_id=user_id)
leave_crud.soft_delete_leave(db, leave_id, user_id=user_id, user_info=_user)
db.commit()
return success_response(None)
+2
View File
@@ -41,6 +41,8 @@ async def operlog_list(
page_size=page_size,
title=body.title if body else None,
business_type=body.business_type if body else None,
business_id=body.business_id if body else None,
business_title=body.business_title if body else None,
oper_name=body.oper_name if body else None,
status=body.status if body else None,
user_info=info,
+13 -1
View File
@@ -2,7 +2,7 @@
from fastapi import APIRouter
from app.api.pl import plan, transport, freight, inventory
from app.api.pl import base_data
from app.api.pl import base_data, vehicle_recharge, vehicle_maintenance, vehicle_urea
pl_router = APIRouter(prefix="/transport", tags=["运输计划管理"])
@@ -20,3 +20,15 @@ pl_router.include_router(freight.router)
# 盘存统计
pl_router.include_router(inventory.router)
# ETC/加油充值
pl_router.include_router(vehicle_recharge.apply_router)
pl_router.include_router(vehicle_recharge.record_router)
# 车辆维保
pl_router.include_router(vehicle_maintenance.apply_router)
pl_router.include_router(vehicle_maintenance.process_router)
# 车辆尿素
pl_router.include_router(vehicle_urea.apply_router)
pl_router.include_router(vehicle_urea.disburse_router)
+122 -5
View File
@@ -1,4 +1,5 @@
"""运输计划管理 - 基础数据 API(承运商、车辆、人员)"""
from datetime import datetime
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
@@ -7,6 +8,7 @@ from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.db_auth import DbUserContext, get_db_current_user
from app.core.delete_guard import assert_row_deletable_by_creator, user_info_from_context
from app.services.pl_data_scope import apply_pl_scope, assert_pl_row_in_scope
from app.core.tenant_context import get_tenant_no
from app.core.utils import generate_uuid
@@ -18,9 +20,11 @@ from app.models.pl_models import (
PlVehiclePersonnel,
)
from app.schemas.pl import (
CarrierAuditReject,
CarrierCreate,
CarrierRead,
CarrierUpdate,
carrier_to_read,
DriverCreate,
DriverRead,
DriverUpdate,
@@ -40,6 +44,10 @@ from app.services.pl_cert_service import (
nearest_expire_date,
save_owner_certs,
)
from app.services.pl_carrier_service import (
carrier_eligible_for_transport,
carrier_qualification_complete,
)
from app.services.pl_personnel_service import (
load_vehicle_personnel_rows,
load_vehicle_personnel_summaries,
@@ -58,6 +66,7 @@ router = APIRouter(prefix="", tags=["运输计划-基础数据"])
def list_carriers(
q: Optional[str] = Query(default=None, description="模糊搜索"),
status: Optional[str] = Query(default=None, description="状态"),
audit_status: Optional[str] = Query(default=None, alias="auditStatus", description="审核状态"),
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> list[CarrierRead]:
@@ -70,6 +79,9 @@ def list_carriers(
if status:
query = query.filter(PlCarrier.status == status)
if audit_status:
query = query.filter(PlCarrier.audit_status == audit_status)
if q:
query = query.filter(
or_(
@@ -84,7 +96,7 @@ def list_carriers(
case((PlCarrier.order_num.is_(None), 1), else_=0).asc(),
PlCarrier.create_time.desc(),
)
return query.all()
return [carrier_to_read(row) for row in query.all()]
@router.post("/carrier", response_model=CarrierRead, status_code=status.HTTP_201_CREATED)
@@ -102,6 +114,11 @@ def create_carrier(
contact_name=payload.contact_name,
contact_phone=payload.contact_phone,
status=payload.status,
business_license_urls=payload.business_license_urls.strip(),
road_transport_license_urls=payload.road_transport_license_urls.strip(),
transport_agreement_urls=payload.transport_agreement_urls.strip(),
transport_agreement_expire_date=payload.transport_agreement_expire_date,
audit_status="pending",
remark=payload.remark,
create_by=user.id,
create_dept_id=user.dept_id,
@@ -109,7 +126,7 @@ def create_carrier(
db.add(carrier)
db.commit()
db.refresh(carrier)
return carrier
return carrier_to_read(carrier)
@router.get("/carrier/{id}", response_model=CarrierRead)
@@ -120,7 +137,7 @@ def get_carrier(
) -> CarrierRead:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
carrier = _get_carrier(db, tenant_no, id, user)
return carrier
return carrier_to_read(carrier)
@router.put("/carrier/{id}", response_model=CarrierRead)
@@ -133,13 +150,28 @@ def update_carrier(
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
carrier = _get_carrier(db, tenant_no, id, user)
for key, value in payload.model_dump(exclude_unset=True).items():
payload_data = payload.model_dump(exclude_unset=True)
qual_keys = (
"business_license_urls",
"road_transport_license_urls",
"transport_agreement_urls",
"transport_agreement_expire_date",
)
qual_changed = any(key in payload_data for key in qual_keys)
for key, value in payload_data.items():
setattr(carrier, key, value)
if qual_changed and carrier.audit_status == "approved":
carrier.audit_status = "pending"
carrier.audit_by = None
carrier.audit_time = None
carrier.audit_remark = None
carrier.update_by = user.id
db.commit()
db.refresh(carrier)
return carrier
return carrier_to_read(carrier)
@router.delete("/carrier/{id}", status_code=status.HTTP_204_NO_CONTENT)
@@ -150,11 +182,59 @@ def delete_carrier(
) -> None:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
carrier = _get_carrier(db, tenant_no, id, user)
assert_row_deletable_by_creator(carrier, user_info_from_context(user))
carrier.del_flag = 1
carrier.update_by = user.id
db.commit()
@router.post("/carrier/{id}/approve", response_model=CarrierRead)
def approve_carrier(
id: str,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> CarrierRead:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
carrier = _get_carrier(db, tenant_no, id, user)
if carrier.audit_status == "approved":
raise HTTPException(status_code=400, detail="承运商已审核通过")
if not carrier_qualification_complete(carrier):
raise HTTPException(status_code=400, detail="承运商资质附件不完整,无法审核通过")
carrier.audit_status = "approved"
carrier.audit_by = user.id
carrier.audit_time = datetime.now()
carrier.audit_remark = None
carrier.update_by = user.id
db.commit()
db.refresh(carrier)
return carrier_to_read(carrier)
@router.post("/carrier/{id}/reject", response_model=CarrierRead)
def reject_carrier(
id: str,
payload: CarrierAuditReject,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> CarrierRead:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
carrier = _get_carrier(db, tenant_no, id, user)
if carrier.audit_status == "approved":
raise HTTPException(status_code=400, detail="承运商已审核通过,无法驳回")
carrier.audit_status = "rejected"
carrier.audit_by = user.id
carrier.audit_time = datetime.now()
carrier.audit_remark = (payload.remark or "").strip() or None
carrier.update_by = user.id
db.commit()
db.refresh(carrier)
return carrier_to_read(carrier)
def _get_carrier(db: Session, tenant_no: str, carrier_id: str, user: DbUserContext) -> PlCarrier:
carrier = (
db.query(PlCarrier)
@@ -212,6 +292,11 @@ def list_vehicles(
vehicle_type: Optional[str] = Query(default=None, alias="vehicleType", description="车辆类型"),
carrier_id: Optional[str] = Query(default=None, alias="carrierId", description="承运商ID"),
vehicle_status: Optional[str] = Query(default=None, alias="vehicleStatus", description="车辆状态"),
exclude_ineligible_carrier: Optional[bool] = Query(
default=False,
alias="excludeIneligibleCarrier",
description="排除资质不全或协议过期的承运商车辆",
),
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> list[VehicleRead]:
@@ -240,6 +325,34 @@ def list_vehicles(
PlVehicle.create_time.desc(),
)
vehicles = query.all()
if exclude_ineligible_carrier:
carrier_ids = {
v.carrier_id for v in vehicles if v.vehicle_type == "carrier" and v.carrier_id
}
carrier_map: dict[str, PlCarrier] = {}
if carrier_ids:
carriers = (
db.query(PlCarrier)
.filter(
PlCarrier.id.in_(carrier_ids),
PlCarrier.tenant_no == tenant_no,
PlCarrier.del_flag == 0,
)
.all()
)
carrier_map = {c.id: c for c in carriers}
vehicles = [
v
for v in vehicles
if v.vehicle_type != "carrier"
or not v.carrier_id
or (
v.carrier_id in carrier_map
and carrier_eligible_for_transport(carrier_map[v.carrier_id])
)
]
summaries = load_vehicle_personnel_summaries(
db, tenant_no, [v.id for v in vehicles]
)
@@ -387,6 +500,7 @@ def delete_vehicle(
) -> None:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
vehicle = _get_vehicle(db, tenant_no, id, user)
assert_row_deletable_by_creator(vehicle, user_info_from_context(user))
vehicle.del_flag = 1
vehicle.update_by = user.id
db.commit()
@@ -568,6 +682,7 @@ def delete_personnel(
) -> None:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
personnel = _get_personnel(db, tenant_no, id, user)
assert_row_deletable_by_creator(personnel, user_info_from_context(user))
personnel.del_flag = 1
personnel.update_by = user.id
db.commit()
@@ -710,6 +825,7 @@ def delete_driver(
) -> None:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
personnel = _get_personnel(db, tenant_no, id, user)
assert_row_deletable_by_creator(personnel, user_info_from_context(user))
personnel.del_flag = 1
personnel.update_by = user.id
db.commit()
@@ -783,5 +899,6 @@ def unbind_vehicle_driver(
assert_pl_row_in_scope(db, vd, PlVehiclePersonnel, user, tenant_no)
else:
assert_pl_row_in_scope(db, vd, PlVehicleDriver, user, tenant_no)
assert_row_deletable_by_creator(vd, user_info_from_context(user))
vd.del_flag = 1
db.commit()
+16
View File
@@ -11,6 +11,7 @@ from app.core.tenant_context import get_tenant_no
from app.models.pl_models import PlFreightSettlement
from app.schemas.pl.freight import (
FreightSettlementApprovalActionBody,
FreightSettlementByTransportRead,
FreightSettlementCreate,
FreightSettlementDisburseBody,
FreightSettlementListResponse,
@@ -100,6 +101,21 @@ def list_settlements(
return FreightSettlementListResponse(total=total, list=items)
@router.get("/by-transport/{transport_id}", response_model=Optional[FreightSettlementByTransportRead])
def get_settlement_by_transport(
transport_id: str,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> Optional[FreightSettlementByTransportRead]:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
detail = freight_svc.get_settlement_by_transport_id(
db, tenant_no=tenant_no, transport_id=transport_id, user=user
)
if not detail:
return None
return FreightSettlementByTransportRead.model_validate(detail)
@router.post("/", response_model=FreightSettlementRead, status_code=status.HTTP_201_CREATED)
def create_settlement(
payload: FreightSettlementCreate,
+2
View File
@@ -9,6 +9,7 @@ from sqlalchemy.orm import Session
from app.core.config import settings
from app.core.database import get_db
from app.core.db_auth import DbUserContext, get_db_current_user
from app.core.delete_guard import assert_row_deletable_by_creator, user_info_from_context
from app.services.pl_data_scope import apply_pl_scope, assert_pl_row_in_scope
from app.core.tenant_context import get_tenant_no
from app.core.utils import generate_uuid
@@ -970,6 +971,7 @@ def delete_plan(
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
plan = _get_plan(db, tenant_no, id, user)
assert_row_deletable_by_creator(plan, user_info_from_context(user))
plan.del_flag = 1
plan.update_by = user.id
+58 -11
View File
@@ -52,6 +52,7 @@ from app.services.pl_transport_site_service import (
sync_site_links_for_transport,
)
from app.services.pl_plan_service import is_ton_product_unit
from app.services.pl_carrier_service import assert_carrier_eligible_for_transport
router = APIRouter(prefix="/order", tags=["运输计划-运输管理"])
@@ -166,6 +167,41 @@ def _get_vehicle_info(db: Session, vehicle_id: str, tenant_no: Optional[str] = N
return info
def _assert_vehicle_carrier_eligible(
db: Session,
tenant_no: str,
vehicle_id: str,
*,
rearrange: bool = False,
) -> None:
vehicle = (
db.query(PlVehicle)
.filter(
PlVehicle.id == vehicle_id,
PlVehicle.tenant_no == tenant_no,
PlVehicle.del_flag == 0,
)
.first()
)
if not vehicle:
raise HTTPException(status_code=400, detail="车辆不存在")
if not vehicle.carrier_id:
return
carrier = (
db.query(PlCarrier)
.filter(
PlCarrier.id == vehicle.carrier_id,
PlCarrier.tenant_no == tenant_no,
PlCarrier.del_flag == 0,
)
.first()
)
if not carrier:
raise HTTPException(status_code=400, detail="承运商不存在")
assert_carrier_eligible_for_transport(carrier, rearrange=rearrange)
def _ensure_driver_access_token(transport: PlTransport) -> str:
token = (transport.driver_access_token or "").strip()
if not token:
@@ -262,7 +298,8 @@ def _apply_transport_arrange_fields(
transport.freight_price = payload.freight_price
transport.freight_amount = freight_amount or 0
transport.freight_type = payload.freight_type
transport.has_invoice = payload.has_invoice
if payload.has_invoice is not None:
transport.has_invoice = payload.has_invoice
if payload.remark is not None:
transport.remark = payload.remark
transport.update_by = user_id
@@ -441,6 +478,26 @@ def arrange_transport(
raise HTTPException(status_code=404, detail="计划不存在")
assert_pl_row_in_scope(db, plan, PlPlan, user, tenant_no)
existing_transport = None
if plan.transport_id:
existing_transport = db.query(PlTransport).filter(
PlTransport.id == plan.transport_id,
PlTransport.tenant_no == tenant_no,
PlTransport.del_flag == 0,
).first()
is_rearrange = _is_rearrange(plan, existing_transport)
if is_rearrange and existing_transport and existing_transport.vehicle_id:
_assert_vehicle_carrier_eligible(
db,
tenant_no,
existing_transport.vehicle_id,
rearrange=True,
)
_assert_vehicle_carrier_eligible(db, tenant_no, payload.vehicle_id, rearrange=False)
# 获取车辆信息
vehicle_info = _get_vehicle_info(db, payload.vehicle_id, tenant_no)
@@ -481,16 +538,6 @@ def arrange_transport(
_apply_plan_logistics_from_payload(plan, payload)
start_point, end_point = _resolve_transport_endpoints(plan, payload)
existing_transport = None
if plan.transport_id:
existing_transport = db.query(PlTransport).filter(
PlTransport.id == plan.transport_id,
PlTransport.tenant_no == tenant_no,
PlTransport.del_flag == 0,
).first()
is_rearrange = _is_rearrange(plan, existing_transport)
if (
plan.plan_type in ("inbound", "outbound")
and is_rearrange
+202
View File
@@ -0,0 +1,202 @@
"""车辆维保 API"""
from datetime import date
from typing import Optional
from fastapi import APIRouter, Depends, Query, status
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.db_auth import DbUserContext, get_db_current_user
from app.core.tenant_context import get_tenant_no
from app.schemas.pl.vehicle_maintenance import (
VehicleMaintenanceApplyCreate,
VehicleMaintenanceApplyListResponse,
VehicleMaintenanceApplyRead,
VehicleMaintenanceApplyReminder,
VehicleMaintenanceApplyUpdate,
VehicleMaintenanceProcessBoardListResponse,
VehicleMaintenanceProcessCreate,
VehicleMaintenanceProcessRead,
VehicleMaintenanceProcessUpdate,
)
from app.services import pl_vehicle_maintenance_service as svc
apply_router = APIRouter(prefix="/vehicle-maintenance-apply", tags=["运输计划-车辆维保申请"])
process_router = APIRouter(prefix="/vehicle-maintenance-process", tags=["运输计划-车辆维保处理"])
@apply_router.get("/", response_model=VehicleMaintenanceApplyListResponse)
def list_applies(
q: Optional[str] = Query(default=None),
maintenance_type: Optional[str] = Query(default=None, alias="maintenanceType"),
status: Optional[str] = Query(default=None),
apply_date_from: Optional[date] = Query(default=None, alias="applyDateFrom"),
apply_date_to: Optional[date] = Query(default=None, alias="applyDateTo"),
applicant_id: Optional[str] = Query(default=None, alias="applicantId"),
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100, alias="pageSize"),
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleMaintenanceApplyListResponse:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
items, total = svc.list_applies(
db,
tenant_no,
user,
q=q,
maintenance_type=maintenance_type,
status=status,
apply_date_from=apply_date_from,
apply_date_to=apply_date_to,
applicant_id=applicant_id,
page=page,
page_size=page_size,
)
return VehicleMaintenanceApplyListResponse(
items=items, total=total, page=page, pageSize=page_size
)
@apply_router.get("/reminders", response_model=list[VehicleMaintenanceApplyReminder])
def list_reminders(
license_plate: str = Query(..., alias="licensePlate"),
exclude_apply_id: Optional[str] = Query(default=None, alias="excludeApplyId"),
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> list[VehicleMaintenanceApplyReminder]:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
return svc.list_reminders(
db,
tenant_no,
user,
license_plate,
exclude_apply_id=exclude_apply_id,
)
@apply_router.get("/{id}", response_model=VehicleMaintenanceApplyRead)
def get_apply(
id: str,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleMaintenanceApplyRead:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
return svc.get_apply(db, tenant_no, user, id)
@apply_router.post("/", response_model=VehicleMaintenanceApplyRead, status_code=status.HTTP_201_CREATED)
def create_apply(
payload: VehicleMaintenanceApplyCreate,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleMaintenanceApplyRead:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
return svc.create_apply(db, tenant_no, user, payload)
@apply_router.put("/{id}", response_model=VehicleMaintenanceApplyRead)
def update_apply(
id: str,
payload: VehicleMaintenanceApplyUpdate,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleMaintenanceApplyRead:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
return svc.update_apply(db, tenant_no, user, id, payload)
@apply_router.post("/{id}/cancel", response_model=VehicleMaintenanceApplyRead)
def cancel_apply(
id: str,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleMaintenanceApplyRead:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
return svc.cancel_apply(db, tenant_no, user, id)
@apply_router.delete("/{ids}", status_code=status.HTTP_204_NO_CONTENT)
def delete_applies(
ids: str,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> None:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
apply_ids = [item for item in ids.split(",") if item]
svc.delete_applies(db, tenant_no, user, apply_ids)
@process_router.get("/process-board", response_model=VehicleMaintenanceProcessBoardListResponse)
def list_process_board(
q: Optional[str] = Query(default=None),
maintenance_type: Optional[str] = Query(default=None, alias="maintenanceType"),
apply_status: Optional[str] = Query(default=None, alias="applyStatus"),
apply_date_from: Optional[date] = Query(default=None, alias="applyDateFrom"),
apply_date_to: Optional[date] = Query(default=None, alias="applyDateTo"),
process_date_from: Optional[date] = Query(default=None, alias="processDateFrom"),
process_date_to: Optional[date] = Query(default=None, alias="processDateTo"),
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100, alias="pageSize"),
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleMaintenanceProcessBoardListResponse:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
items, total = svc.list_process_board(
db,
tenant_no,
user,
q=q,
maintenance_type=maintenance_type,
apply_status=apply_status,
apply_date_from=apply_date_from,
apply_date_to=apply_date_to,
process_date_from=process_date_from,
process_date_to=process_date_to,
page=page,
page_size=page_size,
)
return VehicleMaintenanceProcessBoardListResponse(
items=items, total=total, page=page, pageSize=page_size
)
@process_router.get("/{id}", response_model=VehicleMaintenanceProcessRead)
def get_process(
id: str,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleMaintenanceProcessRead:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
return svc.get_process(db, tenant_no, user, id)
@process_router.post("/", response_model=VehicleMaintenanceProcessRead, status_code=status.HTTP_201_CREATED)
def create_process(
payload: VehicleMaintenanceProcessCreate,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleMaintenanceProcessRead:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
return svc.create_process(db, tenant_no, user, payload)
@process_router.put("/{id}", response_model=VehicleMaintenanceProcessRead)
def update_process(
id: str,
payload: VehicleMaintenanceProcessUpdate,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleMaintenanceProcessRead:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
return svc.update_process(db, tenant_no, user, id, payload)
@process_router.delete("/{ids}", status_code=status.HTTP_204_NO_CONTENT)
def delete_processes(
ids: str,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> None:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
process_ids = [item for item in ids.split(",") if item]
svc.delete_processes(db, tenant_no, user, process_ids)
+228
View File
@@ -0,0 +1,228 @@
"""车辆 ETC/加油充值 API"""
from datetime import date
from typing import Optional
from fastapi import APIRouter, Depends, Query, status
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.db_auth import DbUserContext, get_db_current_user
from app.core.tenant_context import get_tenant_no
from app.schemas.pl.vehicle_recharge import (
VehicleRechargeApplyCreate,
VehicleRechargeApplyListResponse,
VehicleRechargeApplyOption,
VehicleRechargeApplyRead,
VehicleRechargeApplyUpdate,
VehicleRechargeDisburseBoardListResponse,
VehicleRechargeRecordCreate,
VehicleRechargeRecordListResponse,
VehicleRechargeRecordRead,
VehicleRechargeRecordUpdate,
)
from app.services import pl_vehicle_recharge_service as svc
apply_router = APIRouter(prefix="/vehicle-recharge-apply", tags=["运输计划-ETC加油申请"])
record_router = APIRouter(prefix="/vehicle-recharge-record", tags=["运输计划-ETC加油发放"])
@apply_router.get("/", response_model=VehicleRechargeApplyListResponse)
def list_applies(
q: Optional[str] = Query(default=None),
recharge_type: Optional[str] = Query(default=None, alias="rechargeType"),
status: Optional[str] = Query(default=None),
apply_date_from: Optional[date] = Query(default=None, alias="applyDateFrom"),
apply_date_to: Optional[date] = Query(default=None, alias="applyDateTo"),
applicant_id: Optional[str] = Query(default=None, alias="applicantId"),
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100, alias="pageSize"),
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleRechargeApplyListResponse:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
items, total = svc.list_applies(
db,
tenant_no,
user,
q=q,
recharge_type=recharge_type,
status=status,
apply_date_from=apply_date_from,
apply_date_to=apply_date_to,
applicant_id=applicant_id,
page=page,
page_size=page_size,
)
return VehicleRechargeApplyListResponse(
items=items, total=total, page=page, pageSize=page_size
)
@apply_router.get("/pending-options", response_model=list[VehicleRechargeApplyOption])
def pending_apply_options(
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> list[VehicleRechargeApplyOption]:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
rows = svc.list_pending_apply_options(db, tenant_no, user)
return [VehicleRechargeApplyOption.model_validate(row) for row in rows]
@apply_router.get("/{id}", response_model=VehicleRechargeApplyRead)
def get_apply(
id: str,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleRechargeApplyRead:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
return svc.get_apply(db, tenant_no, user, id)
@apply_router.post("/", response_model=VehicleRechargeApplyRead, status_code=status.HTTP_201_CREATED)
def create_apply(
payload: VehicleRechargeApplyCreate,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleRechargeApplyRead:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
return svc.create_apply(db, tenant_no, user, payload)
@apply_router.put("/{id}", response_model=VehicleRechargeApplyRead)
def update_apply(
id: str,
payload: VehicleRechargeApplyUpdate,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleRechargeApplyRead:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
return svc.update_apply(db, tenant_no, user, id, payload)
@apply_router.post("/{id}/cancel", response_model=VehicleRechargeApplyRead)
def cancel_apply(
id: str,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleRechargeApplyRead:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
return svc.cancel_apply(db, tenant_no, user, id)
@apply_router.delete("/{ids}", status_code=status.HTTP_204_NO_CONTENT)
def delete_applies(
ids: str,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> None:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
apply_ids = [item for item in ids.split(",") if item]
svc.delete_applies(db, tenant_no, user, apply_ids)
@record_router.get("/", response_model=VehicleRechargeRecordListResponse)
def list_records(
q: Optional[str] = Query(default=None),
recharge_type: Optional[str] = Query(default=None, alias="rechargeType"),
recharge_date_from: Optional[date] = Query(default=None, alias="rechargeDateFrom"),
recharge_date_to: Optional[date] = Query(default=None, alias="rechargeDateTo"),
gas_station: Optional[str] = Query(default=None, alias="gasStation"),
apply_no: Optional[str] = Query(default=None, alias="applyNo"),
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100, alias="pageSize"),
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleRechargeRecordListResponse:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
items, total = svc.list_records(
db,
tenant_no,
user,
q=q,
recharge_type=recharge_type,
recharge_date_from=recharge_date_from,
recharge_date_to=recharge_date_to,
gas_station=gas_station,
apply_no=apply_no,
page=page,
page_size=page_size,
)
return VehicleRechargeRecordListResponse(
items=items, total=total, page=page, pageSize=page_size
)
@record_router.get("/disburse-board", response_model=VehicleRechargeDisburseBoardListResponse)
def list_disburse_board(
q: Optional[str] = Query(default=None),
recharge_type: Optional[str] = Query(default=None, alias="rechargeType"),
apply_status: Optional[str] = Query(default=None, alias="applyStatus"),
apply_date_from: Optional[date] = Query(default=None, alias="applyDateFrom"),
apply_date_to: Optional[date] = Query(default=None, alias="applyDateTo"),
recharge_date_from: Optional[date] = Query(default=None, alias="rechargeDateFrom"),
recharge_date_to: Optional[date] = Query(default=None, alias="rechargeDateTo"),
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100, alias="pageSize"),
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleRechargeDisburseBoardListResponse:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
items, total = svc.list_disburse_board(
db,
tenant_no,
user,
q=q,
recharge_type=recharge_type,
apply_status=apply_status,
apply_date_from=apply_date_from,
apply_date_to=apply_date_to,
recharge_date_from=recharge_date_from,
recharge_date_to=recharge_date_to,
page=page,
page_size=page_size,
)
return VehicleRechargeDisburseBoardListResponse(
items=items, total=total, page=page, pageSize=page_size
)
@record_router.get("/{id}", response_model=VehicleRechargeRecordRead)
def get_record(
id: str,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleRechargeRecordRead:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
return svc.get_record(db, tenant_no, user, id)
@record_router.post("/", response_model=VehicleRechargeRecordRead, status_code=status.HTTP_201_CREATED)
def create_record(
payload: VehicleRechargeRecordCreate,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleRechargeRecordRead:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
return svc.create_record(db, tenant_no, user, payload)
@record_router.put("/{id}", response_model=VehicleRechargeRecordRead)
def update_record(
id: str,
payload: VehicleRechargeRecordUpdate,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleRechargeRecordRead:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
return svc.update_record(db, tenant_no, user, id, payload)
@record_router.delete("/{ids}", status_code=status.HTTP_204_NO_CONTENT)
def delete_records(
ids: str,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> None:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
record_ids = [item for item in ids.split(",") if item]
svc.delete_records(db, tenant_no, user, record_ids)
+180
View File
@@ -0,0 +1,180 @@
"""车辆尿素 API"""
from datetime import date
from typing import Optional
from fastapi import APIRouter, Depends, Query, status
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.db_auth import DbUserContext, get_db_current_user
from app.core.tenant_context import get_tenant_no
from app.schemas.pl.vehicle_urea import (
VehicleUreaApplyCreate,
VehicleUreaApplyListResponse,
VehicleUreaApplyRead,
VehicleUreaApplyUpdate,
VehicleUreaDisburseBoardListResponse,
VehicleUreaDisburseCreate,
VehicleUreaDisburseRead,
VehicleUreaDisburseUpdate,
)
from app.services import pl_vehicle_urea_service as svc
apply_router = APIRouter(prefix="/vehicle-urea-apply", tags=["运输计划-车辆尿素申请"])
disburse_router = APIRouter(prefix="/vehicle-urea-disburse", tags=["运输计划-车辆尿素发放"])
@apply_router.get("/", response_model=VehicleUreaApplyListResponse)
def list_applies(
q: Optional[str] = Query(default=None),
status: Optional[str] = Query(default=None),
apply_date_from: Optional[date] = Query(default=None, alias="applyDateFrom"),
apply_date_to: Optional[date] = Query(default=None, alias="applyDateTo"),
applicant_id: Optional[str] = Query(default=None, alias="applicantId"),
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100, alias="pageSize"),
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleUreaApplyListResponse:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
items, total = svc.list_applies(
db,
tenant_no,
user,
q=q,
status=status,
apply_date_from=apply_date_from,
apply_date_to=apply_date_to,
applicant_id=applicant_id,
page=page,
page_size=page_size,
)
return VehicleUreaApplyListResponse(
items=items, total=total, page=page, pageSize=page_size
)
@apply_router.get("/{id}", response_model=VehicleUreaApplyRead)
def get_apply(
id: str,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleUreaApplyRead:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
return svc.get_apply(db, tenant_no, user, id)
@apply_router.post("/", response_model=VehicleUreaApplyRead, status_code=status.HTTP_201_CREATED)
def create_apply(
payload: VehicleUreaApplyCreate,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleUreaApplyRead:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
return svc.create_apply(db, tenant_no, user, payload)
@apply_router.put("/{id}", response_model=VehicleUreaApplyRead)
def update_apply(
id: str,
payload: VehicleUreaApplyUpdate,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleUreaApplyRead:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
return svc.update_apply(db, tenant_no, user, id, payload)
@apply_router.post("/{id}/cancel", response_model=VehicleUreaApplyRead)
def cancel_apply(
id: str,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleUreaApplyRead:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
return svc.cancel_apply(db, tenant_no, user, id)
@apply_router.delete("/{ids}", status_code=status.HTTP_204_NO_CONTENT)
def delete_applies(
ids: str,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> None:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
apply_ids = [item for item in ids.split(",") if item]
svc.delete_applies(db, tenant_no, user, apply_ids)
@disburse_router.get("/disburse-board", response_model=VehicleUreaDisburseBoardListResponse)
def list_disburse_board(
q: Optional[str] = Query(default=None),
apply_status: Optional[str] = Query(default=None, alias="applyStatus"),
apply_date_from: Optional[date] = Query(default=None, alias="applyDateFrom"),
apply_date_to: Optional[date] = Query(default=None, alias="applyDateTo"),
disburse_date_from: Optional[date] = Query(default=None, alias="disburseDateFrom"),
disburse_date_to: Optional[date] = Query(default=None, alias="disburseDateTo"),
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100, alias="pageSize"),
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleUreaDisburseBoardListResponse:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
items, total = svc.list_disburse_board(
db,
tenant_no,
user,
q=q,
apply_status=apply_status,
apply_date_from=apply_date_from,
apply_date_to=apply_date_to,
disburse_date_from=disburse_date_from,
disburse_date_to=disburse_date_to,
page=page,
page_size=page_size,
)
return VehicleUreaDisburseBoardListResponse(
items=items, total=total, page=page, pageSize=page_size
)
@disburse_router.get("/{id}", response_model=VehicleUreaDisburseRead)
def get_disburse(
id: str,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleUreaDisburseRead:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
return svc.get_disburse(db, tenant_no, user, id)
@disburse_router.post("/", response_model=VehicleUreaDisburseRead, status_code=status.HTTP_201_CREATED)
def create_disburse(
payload: VehicleUreaDisburseCreate,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleUreaDisburseRead:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
return svc.create_disburse(db, tenant_no, user, payload)
@disburse_router.put("/{id}", response_model=VehicleUreaDisburseRead)
def update_disburse(
id: str,
payload: VehicleUreaDisburseUpdate,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> VehicleUreaDisburseRead:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
return svc.update_disburse(db, tenant_no, user, id, payload)
@disburse_router.delete("/{ids}", status_code=status.HTTP_204_NO_CONTENT)
def delete_disburses(
ids: str,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> None:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
disburse_ids = [item for item in ids.split(",") if item]
svc.delete_disburses(db, tenant_no, user, disburse_ids)
+2
View File
@@ -57,12 +57,14 @@ def _build_user_info_payload(user: SysUser, web_role: str) -> dict:
def _jwt_user_payload(user: SysUser, web_role: str) -> dict:
tenant_no = (getattr(user, "tenant_no", None) or "").strip() or settings.DEFAULT_TENANT_NO
return {
"sub": user.id,
"username": user.user_no,
"role": web_role,
"region_id": user.dept_id,
"real_name": user.nick_name,
"tenant_no": tenant_no,
}
+1 -1
View File
@@ -244,7 +244,7 @@ async def delete_process_def(
db: Session = Depends(get_db),
):
try:
wf_crud.soft_delete_process_def(db, def_id)
wf_crud.soft_delete_process_def(db, def_id, user_info=_user)
return success_response(None, message="删除成功")
except ValueError as e:
return error_response(400, str(e))
+83 -10
View File
@@ -13,6 +13,8 @@ from starlette.responses import Response
from app.core.config import settings
from app.core.database import SessionLocal
from app.core.oper_log_context import clear_oper_log_meta, snapshot_oper_log_meta
from app.core.oper_log_infer import business_type_from_path, title_from_path
logger = logging.getLogger("sqxp")
@@ -40,6 +42,26 @@ def _truncate(s: str, max_len: int) -> str:
return s[:max_len] + f"\n... [truncated, total {len(s)} chars]"
# MySQL TEXT 列上限为 65535 字节(非字符);utf8mb4 下中文等多字节字符需按字节截断。
_DB_TEXT_MAX_BYTES = 60000
def _truncate_utf8_bytes(s: str, max_bytes: int = _DB_TEXT_MAX_BYTES) -> str:
if not s:
return s
encoded = s.encode("utf-8")
if len(encoded) <= max_bytes:
return s
truncated = encoded[:max_bytes]
while truncated and (truncated[-1] & 0xC0) == 0x80:
truncated = truncated[:-1]
text = truncated.decode("utf-8", errors="ignore")
return (
text
+ f"\n... [truncated, total {len(s)} chars, {len(encoded)} bytes]"
)
def _redact_obj(obj: Any) -> Any:
if isinstance(obj, dict):
out = {}
@@ -86,12 +108,14 @@ def _format_body_bytes(raw: bytes, content_type: str, max_bytes: int) -> str:
return _truncate(raw.decode("utf-8", errors="replace"), settings.API_REQUEST_LOG_MAX_BODY)
def _claims_from_auth(auth_header: Optional[str]) -> Tuple[Optional[str], Optional[str], Optional[str]]:
def _claims_from_auth(
auth_header: Optional[str],
) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str], Optional[str]]:
if not auth_header or not auth_header.startswith("Bearer "):
return None, None, None
return None, None, None, None, None
token = auth_header[7:].strip()
if not token:
return None, None, None
return None, None, None, None, None
try:
payload = jwt.decode(
token,
@@ -99,9 +123,28 @@ def _claims_from_auth(auth_header: Optional[str]) -> Tuple[Optional[str], Option
algorithms=[settings.ALGORITHM],
options={"verify_signature": False, "verify_exp": False},
)
return payload.get("sub"), payload.get("username"), payload.get("role")
return (
payload.get("sub"),
payload.get("username"),
payload.get("role"),
payload.get("real_name"),
payload.get("tenant_no"),
)
except Exception:
return None, None, None
return None, None, None, None, None
def _should_skip_exclude(path: str) -> bool:
raw = (settings.API_REQUEST_LOG_EXCLUDE_PATHS or "").strip()
if not raw:
return False
for item in raw.split(","):
prefix = item.strip()
if not prefix:
continue
if path == prefix or path.startswith(prefix):
return True
return False
def _should_skip(request: Request) -> bool:
@@ -112,6 +155,8 @@ def _should_skip(request: Request) -> bool:
return True
if p.startswith("/docs") or p.startswith("/redoc"):
return True
if _should_skip_exclude(p):
return True
return False
@@ -129,6 +174,12 @@ def _persist_log(
role: Optional[str],
ip_address: Optional[str],
user_agent: Optional[str],
create_name: Optional[str] = None,
tenant_no: Optional[str] = None,
business_title: Optional[str] = None,
business_type: Optional[int] = None,
business_id: Optional[str] = None,
oper_summary: Optional[str] = None,
) -> None:
from datetime import datetime
@@ -142,8 +193,8 @@ def _persist_log(
http_method=http_method,
path=_truncate(path, 500),
query_string=_truncate(query_string or "", 2000) or None,
request_body=request_body,
response_body=response_body,
request_body=_truncate_utf8_bytes(request_body) if request_body else None,
response_body=_truncate_utf8_bytes(response_body) if response_body else None,
status_code=status_code,
duration_ms=duration_ms,
user_id=user_id,
@@ -151,10 +202,15 @@ def _persist_log(
role=role,
ip_address=ip_address,
user_agent=_truncate(user_agent or "", 500) if user_agent else None,
tenant_no=_truncate(tenant_no or "", 64) if tenant_no else None,
business_title=_truncate(business_title or "", 100) if business_title else None,
business_type=business_type,
business_id=_truncate(business_id or "", 64) if business_id else None,
oper_summary=_truncate(oper_summary or "", 500) if oper_summary else None,
is_del=0,
create_at=datetime.now(),
create_by=None,
create_name=None,
create_by=user_id,
create_name=_truncate(create_name or "", 100) if create_name else None,
)
db.add(row)
db.commit()
@@ -209,7 +265,7 @@ class ApiRequestLogMiddleware(BaseHTTPMiddleware):
request = Request(request.scope, receive)
uid, uname, role = _claims_from_auth(request.headers.get("authorization"))
uid, uname, role, real_name, tenant_no = _claims_from_auth(request.headers.get("authorization"))
ip = request.headers.get("x-forwarded-for", "").split(",")[0].strip()
if not ip and request.client:
ip = request.client.host
@@ -258,6 +314,16 @@ class ApiRequestLogMiddleware(BaseHTTPMiddleware):
elapsed_ms = int((time.perf_counter() - start) * 1000)
path = request.url.path
qs = request.url.query
meta = snapshot_oper_log_meta()
biz_title = meta.business_title if meta and meta.business_title else title_from_path(path)
biz_type = (
meta.business_type
if meta and meta.business_type is not None
else business_type_from_path(path, request.method)
)
biz_id = meta.business_id if meta else None
oper_summary = meta.oper_summary if meta else None
effective_tenant = tenant_no or settings.DEFAULT_TENANT_NO
_schedule_persist_log(
http_method=request.method,
path=path,
@@ -271,6 +337,13 @@ class ApiRequestLogMiddleware(BaseHTTPMiddleware):
role=role,
ip_address=ip,
user_agent=ua,
create_name=real_name,
tenant_no=effective_tenant,
business_title=biz_title,
business_type=biz_type,
business_id=biz_id,
oper_summary=oper_summary,
)
clear_oper_log_meta()
return out
+1
View File
@@ -122,6 +122,7 @@ class Settings(BaseSettings):
API_REQUEST_LOG_ENABLED: bool = True
API_REQUEST_LOG_MAX_BODY: int = 65536
API_REQUEST_LOG_MAX_BODY_BYTES: int = 131072
API_REQUEST_LOG_EXCLUDE_PATHS: str = ""
# Web API 请求/响应加密配置
API_ENCRYPT_ENABLED: bool = False
+118
View File
@@ -0,0 +1,118 @@
"""删除权限:仅创建人可删(super_admin 例外)。"""
from __future__ import annotations
from typing import Any, Dict, Iterable, Optional, Union
from fastapi import HTTPException
RowLike = Union[Any, Dict[str, Any]]
def normalize_user_info(
user_info: Optional[Dict[str, Any]] = None,
*,
user_id: Optional[str] = None,
actor_name: Optional[str] = None,
) -> Dict[str, Any]:
info: Dict[str, Any] = dict(user_info or {})
uid = (user_id or info.get("user_id") or info.get("id") or "").strip()
if uid:
info["user_id"] = uid
name = (actor_name or info.get("real_name") or info.get("username") or info.get("userName") or info.get("name") or "").strip()
if name:
info["real_name"] = name
return info
def user_info_from_context(ctx_or_dict: Any) -> Dict[str, Any]:
if isinstance(ctx_or_dict, dict):
return normalize_user_info(ctx_or_dict)
info: Dict[str, Any] = {}
if hasattr(ctx_or_dict, "to_user_info"):
info.update(ctx_or_dict.to_user_info())
name = (getattr(ctx_or_dict, "name", None) or "").strip()
if name:
info["real_name"] = name
return normalize_user_info(info)
def is_super_admin(user_info: Optional[Dict[str, Any]]) -> bool:
if not user_info:
return False
return (user_info.get("role") or "").strip() == "super_admin"
def row_creator_id(row: RowLike) -> str:
if row is None:
return ""
if isinstance(row, dict):
return (row.get("create_by") or row.get("createBy") or "").strip()
return (getattr(row, "create_by", None) or getattr(row, "createBy", None) or "").strip()
def _actor_ids(user_info: Optional[Dict[str, Any]]) -> set[str]:
if not user_info:
return set()
ids: set[str] = set()
for key in ("user_id", "id"):
val = (user_info.get(key) or "").strip()
if val:
ids.add(val)
return ids
def _actor_names(user_info: Optional[Dict[str, Any]]) -> set[str]:
if not user_info:
return set()
names: set[str] = set()
for key in ("real_name", "username", "userName", "name"):
val = (user_info.get(key) or "").strip()
if val:
names.add(val)
return names
def can_delete_by_creator(
row: RowLike,
user_info: Optional[Dict[str, Any]] = None,
*,
user_id: Optional[str] = None,
actor_name: Optional[str] = None,
) -> bool:
info = normalize_user_info(user_info, user_id=user_id, actor_name=actor_name)
if is_super_admin(info):
return True
creator = row_creator_id(row)
if not creator:
return False
if creator in _actor_ids(info):
return True
if creator in _actor_names(info):
return True
return False
def assert_row_deletable_by_creator(
row: RowLike,
user_info: Optional[Dict[str, Any]] = None,
*,
user_id: Optional[str] = None,
actor_name: Optional[str] = None,
message: str = "仅创建人可删除",
) -> None:
if can_delete_by_creator(row, user_info, user_id=user_id, actor_name=actor_name):
return
raise HTTPException(status_code=403, detail=message)
def assert_rows_deletable_by_creator(
rows: Iterable[RowLike],
user_info: Optional[Dict[str, Any]] = None,
*,
user_id: Optional[str] = None,
actor_name: Optional[str] = None,
message: str = "仅创建人可删除",
) -> None:
info = normalize_user_info(user_info, user_id=user_id, actor_name=actor_name)
for row in rows:
assert_row_deletable_by_creator(row, info, message=message)
+34
View File
@@ -0,0 +1,34 @@
"""请求级操作日志元数据(ContextVar)"""
from __future__ import annotations
from contextvars import ContextVar
from dataclasses import dataclass
from typing import Optional
@dataclass
class OperLogMeta:
business_title: Optional[str] = None
business_type: Optional[int] = None
business_id: Optional[str] = None
oper_summary: Optional[str] = None
_oper_log_meta: ContextVar[Optional[OperLogMeta]] = ContextVar("oper_log_meta", default=None)
def set_oper_log_meta(meta: OperLogMeta) -> None:
_oper_log_meta.set(meta)
def get_oper_log_meta() -> Optional[OperLogMeta]:
return _oper_log_meta.get()
def clear_oper_log_meta() -> None:
_oper_log_meta.set(None)
def snapshot_oper_log_meta() -> Optional[OperLogMeta]:
"""供异步落库线程使用,避免 ContextVar 跨线程丢失。"""
return get_oper_log_meta()
+164
View File
@@ -0,0 +1,164 @@
"""路由操作日志装饰器:写入 ContextVar 供中间件落库"""
from __future__ import annotations
import asyncio
import inspect
from functools import wraps
from typing import Any, Callable, Optional, TypeVar, get_type_hints
from app.core.oper_log_context import OperLogMeta, set_oper_log_meta
F = TypeVar("F", bound=Callable[..., Any])
_SUMMARY_VERBS = {
0: "操作",
1: "新增",
2: "修改",
3: "删除",
5: "导出",
6: "导入",
}
def _to_dict(obj: Any) -> dict:
if obj is None:
return {}
if isinstance(obj, dict):
return obj
if hasattr(obj, "model_dump"):
try:
return obj.model_dump(by_alias=True)
except Exception:
return obj.model_dump()
return {}
def _pick_field(data: dict, field: str) -> Optional[str]:
if not field or not data:
return None
if field in data and data[field] is not None:
return str(data[field])
# camelCase from snake
parts = field.split("_")
camel = parts[0] + "".join(p.capitalize() for p in parts[1:])
if camel in data and data[camel] is not None:
return str(data[camel])
return None
def _resolve_business_id(
kwargs: dict,
business_id_field: Optional[str],
path_param: Optional[str],
) -> Optional[str]:
if path_param and kwargs.get(path_param) is not None:
val = str(kwargs[path_param])
if "," in val:
return val.split(",")[0].strip() or None
return val
body = kwargs.get("body")
data = _to_dict(body)
if business_id_field:
val = _pick_field(data, business_id_field)
if val:
return val
raw = data.get(business_id_field)
if isinstance(raw, list) and raw:
return str(raw[0])
if data.get("id"):
return str(data["id"])
return None
def _format_summary(template: Optional[str], business_type: int, data: dict) -> Optional[str]:
if template:
out = template
for key, val in data.items():
out = out.replace(f"{{{key}}}", str(val) if val is not None else "")
if "{" not in out and "}" not in out:
return out[:500]
name = (
_pick_field(data, "customer_name")
or _pick_field(data, "customerName")
or _pick_field(data, "contract_name")
or _pick_field(data, "contractName")
or _pick_field(data, "contract_no")
or _pick_field(data, "contractNo")
or ""
)
verb = _SUMMARY_VERBS.get(business_type, "操作")
if name:
return f"{verb}【{name}】"[:500]
return None
def _preserve_fastapi_signature(wrapper: Callable[..., Any], func: Callable[..., Any]) -> None:
"""FastAPI 通过 inspect.signature + wrapper.__globals__ 解析类型;需在原函数命名空间下解析后写回。"""
try:
hints = get_type_hints(func, globalns=func.__globals__, localns=None)
sig = inspect.signature(func)
typed_params = [
inspect.Parameter(
name=param.name,
kind=param.kind,
default=param.default,
annotation=hints.get(param.name, param.annotation),
)
for param in sig.parameters.values()
]
wrapper.__signature__ = inspect.Signature(
parameters=typed_params,
return_annotation=hints.get("return", sig.return_annotation),
)
wrapper.__annotations__ = hints
except Exception:
pass
def oper_log(
title: str,
business_type: int = 0,
business_id_field: Optional[str] = "id",
path_param: Optional[str] = None,
summary: Optional[str] = None,
) -> Callable[[F], F]:
"""在 handler 执行前设置操作日志元数据。"""
def decorator(func: F) -> F:
@wraps(func)
async def async_wrapper(*args, **kwargs):
data = _to_dict(kwargs.get("body"))
business_id = _resolve_business_id(kwargs, business_id_field, path_param)
oper_summary = _format_summary(summary, business_type, data)
set_oper_log_meta(
OperLogMeta(
business_title=title,
business_type=business_type,
business_id=business_id,
oper_summary=oper_summary,
)
)
return await func(*args, **kwargs)
@wraps(func)
def sync_wrapper(*args, **kwargs):
data = _to_dict(kwargs.get("body"))
business_id = _resolve_business_id(kwargs, business_id_field, path_param)
oper_summary = _format_summary(summary, business_type, data)
set_oper_log_meta(
OperLogMeta(
business_title=title,
business_type=business_type,
business_id=business_id,
oper_summary=oper_summary,
)
)
return func(*args, **kwargs)
if asyncio.iscoroutinefunction(func):
_preserve_fastapi_signature(async_wrapper, func)
return async_wrapper # type: ignore[return-value]
_preserve_fastapi_signature(sync_wrapper, func)
return sync_wrapper # type: ignore[return-value]
return decorator
+63
View File
@@ -0,0 +1,63 @@
"""操作日志路径推断(中间件落库与查询兜底)"""
from __future__ import annotations
import re
from typing import List, Tuple
_ACTION_TYPE_MAP = {
"add": 1,
"create": 1,
"insert": 1,
"update": 2,
"edit": 2,
"modify": 2,
"delete": 3,
"remove": 3,
"export": 5,
"import": 6,
"transfer": 2,
"intopool": 2,
"deal": 2,
"claim": 2,
"release": 2,
"invalid": 2,
"assign": 2,
"sign": 2,
"renew": 1,
}
_MODULE_TITLE_RULES: List[Tuple[re.Pattern[str], str]] = [
(re.compile(r"/crm/customer", re.I), "CRM客户管理"),
(re.compile(r"/crm/contact", re.I), "CRM联系人"),
(re.compile(r"/crm/opportunity", re.I), "CRM商机管理"),
(re.compile(r"/crm/contract", re.I), "CRM合同管理"),
(re.compile(r"/crm/invoice-info", re.I), "CRM开票信息"),
(re.compile(r"/crm/", re.I), "CRM管理"),
(re.compile(r"/web/admin/", re.I), "系统管理"),
(re.compile(r"/web/auth/", re.I), "认证授权"),
(re.compile(r"/web/monitor/", re.I), "系统监控"),
]
def title_from_path(path: str) -> str:
for pattern, title in _MODULE_TITLE_RULES:
if pattern.search(path or ""):
return title
parts = [p for p in (path or "").split("/") if p]
return parts[-1] if parts else "操作日志"
def business_type_from_path(path: str, http_method: str) -> int:
parts = [p for p in (path or "").split("/") if p]
if parts:
last = parts[-1].lower()
if last in _ACTION_TYPE_MAP:
return _ACTION_TYPE_MAP[last]
method = (http_method or "").upper()
if method == "POST":
return 1
if method in ("PUT", "PATCH"):
return 2
if method == "DELETE":
return 3
return 0
+5
View File
@@ -106,6 +106,11 @@ class ApiRequestLogCRUD:
role=log.role,
ip_address=log.ip_address,
user_agent=log.user_agent,
tenant_no=log.tenant_no,
business_title=log.business_title,
business_type=log.business_type,
business_id=log.business_id,
oper_summary=log.oper_summary,
create_at=log.create_at,
create_by=log.create_by,
create_name=log.create_name,
+2
View File
@@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import and_, func, or_
from sqlalchemy.orm import Session
from app.core.delete_guard import assert_row_deletable_by_creator
from app.core.utils import generate_uuid
from app.models.crm_models import CrmClue
from app.models.models import SysUser
@@ -399,6 +400,7 @@ def soft_delete_clue(
_ensure_clue_in_scope(db, obj, user_info, obj.tenant_no)
if obj.is_converted == 1:
raise ValueError("已转化的线索不可删除")
assert_row_deletable_by_creator(obj, user_info, user_id=user_id)
obj.del_flag = 1
obj.update_by = user_id
obj.update_time = datetime.now()
+2
View File
@@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import func
from sqlalchemy.orm import Session
from app.core.delete_guard import assert_row_deletable_by_creator
from app.core.utils import generate_uuid
from app.crud.crm.clue import get_clue, _ensure_clue_in_scope
from app.crud.crm.data_scope_helpers import apply_crm_create_by_scope, ensure_crm_create_by_row_in_scope
@@ -216,6 +217,7 @@ def delete_clue_follow(
ensure_crm_create_by_row_in_scope(
db, row, user_info, row.tenant_no, message="无权访问该跟进记录"
)
assert_row_deletable_by_creator(row, user_info)
clue_id = row.clue_id
row.del_flag = 1
+2
View File
@@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import func, or_
from sqlalchemy.orm import Session
from app.core.delete_guard import assert_row_deletable_by_creator
from app.core.utils import generate_uuid
from app.crud.crm.data_scope_helpers import (
apply_crm_create_by_scope,
@@ -335,6 +336,7 @@ def soft_delete_contact(
if tenant_no and obj.tenant_no != tenant_no:
raise ValueError("联系人不存在")
ensure_crm_contact_row_in_scope(db, obj, user_info, obj.tenant_no)
assert_row_deletable_by_creator(obj, user_info, user_id=user_id)
customer_id = obj.customer_id
obj.del_flag = 1
obj.update_by = user_id
+4
View File
@@ -12,6 +12,7 @@ from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import func, or_
from sqlalchemy.orm import Query, Session
from app.core.delete_guard import assert_row_deletable_by_creator
from app.core.utils import generate_uuid
from app.crud.crm.data_scope_helpers import apply_crm_owner_scope, ensure_crm_owner_row_in_scope, apply_crm_create_by_scope, ensure_crm_create_by_row_in_scope
from app.crud.crm.contact import get_contact
@@ -1064,6 +1065,7 @@ def soft_delete_contracts(
continue
if validate and entity.status != "draft":
raise ValueError("只能删除草稿状态的合同")
assert_row_deletable_by_creator(entity, user_info)
entity.del_flag = 1
entity.update_time = datetime.now()
count += 1
@@ -2287,6 +2289,7 @@ def soft_delete_contract_changes(
)
if validate and entity.approval_status != "pending":
raise ValueError("只能删除待审批状态的变更记录")
assert_row_deletable_by_creator(entity, user_info)
entity.del_flag = 1
entity.update_time = datetime.now()
count += 1
@@ -2491,6 +2494,7 @@ def soft_delete_contract_templates(
)
if validate and (entity.usage_count or 0) > 0:
raise ValueError(f"模板【{entity.template_name}】已被使用,不能删除")
assert_row_deletable_by_creator(entity, user_info)
entity.del_flag = 1
entity.update_time = datetime.now()
count += 1
+2
View File
@@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import and_, func, or_
from sqlalchemy.orm import Session
from app.core.delete_guard import assert_row_deletable_by_creator
from app.core.utils import generate_uuid
from app.models.crm_models import CrmCustomer
from app.models.models import SysUser
@@ -501,6 +502,7 @@ def soft_delete_customer(
if tenant_no and obj.tenant_no != tenant_no:
raise ValueError("客户不存在")
_ensure_customer_in_scope(db, obj, user_info, obj.tenant_no)
assert_row_deletable_by_creator(obj, user_info, user_id=user_id)
obj.del_flag = 1
obj.update_by = user_id
obj.update_time = datetime.now()
+2
View File
@@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import func, or_
from sqlalchemy.orm import Session
from app.core.delete_guard import assert_row_deletable_by_creator
from app.core.utils import generate_uuid
from app.crud.crm.customer import _resolve_user_display_name, get_customer, _ensure_customer_in_scope
from app.models.crm_models import CrmCustomerFollow
@@ -240,6 +241,7 @@ def soft_delete_follow(
if tenant_no and obj.tenant_no != tenant_no:
raise ValueError("跟进记录不存在")
ensure_crm_create_by_row_in_scope(db, obj, user_info, obj.tenant_no, message="无权访问该跟进记录")
assert_row_deletable_by_creator(obj, user_info, user_id=user_id)
obj.del_flag = 1
obj.update_by = user_id
obj.update_time = datetime.now()
+2
View File
@@ -6,6 +6,7 @@ from typing import Any, Dict, List, Optional
from sqlalchemy.orm import Session
from app.core.delete_guard import assert_row_deletable_by_creator
from app.core.utils import generate_uuid
from app.crud.crm.data_scope_helpers import (
ensure_crm_customer_linked_row_in_scope,
@@ -246,6 +247,7 @@ def soft_delete_invoice_info(
ensure_crm_customer_linked_row_in_scope(
db, obj, user_info, obj.tenant_no, message="无权访问该开票信息"
)
assert_row_deletable_by_creator(obj, user_info, user_id=user_id)
obj.del_flag = 1
obj.update_by = user_id
obj.update_time = datetime.now()
+2
View File
@@ -9,6 +9,7 @@ from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import case, func, or_
from sqlalchemy.orm import Session
from app.core.delete_guard import assert_row_deletable_by_creator
from app.core.utils import generate_uuid
from app.models.crm_models import (
CrmContact,
@@ -663,6 +664,7 @@ def soft_delete_opportunities(
for row in rows:
_ensure_opportunity_in_scope(db, row, user_info, row.tenant_no)
_guard_terminal_status(row, "删除")
assert_row_deletable_by_creator(row, user_info, user_id=user_id)
row.del_flag = 1
row.update_by = user_id
row.update_time = now
@@ -7,6 +7,7 @@ from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy.orm import Session
from app.core.delete_guard import assert_row_deletable_by_creator
from app.core.utils import generate_uuid
from app.crud.crm.data_scope_helpers import apply_crm_create_by_scope, ensure_crm_create_by_row_in_scope
from app.crud.crm.opportunity import _ensure_opportunity_in_scope
@@ -300,6 +301,7 @@ def soft_delete_opportunity_follow(
ensure_crm_create_by_row_in_scope(
db, row, user_info, row.tenant_no, message="无权访问该跟进记录"
)
assert_row_deletable_by_creator(row, user_info, user_id=user_id)
row.del_flag = 1
row.update_by = user_id
row.update_time = datetime.now()
@@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy.orm import Session
from app.core.delete_guard import assert_row_deletable_by_creator
from app.core.utils import generate_uuid
from app.crud.crm.opportunity import _ensure_opportunity_in_scope
from app.models.crm_models import CrmOpportunity, CrmOpportunityProduct, CrmProduct
@@ -316,6 +317,7 @@ def soft_delete_opportunity_products(
_get_opportunity_for_product(db, opp_id, tenant_no, user_info)
now = datetime.now()
for row in rows:
assert_row_deletable_by_creator(row, user_info, user_id=user_id)
row.del_flag = 1
row.update_by = user_id
row.update_time = now
+2
View File
@@ -9,6 +9,7 @@ from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import or_
from sqlalchemy.orm import Session
from app.core.delete_guard import assert_row_deletable_by_creator
from app.core.utils import generate_uuid
from app.models.crm_models import CrmCustomer, CrmCustomerPool, CrmCustomerPoolRule
from app.crud.crm.data_scope_helpers import (
@@ -172,6 +173,7 @@ def delete_pool_rules(
ensure_crm_create_by_row_in_scope(db, row, user_info, tenant_no, message="无权访问该公海规则")
if row.status == "enable":
raise ValueError(f"公海池规则【{row.pool_name}】正在启用中,请先禁用后再删除")
assert_row_deletable_by_creator(row, user_info)
row.del_flag = 1
row.update_time = datetime.now()
count += 1
+3
View File
@@ -9,6 +9,7 @@ from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import and_, func, or_
from sqlalchemy.orm import Session
from app.core.delete_guard import assert_row_deletable_by_creator
from app.core.utils import generate_uuid
from app.crud.crm.data_scope_helpers import (
apply_crm_create_by_scope,
@@ -256,6 +257,7 @@ def soft_delete_category(db: Session, category_id: str, *, user_info=None) -> No
if has_products:
raise ValueError("该分类下存在子分类/产品,无法删除")
assert_row_deletable_by_creator(obj, user_info)
obj.del_flag = 1
db.commit()
@@ -566,6 +568,7 @@ def soft_delete_product(db: Session, product_id: str, *, user_info=None) -> None
ensure_crm_create_by_row_in_scope(
db, obj, user_info, obj.tenant_no, message="无权访问该产品"
)
assert_row_deletable_by_creator(obj, user_info)
obj.del_flag = 1
db.commit()
+2
View File
@@ -11,6 +11,7 @@ from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import exists, func, or_
from sqlalchemy.orm import Query, Session, aliased
from app.core.delete_guard import assert_row_deletable_by_creator
from app.core.utils import generate_uuid
from app.crud.crm.contact import get_contact
from app.crud.crm.customer import get_customer
@@ -806,6 +807,7 @@ def soft_delete_quotes(
raise ValueError("只能删除草稿状态的报价单")
if entity.contract_id:
raise ValueError("已转合同的报价单不能删除")
assert_row_deletable_by_creator(entity, user_info)
entity.del_flag = 1
entity.update_time = datetime.now()
count += 1
+2
View File
@@ -7,6 +7,7 @@ from typing import Any, Dict, List, Optional
from sqlalchemy.orm import Session
from app.core.delete_guard import assert_row_deletable_by_creator
from app.core.utils import generate_uuid
from app.crud.crm.product import get_product
from app.models.crm_models import CrmQuoteProduct
@@ -253,6 +254,7 @@ def soft_delete_quote_products(
)
if sibling_count <= 1:
raise ValueError("报价单至少保留1个产品")
assert_row_deletable_by_creator(entity, user_info)
entity.del_flag = 1
entity.update_time = datetime.now()
count += 1
+2
View File
@@ -7,6 +7,7 @@ from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy.orm import Session
from app.core.delete_guard import assert_row_deletable_by_creator
from app.core.utils import generate_uuid
from app.crud.crm.data_scope_helpers import (
apply_crm_create_by_scope,
@@ -223,6 +224,7 @@ def soft_delete_supplier_info(
ensure_crm_create_by_row_in_scope(
db, obj, user_info, obj.tenant_no, message="无权访问该供方信息"
)
assert_row_deletable_by_creator(obj, user_info, user_id=user_id)
obj.del_flag = 1
obj.update_by = user_id
obj.update_time = datetime.now()
+4 -1
View File
@@ -6,6 +6,7 @@ from typing import Any, Dict, List, Optional
from sqlalchemy import and_, or_
from sqlalchemy.orm import Session
from app.core.delete_guard import assert_row_deletable_by_creator
from app.core.utils import generate_uuid
from app.models.models import SysDept
@@ -22,6 +23,7 @@ def _row_to_dict(d: SysDept) -> Dict[str, Any]:
"tenantNo": d.tenant_no,
"orderNum": d.order_num if d.order_num is not None else 0,
"remark": d.remark or "",
"createBy": d.create_by or "",
"createTime": d.create_time.strftime("%Y-%m-%d %H:%M:%S") if d.create_time else "",
"updateTime": d.update_time.strftime("%Y-%m-%d %H:%M:%S") if d.update_time else "",
}
@@ -194,10 +196,11 @@ def _cascade_soft_delete(db: Session, dept_id: str) -> None:
db.add(child)
def soft_delete_dept(db: Session, dept_id: str) -> None:
def soft_delete_dept(db: Session, dept_id: str, *, user_info: dict = None) -> None:
obj = get_dept(db, dept_id)
if not obj:
raise ValueError("组织不存在")
assert_row_deletable_by_creator(obj, user_info)
_cascade_soft_delete(db, dept_id)
obj.del_flag = 1
obj.update_time = datetime.now()
+4 -1
View File
@@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import and_, func, or_
from sqlalchemy.orm import Session
from app.core.delete_guard import assert_row_deletable_by_creator
from app.core.utils import generate_uuid
from app.models.models import SysDictData
@@ -22,6 +23,7 @@ def _row_to_dict(d: SysDictData) -> Dict[str, Any]:
"tenantNo": d.tenant_no,
"orderNum": d.order_num if d.order_num is not None else 0,
"remark": d.remark or "",
"createBy": d.create_by or "",
"createTime": d.create_time.strftime("%Y-%m-%d %H:%M:%S") if d.create_time else "",
"updateTime": d.update_time.strftime("%Y-%m-%d %H:%M:%S") if d.update_time else "",
}
@@ -139,10 +141,11 @@ def update_dict_data(
return obj
def soft_delete_dict_data(db: Session, data_id: str) -> None:
def soft_delete_dict_data(db: Session, data_id: str, *, user_info: dict = None) -> None:
obj = get_dict_data(db, data_id)
if not obj:
raise ValueError("字典数据不存在")
assert_row_deletable_by_creator(obj, user_info)
obj.del_flag = 1
obj.update_time = datetime.now()
db.add(obj)
+4 -1
View File
@@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import and_, func, or_
from sqlalchemy.orm import Session
from app.core.delete_guard import assert_row_deletable_by_creator
from app.core.utils import generate_uuid
from app.models.models import SysDictType, SysDictData
@@ -20,6 +21,7 @@ def _row_to_dict(t: SysDictType) -> Dict[str, Any]:
"tenantNo": t.tenant_no,
"orderNum": t.order_num if t.order_num is not None else 0,
"remark": t.remark or "",
"createBy": t.create_by or "",
"createTime": t.create_time.strftime("%Y-%m-%d %H:%M:%S") if t.create_time else "",
"updateTime": t.update_time.strftime("%Y-%m-%d %H:%M:%S") if t.update_time else "",
}
@@ -150,10 +152,11 @@ def update_dict_type(
return obj
def soft_delete_dict_type(db: Session, dict_type_id: str) -> None:
def soft_delete_dict_type(db: Session, dict_type_id: str, *, user_info: dict = None) -> None:
obj = get_dict_type(db, dict_type_id)
if not obj:
raise ValueError("字典类型不存在")
assert_row_deletable_by_creator(obj, user_info)
obj.del_flag = 1
obj.update_time = datetime.now()
db.add(obj)
+3 -1
View File
@@ -7,6 +7,7 @@ from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import and_, or_
from sqlalchemy.orm import Session
from app.core.delete_guard import assert_row_deletable_by_creator
from app.core.utils import generate_uuid
from app.models.models import BizLeaveRequest
@@ -138,10 +139,11 @@ def update_leave_status(db: Session, leave_id: str, status: str, wf_instance_id:
db.flush()
def soft_delete_leave(db: Session, leave_id: str, user_id: str = "") -> None:
def soft_delete_leave(db: Session, leave_id: str, user_id: str = "", *, user_info: dict = None) -> None:
row = db.query(BizLeaveRequest).filter(BizLeaveRequest.id == leave_id, BizLeaveRequest.del_flag == 0).first()
if not row:
return
assert_row_deletable_by_creator(row, user_info)
row.del_flag = 1
row.update_by = user_id
row.update_time = datetime.now()
+4 -1
View File
@@ -6,6 +6,7 @@ from typing import Any, Dict, List, Optional
from sqlalchemy import and_
from sqlalchemy.orm import Session
from app.core.delete_guard import assert_row_deletable_by_creator
from app.core.utils import generate_uuid
from app.models.models import SysMenu
@@ -25,6 +26,7 @@ def _row_to_dict(m: SysMenu) -> Dict[str, Any]:
"tenantNo": m.tenant_no,
"orderNum": m.order_num if m.order_num is not None else 0,
"remark": m.remark or "",
"createBy": m.create_by or "",
"createTime": m.create_time.strftime("%Y-%m-%d %H:%M:%S") if m.create_time else "",
"updateTime": m.update_time.strftime("%Y-%m-%d %H:%M:%S") if m.update_time else "",
}
@@ -230,10 +232,11 @@ def _cascade_soft_delete(db: Session, menu_id: str) -> None:
db.add(child)
def soft_delete_menu(db: Session, menu_id: str) -> None:
def soft_delete_menu(db: Session, menu_id: str, *, user_info: dict = None) -> None:
obj = get_menu(db, menu_id)
if not obj:
raise ValueError("菜单不存在")
assert_row_deletable_by_creator(obj, user_info)
_cascade_soft_delete(db, menu_id)
obj.del_flag = 1
obj.update_time = datetime.now()
+45 -74
View File
@@ -1,72 +1,17 @@
"""操作日志:基于 sys_api_request_log 适配 RuoYi OperLog 视图"""
from __future__ import annotations
import re
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import desc, func, or_
from sqlalchemy.orm import Session
from app.core.oper_log_infer import business_type_from_path, title_from_path
from app.models.models import SysApiRequestLog, SysUser
_FMT = "%Y-%m-%d %H:%M:%S"
# path 末段 -> sys_oper_type 字典值
_ACTION_TYPE_MAP = {
"add": 1,
"create": 1,
"insert": 1,
"update": 2,
"edit": 2,
"modify": 2,
"delete": 3,
"remove": 3,
"export": 5,
"import": 6,
"transfer": 2,
"intopool": 2,
"deal": 2,
"claim": 2,
"release": 2,
}
# path 片段 -> 模块标题
_MODULE_TITLE_RULES: List[Tuple[re.Pattern[str], str]] = [
(re.compile(r"/crm/customer", re.I), "CRM客户管理"),
(re.compile(r"/crm/contact", re.I), "CRM联系人"),
(re.compile(r"/crm/opportunity", re.I), "CRM商机管理"),
(re.compile(r"/crm/contract", re.I), "CRM合同管理"),
(re.compile(r"/crm/invoice-info", re.I), "CRM开票信息"),
(re.compile(r"/crm/", re.I), "CRM管理"),
(re.compile(r"/web/admin/", re.I), "系统管理"),
(re.compile(r"/web/auth/", re.I), "认证授权"),
]
def _title_from_path(path: str) -> str:
for pattern, title in _MODULE_TITLE_RULES:
if pattern.search(path or ""):
return title
parts = [p for p in (path or "").split("/") if p]
return parts[-1] if parts else "操作日志"
def _business_type_from_path(path: str, http_method: str) -> int:
parts = [p for p in (path or "").split("/") if p]
if parts:
last = parts[-1].lower()
if last in _ACTION_TYPE_MAP:
return _ACTION_TYPE_MAP[last]
method = (http_method or "").upper()
if method == "POST":
return 1
if method in ("PUT", "PATCH"):
return 2
if method == "DELETE":
return 3
return 0
def _oper_status(status_code: int) -> int:
return 0 if 200 <= int(status_code or 0) < 400 else 1
@@ -74,6 +19,7 @@ def _oper_status(status_code: int) -> int:
def _row_to_oper_log(row: Any) -> Dict[str, Any]:
path = getattr(row, "path", "") or ""
http_method = getattr(row, "http_method", "") or ""
status_code = int(getattr(row, "status_code", 0) or 0)
create_at = getattr(row, "create_at", None)
oper_time = create_at.strftime(_FMT) if isinstance(create_at, datetime) else ""
@@ -81,11 +27,19 @@ def _row_to_oper_log(row: Any) -> Dict[str, Any]:
request_body = getattr(row, "request_body", None) or ""
response_body = getattr(row, "response_body", None) or ""
stored_title = getattr(row, "business_title", None)
stored_type = getattr(row, "business_type", None)
title = stored_title or title_from_path(path)
business_type = stored_type if stored_type is not None else business_type_from_path(path, http_method)
oper_summary = getattr(row, "oper_summary", None) or ""
return {
"id": getattr(row, "id", ""),
"title": _title_from_path(path),
"businessType": _business_type_from_path(path, getattr(row, "http_method", "")),
"method": getattr(row, "http_method", "") or "",
"title": title,
"businessType": business_type,
"businessId": getattr(row, "business_id", None) or "",
"operSummary": oper_summary,
"method": http_method,
"operName": oper_name,
"operUrl": path,
"operIp": getattr(row, "ip_address", "") or "",
@@ -164,12 +118,33 @@ def list_oper_logs(
business_type: Optional[int] = None,
oper_name: Optional[str] = None,
status: Optional[int] = None,
business_id: Optional[str] = None,
business_title: Optional[str] = None,
user_info: Optional[dict] = None,
tenant_no: Optional[str] = None,
) -> Tuple[List[Dict[str, Any]], int]:
q = db.query(SysApiRequestLog).filter(SysApiRequestLog.is_del == 0)
effective_tenant = tenant_no or (user_info or {}).get("tenant_no")
if effective_tenant:
q = q.filter(SysApiRequestLog.tenant_no == effective_tenant)
if user_info:
q = _apply_oper_log_scope(q, db, user_info, tenant_no or user_info.get("tenant_no"))
q = _apply_oper_log_scope(q, db, user_info, effective_tenant or "")
if business_id:
q = q.filter(SysApiRequestLog.business_id == business_id.strip())
if business_title:
kw = f"%{business_title.strip()}%"
q = q.filter(SysApiRequestLog.business_title.like(kw))
if title:
title_kw = f"%{title.strip()}%"
q = q.filter(SysApiRequestLog.business_title.like(title_kw))
if business_type is not None:
q = q.filter(SysApiRequestLog.business_type == int(business_type))
if oper_name:
kw = f"%{oper_name.strip()}%"
@@ -191,21 +166,15 @@ def list_oper_logs(
)
)
# title / businessType 为衍生字段,先拉取再在内存过滤(数据量可控)
rows = q.order_by(desc(SysApiRequestLog.create_at)).limit(5000).all()
items = [_row_to_oper_log(r) for r in rows]
if title:
title_kw = title.strip().lower()
items = [i for i in items if title_kw in (i.get("title") or "").lower()]
if business_type is not None:
items = [i for i in items if i.get("businessType") == int(business_type)]
total = len(items)
total = int(q.with_entities(func.count(SysApiRequestLog.id)).order_by(None).scalar() or 0)
start = (max(page, 1) - 1) * page_size
end = start + page_size
return items[start:end], total
rows = (
q.order_by(desc(SysApiRequestLog.create_at))
.offset(start)
.limit(page_size)
.all()
)
return [_row_to_oper_log(r) for r in rows], total
def get_oper_log(
@@ -220,4 +189,6 @@ def get_oper_log(
return None
if user_info and not _oper_log_row_in_scope(db, row, user_info, tenant_no):
return None
if tenant_no and row.tenant_no and row.tenant_no != tenant_no:
return None
return _row_to_oper_log(row)
+4 -1
View File
@@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import and_, func, or_
from sqlalchemy.orm import Session
from app.core.delete_guard import assert_row_deletable_by_creator
from app.core.utils import generate_uuid
from app.models.models import SysPost
@@ -21,6 +22,7 @@ def _row_to_dict(p: SysPost) -> Dict[str, Any]:
"tenantNo": p.tenant_no,
"orderNum": p.order_num if p.order_num is not None else 0,
"remark": p.remark or "",
"createBy": p.create_by or "",
"createTime": p.create_time.strftime("%Y-%m-%d %H:%M:%S") if p.create_time else "",
"updateTime": p.update_time.strftime("%Y-%m-%d %H:%M:%S") if p.update_time else "",
}
@@ -150,10 +152,11 @@ def update_post(
return obj
def soft_delete_post(db: Session, post_id: str) -> None:
def soft_delete_post(db: Session, post_id: str, *, user_info: dict = None) -> None:
obj = get_post(db, post_id)
if not obj:
raise ValueError("职位不存在")
assert_row_deletable_by_creator(obj, user_info)
obj.del_flag = 1
obj.update_time = datetime.now()
db.add(obj)
+4 -1
View File
@@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import and_, func, or_
from sqlalchemy.orm import Session
from app.core.delete_guard import assert_row_deletable_by_creator
from app.core.utils import generate_uuid
from app.models.models import SysRole, SysRoleMenu, SysRoleDataScopeDetail
@@ -26,6 +27,7 @@ def _row_to_dict(r: SysRole) -> Dict[str, Any]:
"tenantNo": r.tenant_no or "",
"orderNum": r.order_num if r.order_num is not None else 0,
"remark": r.remark or "",
"createBy": r.create_by or "",
"createTime": r.create_time.strftime("%Y-%m-%d %H:%M:%S") if r.create_time else "",
"updateTime": r.update_time.strftime("%Y-%m-%d %H:%M:%S") if r.update_time else "",
}
@@ -173,10 +175,11 @@ def update_role(
return obj
def soft_delete_role(db: Session, role_id: str) -> None:
def soft_delete_role(db: Session, role_id: str, *, user_info: dict = None) -> None:
obj = get_role(db, role_id)
if not obj:
raise ValueError("角色不存在")
assert_row_deletable_by_creator(obj, user_info)
obj.del_flag = 1
obj.update_time = datetime.now()
db.add(obj)
+4 -1
View File
@@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import and_, func, or_
from sqlalchemy.orm import Session
from app.core.delete_guard import assert_row_deletable_by_creator
from app.core.utils import generate_uuid
from app.models.models import SysSite
@@ -26,6 +27,7 @@ def _row_to_dict(s: SysSite) -> Dict[str, Any]:
"tenantNo": s.tenant_no,
"orderNum": s.order_num if s.order_num is not None else 0,
"remark": s.remark or "",
"createBy": s.create_by or "",
"createTime": s.create_time.strftime("%Y-%m-%d %H:%M:%S") if s.create_time else "",
"updateTime": s.update_time.strftime("%Y-%m-%d %H:%M:%S") if s.update_time else "",
}
@@ -180,10 +182,11 @@ def update_site(
return obj
def soft_delete_site(db: Session, site_id: str) -> None:
def soft_delete_site(db: Session, site_id: str, *, user_info: dict = None) -> None:
obj = get_site(db, site_id)
if not obj:
raise ValueError("站点不存在")
assert_row_deletable_by_creator(obj, user_info)
obj.del_flag = 1
obj.update_time = datetime.now()
db.add(obj)
+4 -1
View File
@@ -9,6 +9,7 @@ from sqlalchemy import and_, func, or_
from sqlalchemy.orm import Session
from app.core.config import settings
from app.core.delete_guard import assert_row_deletable_by_creator
from app.core.utils import generate_uuid
from app.models.models import SysTenant
@@ -22,6 +23,7 @@ def _row_to_dict(t: SysTenant) -> Dict[str, Any]:
"contactPhone": t.contact_phone or "",
"orderNum": t.order_num if t.order_num is not None else 0,
"remark": t.remark or "",
"createBy": t.create_by or "",
"createTime": t.create_time.strftime("%Y-%m-%d %H:%M:%S") if t.create_time else "",
"updateTime": t.update_time.strftime("%Y-%m-%d %H:%M:%S") if t.update_time else "",
}
@@ -153,12 +155,13 @@ def update_tenant(
return obj
def soft_delete_tenant(db: Session, tenant_id: str) -> None:
def soft_delete_tenant(db: Session, tenant_id: str, *, user_info: dict = None) -> None:
obj = get_tenant(db, tenant_id)
if not obj:
raise ValueError("租户不存在")
if obj.tenant_no == settings.DEFAULT_TENANT_NO:
raise ValueError("默认租户不可删除")
assert_row_deletable_by_creator(obj, user_info)
obj.del_flag = 1
obj.update_time = datetime.now()
db.add(obj)
+4 -1
View File
@@ -10,6 +10,7 @@ from sqlalchemy import and_, func, or_
from sqlalchemy.orm import Session
from app.core.config import settings
from app.core.delete_guard import assert_row_deletable_by_creator
from app.core.utils import generate_uuid
from app.crud.base import CRUDBase
from app.models.models import SysUser, SysUserRole, SysUserPost, SysDept
@@ -64,6 +65,7 @@ def _row_to_dict(u: SysUser, dept_name: str = "") -> Dict[str, Any]:
"tenantNo": u.tenant_no,
"orderNum": u.order_num if u.order_num is not None else 0,
"remark": u.remark or "",
"createBy": u.create_by or "",
"createTime": u.create_time.strftime("%Y-%m-%d %H:%M:%S") if u.create_time else "",
"updateTime": u.update_time.strftime("%Y-%m-%d %H:%M:%S") if u.update_time else "",
"deptName": dept_name,
@@ -413,10 +415,11 @@ def update_user(
return obj
def soft_delete_user(db: Session, user_id: str) -> None:
def soft_delete_user(db: Session, user_id: str, *, user_info: dict = None) -> None:
obj = get_user(db, user_id)
if not obj:
raise ValueError("用户不存在")
assert_row_deletable_by_creator(obj, user_info)
obj.del_flag = 1
obj.update_time = datetime.now()
db.add(obj)
+4 -1
View File
@@ -9,6 +9,7 @@ from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import and_, func, or_
from sqlalchemy.orm import Session
from app.core.delete_guard import assert_row_deletable_by_creator
from app.core.utils import generate_uuid
from app.models.models import (
SysUser,
@@ -35,6 +36,7 @@ def _row_to_dict(t: WfProcessDef) -> Dict[str, Any]:
"tenantNo": t.tenant_no,
"orderNum": t.order_num if t.order_num is not None else 0,
"remark": t.remark or "",
"createBy": t.create_by or "",
"createTime": t.create_time.strftime("%Y-%m-%d %H:%M:%S") if t.create_time else "",
"updateTime": t.update_time.strftime("%Y-%m-%d %H:%M:%S") if t.update_time else "",
}
@@ -191,10 +193,11 @@ def update_process_def(
return obj
def soft_delete_process_def(db: Session, def_id: str) -> None:
def soft_delete_process_def(db: Session, def_id: str, *, user_info: dict = None) -> None:
obj = get_process_def(db, def_id)
if not obj:
raise ValueError("流程定义不存在")
assert_row_deletable_by_creator(obj, user_info)
obj.del_flag = 1
obj.update_time = datetime.now()
db.add(obj)
+10
View File
@@ -369,6 +369,11 @@ class SysApiRequestLog(Base):
role = Column(String(64), nullable=True, comment="角色")
ip_address = Column(String(50), nullable=True, comment="IP地址")
user_agent = Column(String(500), nullable=True, comment="User-Agent")
tenant_no = Column(String(64), nullable=True, comment="租户编号")
business_title = Column(String(100), nullable=True, comment="操作模块")
business_type = Column(Integer, nullable=True, comment="操作类型")
business_id = Column(String(64), nullable=True, comment="业务主键")
oper_summary = Column(String(500), nullable=True, comment="操作摘要")
is_del = Column(Integer, default=0, comment="是否删除")
create_at = Column(DateTime, default=datetime.now, comment="创建时间")
create_by = Column(String(32), nullable=True, comment="创建人ID")
@@ -396,6 +401,11 @@ class SysApiRequestLogHis(Base):
role = Column(String(64), nullable=True, comment="角色")
ip_address = Column(String(50), nullable=True, comment="IP地址")
user_agent = Column(String(500), nullable=True, comment="User-Agent")
tenant_no = Column(String(64), nullable=True, comment="租户编号")
business_title = Column(String(100), nullable=True, comment="操作模块")
business_type = Column(Integer, nullable=True, comment="操作类型")
business_id = Column(String(64), nullable=True, comment="业务主键")
oper_summary = Column(String(500), nullable=True, comment="操作摘要")
create_at = Column(DateTime, default=datetime.now, comment="创建时间")
create_by = Column(String(32), nullable=True, comment="创建人ID")
create_name = Column(String(100), nullable=True, comment="创建人姓名")
+163
View File
@@ -35,6 +35,14 @@ class PlCarrier(Base):
contact_name = Column(String(100), nullable=True, comment="联系人")
contact_phone = Column(String(32), nullable=True, comment="联系电话")
status = Column(CHAR(1), nullable=False, default="1", comment="合作状态 1-合作中 0-已终止")
business_license_urls = Column(String(2000), nullable=True, comment="营业执照附件URL,逗号分隔")
road_transport_license_urls = Column(String(2000), nullable=True, comment="道路经营许可证附件URL,逗号分隔")
transport_agreement_urls = Column(String(2000), nullable=True, comment="运输协议附件URL,逗号分隔")
transport_agreement_expire_date = Column(Date, nullable=True, comment="运输协议有效期")
audit_status = Column(String(20), nullable=False, default="pending", comment="审核状态 pending-待审核 approved-已通过 rejected-已驳回")
audit_by = Column(String(64), nullable=True, comment="审核人id")
audit_time = Column(DateTime, nullable=True, comment="审核时间")
audit_remark = Column(String(512), 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="备注")
@@ -102,6 +110,7 @@ class PlCert(Base):
id = Column(String(64), primary_key=True, comment="主键ID")
owner_type = Column(String(20), nullable=False, comment="归属类型 vehicle-车辆 personnel-人员")
owner_id = Column(String(64), nullable=False, comment="归属主体ID")
vehicle_part = Column(String(20), nullable=True, comment="车辆部位 head-主车 trailer-挂车,人员证书为空")
cert_name = Column(String(100), nullable=False, comment="证书名称")
cert_no = Column(String(64), nullable=True, comment="证号")
expire_date = Column(Date, nullable=True, comment="有效期")
@@ -737,3 +746,157 @@ class PlDocumentReminder(Base):
update_by = Column(String(64), nullable=True, comment="更新人id")
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记:0-正常 1-已删除")
class PlVehicleRechargeApply(Base):
"""车辆ETC/加油充值申请 pl_vehicle_recharge_apply"""
__tablename__ = "pl_vehicle_recharge_apply"
id = Column(String(64), primary_key=True, comment="主键ID")
apply_no = Column(String(32), nullable=False, unique=True, comment="申请单号")
license_plate = Column(String(20), nullable=False, comment="车牌号")
recharge_type = Column(String(20), nullable=False, comment="充值类型 etc/fuel")
apply_date = Column(Date, nullable=False, comment="申请日期")
odometer_km = Column(DECIMAL(10, 1), nullable=True, comment="期初公里数")
attachment_urls = Column(String(2000), nullable=True, comment="附件URL")
applicant_id = Column(String(64), nullable=False, comment="申请人ID")
status = Column(String(20), nullable=False, default="pending", 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="备注")
create_by = Column(String(64), nullable=True, comment="创建人id")
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
update_by = Column(String(64), nullable=True, comment="更新人id")
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记:0-正常 1-已删除")
class PlVehicleMaintenanceApply(Base):
"""车辆维保申请 pl_vehicle_maintenance_apply"""
__tablename__ = "pl_vehicle_maintenance_apply"
id = Column(String(64), primary_key=True, comment="主键ID")
apply_no = Column(String(32), nullable=False, unique=True, comment="申请单号")
license_plate = Column(String(20), nullable=False, comment="车牌号")
applicant_id = Column(String(64), nullable=False, comment="申请人ID")
apply_date = Column(Date, nullable=False, comment="申请时间")
maintenance_type = Column(String(64), nullable=False, comment="维保类型")
start_date = Column(Date, nullable=False, comment="维保开始时间")
expected_pickup_date = Column(Date, nullable=True, comment="预计取车时间")
maintenance_content = Column(String(1000), nullable=True, comment="维保内容")
shop_address = Column(String(500), nullable=True, comment="维保店铺地址")
attachment_urls = Column(String(2000), nullable=True, comment="附件URL")
status = Column(String(20), nullable=False, default="pending", 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="备注")
create_by = Column(String(64), nullable=True, comment="创建人id")
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
update_by = Column(String(64), nullable=True, comment="更新人id")
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记:0-正常 1-已删除")
class PlVehicleMaintenanceProcess(Base):
"""车辆维保处理登记 pl_vehicle_maintenance_process"""
__tablename__ = "pl_vehicle_maintenance_process"
id = Column(String(64), primary_key=True, comment="主键ID")
process_no = Column(String(32), nullable=False, unique=True, comment="处理单号")
apply_id = Column(String(64), nullable=True, unique=True, comment="关联申请单ID")
license_plate = Column(String(20), nullable=True, comment="车牌号冗余")
amount = Column(DECIMAL(10, 2), nullable=True, comment="金额")
handler_id = Column(String(64), nullable=False, comment="处理人ID")
process_date = Column(Date, nullable=False, comment="处理时间")
attachment_urls = Column(String(2000), nullable=True, comment="附件URL")
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="备注")
create_by = Column(String(64), nullable=True, comment="创建人id")
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
update_by = Column(String(64), nullable=True, comment="更新人id")
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记:0-正常 1-已删除")
class PlVehicleUreaApply(Base):
"""车辆尿素申请 pl_vehicle_urea_apply"""
__tablename__ = "pl_vehicle_urea_apply"
id = Column(String(64), primary_key=True, comment="主键ID")
apply_no = Column(String(32), nullable=False, unique=True, comment="申请单号")
license_plate = Column(String(20), nullable=False, comment="车牌号")
applicant_id = Column(String(64), nullable=False, comment="申请人ID")
apply_date = Column(Date, nullable=False, comment="申请时间")
warehouse_in_date = Column(Date, nullable=False, comment="入库时间")
unit = Column(String(32), nullable=False, comment="单位")
quantity = Column(DECIMAL(10, 2), nullable=False, comment="入库数")
apply_amount = Column(DECIMAL(10, 2), nullable=False, comment="申请金额")
attachment_urls = Column(String(2000), nullable=True, comment="附件URL")
status = Column(String(20), nullable=False, default="pending", 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="备注")
create_by = Column(String(64), nullable=True, comment="创建人id")
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
update_by = Column(String(64), nullable=True, comment="更新人id")
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记:0-正常 1-已删除")
class PlVehicleUreaDisburse(Base):
"""车辆尿素发放登记 pl_vehicle_urea_disburse"""
__tablename__ = "pl_vehicle_urea_disburse"
id = Column(String(64), primary_key=True, comment="主键ID")
disburse_no = Column(String(32), nullable=False, unique=True, comment="发放单号")
apply_id = Column(String(64), nullable=True, unique=True, comment="关联申请单ID")
license_plate = Column(String(20), nullable=True, comment="车牌号冗余")
amount = Column(DECIMAL(10, 2), nullable=False, comment="发放金额")
handler_id = Column(String(64), nullable=False, comment="处理人ID")
disburse_date = Column(Date, nullable=False, comment="发放时间")
attachment_urls = Column(String(2000), nullable=True, comment="附件URL")
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="备注")
create_by = Column(String(64), nullable=True, comment="创建人id")
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
update_by = Column(String(64), nullable=True, comment="更新人id")
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记:0-正常 1-已删除")
class PlVehicleRechargeRecord(Base):
"""车辆ETC/加油充值发放登记 pl_vehicle_recharge_record"""
__tablename__ = "pl_vehicle_recharge_record"
id = Column(String(64), primary_key=True, comment="主键ID")
recharge_order_no = Column(String(32), nullable=False, unique=True, comment="充值订单号")
recharge_date = Column(Date, nullable=False, comment="充值日期")
recharge_type = Column(String(20), nullable=False, comment="充值类型")
card_no = Column(String(64), nullable=True, comment="卡号")
gas_station = Column(String(200), nullable=True, comment="加油站/渠道")
amount = Column(DECIMAL(10, 2), nullable=False, comment="充值金额")
attachment_urls = Column(String(2000), nullable=True, comment="附件URL")
apply_id = Column(String(64), nullable=True, unique=True, comment="关联申请单ID")
license_plate = Column(String(20), 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="备注")
create_by = Column(String(64), nullable=True, comment="创建人id")
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
update_by = Column(String(64), nullable=True, comment="更新人id")
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记:0-正常 1-已删除")
+1
View File
@@ -57,6 +57,7 @@ class AdvanceApplyOut(BaseModel):
status: str
current_node: Optional[str] = None
apply_time: DatetimeToStr = Field(validation_alias="create_time")
create_by: Optional[str] = Field(default=None, validation_alias="create_by")
remark: Optional[str] = None
actual_amount: Optional[float] = None
offset_amount: Optional[float] = None
+1
View File
@@ -57,6 +57,7 @@ class ExpenseItemOut(BaseModel):
sort_order: int
status: int
remark: Optional[str] = None
create_by: Optional[str] = None
child_count: int = 0
children: Optional[List["ExpenseItemOut"]] = None
+1
View File
@@ -77,6 +77,7 @@ class ReimburseApplyOut(BaseModel):
amount: float
current_node: Optional[str] = None
apply_time: DatetimeToStr = Field(validation_alias="create_time")
create_by: Optional[str] = Field(default=None, validation_alias="create_by")
status: Optional[str] = None
remark: Optional[str] = None
invoice_count: int = 0
+1
View File
@@ -59,4 +59,5 @@ class WorkflowDefOut(BaseModel):
biz_type: str
wf_name: str
status: int
create_by: Optional[str] = None
nodes: List[WorkflowNodeOut] = []
+2
View File
@@ -12,6 +12,8 @@ class OperLogListBody(BaseModel):
page_size: int = Field(default=10, alias="pageSize", ge=1, le=500, description="每页条数")
title: Optional[str] = Field(default=None, description="操作模块")
business_type: Optional[int] = Field(default=None, alias="businessType", description="操作类型")
business_id: Optional[str] = Field(default=None, alias="businessId", description="业务主键")
business_title: Optional[str] = Field(default=None, alias="businessTitle", description="操作模块模糊")
oper_name: Optional[str] = Field(default=None, alias="operName", description="操作人员")
status: Optional[int] = Field(default=None, description="操作状态 0成功 1失败")
+25
View File
@@ -1,6 +1,7 @@
"""运输计划管理 - Pydantic Schema"""
from app.schemas.pl.base_data import (
CarrierBase,
CarrierAuditReject,
CarrierCreate,
CarrierRead,
CarrierUpdate,
@@ -23,6 +24,7 @@ from app.schemas.pl.base_data import (
VehiclePersonnelRow,
VehicleRead,
VehicleUpdate,
carrier_to_read,
)
from app.schemas.pl.plan import (
PlanBase,
@@ -84,6 +86,17 @@ from app.schemas.pl.inventory import (
TransportStatsItem,
TransportStatsResponse,
)
from app.schemas.pl.vehicle_recharge import (
VehicleRechargeApplyCreate,
VehicleRechargeApplyUpdate,
VehicleRechargeApplyRead,
VehicleRechargeApplyListResponse,
VehicleRechargeApplyOption,
VehicleRechargeRecordCreate,
VehicleRechargeRecordUpdate,
VehicleRechargeRecordRead,
VehicleRechargeRecordListResponse,
)
__all__ = [
# Base data
@@ -94,9 +107,11 @@ __all__ = [
"VehiclePersonnelRow",
"CertRow",
"CertRead",
"CarrierAuditReject",
"CarrierCreate",
"CarrierUpdate",
"CarrierRead",
"carrier_to_read",
"VehicleCreate",
"VehicleUpdate",
"VehicleRead",
@@ -166,4 +181,14 @@ __all__ = [
"TransportStatsQuery",
"TransportStatsItem",
"TransportStatsResponse",
# Vehicle recharge
"VehicleRechargeApplyCreate",
"VehicleRechargeApplyUpdate",
"VehicleRechargeApplyRead",
"VehicleRechargeApplyListResponse",
"VehicleRechargeApplyOption",
"VehicleRechargeRecordCreate",
"VehicleRechargeRecordUpdate",
"VehicleRechargeRecordRead",
"VehicleRechargeRecordListResponse",
]
+127 -3
View File
@@ -3,7 +3,12 @@ from datetime import date, datetime
from decimal import Decimal
from typing import Optional
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, model_validator
from app.services.pl_carrier_service import (
carrier_agreement_expired,
carrier_qualification_complete,
)
# =============================================================================
@@ -19,9 +24,42 @@ class CarrierBase(BaseModel):
remark: Optional[str] = Field(default=None, description="备注")
class CarrierCreate(CarrierBase):
class CarrierQualificationBase(BaseModel):
business_license_urls: Optional[str] = Field(
default=None, max_length=2000, alias="businessLicenseUrls", description="营业执照附件URL"
)
road_transport_license_urls: Optional[str] = Field(
default=None,
max_length=2000,
alias="roadTransportLicenseUrls",
description="道路经营许可证附件URL",
)
transport_agreement_urls: Optional[str] = Field(
default=None, max_length=2000, alias="transportAgreementUrls", description="运输协议附件URL"
)
transport_agreement_expire_date: Optional[date] = Field(
default=None, alias="transportAgreementExpireDate", description="运输协议有效期"
)
class CarrierCreate(CarrierBase, CarrierQualificationBase):
business_license_urls: str = Field(..., max_length=2000, alias="businessLicenseUrls")
road_transport_license_urls: str = Field(..., max_length=2000, alias="roadTransportLicenseUrls")
transport_agreement_urls: str = Field(..., max_length=2000, alias="transportAgreementUrls")
transport_agreement_expire_date: date = Field(..., alias="transportAgreementExpireDate")
model_config = {"populate_by_name": True}
@model_validator(mode="after")
def validate_qualification(self) -> "CarrierCreate":
if not self.business_license_urls or not str(self.business_license_urls).strip():
raise ValueError("请上传营业执照")
if not self.road_transport_license_urls or not str(self.road_transport_license_urls).strip():
raise ValueError("请上传道路经营许可证")
if not self.transport_agreement_urls or not str(self.transport_agreement_urls).strip():
raise ValueError("请上传运输协议")
return self
class CarrierUpdate(BaseModel):
carrier_name: Optional[str] = Field(default=None, max_length=200, alias="carrierName")
@@ -29,11 +67,61 @@ class CarrierUpdate(BaseModel):
contact_phone: Optional[str] = Field(default=None, max_length=32, alias="contactPhone")
status: Optional[str] = Field(default=None, max_length=1)
remark: Optional[str] = None
business_license_urls: Optional[str] = Field(
default=None, max_length=2000, alias="businessLicenseUrls"
)
road_transport_license_urls: Optional[str] = Field(
default=None, max_length=2000, alias="roadTransportLicenseUrls"
)
transport_agreement_urls: Optional[str] = Field(
default=None, max_length=2000, alias="transportAgreementUrls"
)
transport_agreement_expire_date: Optional[date] = Field(
default=None, alias="transportAgreementExpireDate"
)
model_config = {"populate_by_name": True}
@model_validator(mode="after")
def validate_qualification_patch(self) -> "CarrierUpdate":
fields = (
self.business_license_urls,
self.road_transport_license_urls,
self.transport_agreement_urls,
self.transport_agreement_expire_date,
)
if not any(item is not None for item in fields):
return self
if self.business_license_urls is not None and not str(self.business_license_urls).strip():
raise ValueError("请上传营业执照")
if (
self.road_transport_license_urls is not None
and not str(self.road_transport_license_urls).strip()
):
raise ValueError("请上传道路经营许可证")
if self.transport_agreement_urls is not None and not str(self.transport_agreement_urls).strip():
raise ValueError("请上传运输协议")
if self.transport_agreement_expire_date is None and any(
item is not None for item in fields[:3]
):
raise ValueError("请填写运输协议有效期")
return self
class CarrierAuditReject(BaseModel):
remark: Optional[str] = Field(default=None, max_length=512, description="驳回原因")
model_config = {"populate_by_name": True}
class CarrierRead(CarrierBase):
class CarrierRead(CarrierBase, CarrierQualificationBase):
id: str
audit_status: str = Field(default="pending", alias="auditStatus")
audit_by: Optional[str] = Field(default=None, alias="auditBy")
audit_time: Optional[datetime] = Field(default=None, alias="auditTime")
audit_remark: Optional[str] = Field(default=None, alias="auditRemark")
qualification_complete: bool = Field(default=False, alias="qualificationComplete")
transport_agreement_expired: bool = Field(default=False, alias="transportAgreementExpired")
tenant_no: str = Field(alias="tenantNo")
order_num: Optional[int] = Field(default=None, alias="orderNum")
create_by: Optional[str] = Field(default=None, alias="createBy")
@@ -45,6 +133,37 @@ class CarrierRead(CarrierBase):
model_config = {"from_attributes": True, "populate_by_name": True}
def carrier_to_read(carrier, today: Optional[date] = None) -> CarrierRead:
complete = carrier_qualification_complete(carrier)
expired = carrier_agreement_expired(carrier, today) if complete else False
return CarrierRead(
id=carrier.id,
carrier_name=carrier.carrier_name,
contact_name=carrier.contact_name,
contact_phone=carrier.contact_phone,
status=carrier.status,
remark=carrier.remark,
business_license_urls=carrier.business_license_urls,
road_transport_license_urls=carrier.road_transport_license_urls,
transport_agreement_urls=carrier.transport_agreement_urls,
transport_agreement_expire_date=carrier.transport_agreement_expire_date,
audit_status=carrier.audit_status or "pending",
audit_by=carrier.audit_by,
audit_time=carrier.audit_time,
audit_remark=carrier.audit_remark,
qualification_complete=complete,
transport_agreement_expired=expired,
tenant_no=carrier.tenant_no,
order_num=carrier.order_num,
create_by=carrier.create_by,
create_dept_id=carrier.create_dept_id,
create_time=carrier.create_time,
update_by=carrier.update_by,
update_time=carrier.update_time,
del_flag=carrier.del_flag,
)
# =============================================================================
# 子表行 Schema
# =============================================================================
@@ -63,6 +182,11 @@ class VehiclePersonnelRow(BaseModel):
class CertRow(BaseModel):
cert_id: Optional[str] = Field(default=None, alias="certId")
vehicle_part: Optional[str] = Field(
default=None,
alias="vehiclePart",
description="车辆部位 head-主车 trailer-挂车,人员证书为空",
)
cert_name: str = Field(..., alias="certName")
cert_no: Optional[str] = Field(default=None, alias="certNo")
expire_date: Optional[date] = Field(default=None, alias="expireDate")
+11
View File
@@ -206,3 +206,14 @@ class FreightSettlementListQuery(BaseModel):
class FreightSettlementListResponse(BaseModel):
total: int
list: list[FreightSettlementListItem]
class FreightSettlementByTransportRead(BaseModel):
id: str
settlement_no: str = Field(alias="settlementNo")
invoice_amount: Optional[Decimal] = Field(default=None, alias="invoiceAmount")
invoice_no: Optional[str] = Field(default=None, alias="invoiceNo")
invoice_date: Optional[date] = Field(default=None, alias="invoiceDate")
invoice_attachment_urls: Optional[str] = Field(default=None, alias="invoiceAttachmentUrls")
settlement_status: str = Field(alias="settlementStatus")
model_config = {"populate_by_name": True}
+1 -1
View File
@@ -129,7 +129,7 @@ class PlanTransportCreateBlock(BaseModel):
transport_qty: Optional[Decimal] = Field(default=None, alias="transportQty", description="运输量")
freight_price: Optional[Decimal] = Field(default=None, alias="freightPrice", description="运输单价")
freight_amount: Optional[Decimal] = Field(default=None, alias="freightAmount", description="运费金额")
has_invoice: int = Field(default=0, alias="hasInvoice", description="是否开票")
has_invoice: Optional[int] = Field(default=None, alias="hasInvoice", description="是否开票(已废弃,由运费结算维护)")
remark: Optional[str] = None
model_config = {"populate_by_name": True}
+1 -1
View File
@@ -58,7 +58,7 @@ class TransportArrange(BaseModel):
freight_type: str = Field(default="by_weight", alias="freightType", description="运费计算方式 by_weight/by_trip")
freight_price: Optional[Decimal] = Field(default=None, alias="freightPrice", description="运输单价")
freight_amount: Optional[Decimal] = Field(default=None, alias="freightAmount", description="运费金额")
has_invoice: int = Field(default=0, alias="hasInvoice", description="是否开票")
has_invoice: Optional[int] = Field(default=None, alias="hasInvoice", description="是否开票(已废弃,由运费结算维护)")
remark: Optional[str] = None
generate_dibang_plan: bool = Field(default=False, alias="generateDibangPlan", description="是否生成地磅单")
model_config = {"populate_by_name": True}
@@ -0,0 +1,153 @@
"""车辆维保 Schema"""
from datetime import date, datetime
from decimal import Decimal
from typing import Optional
from pydantic import BaseModel, Field, field_validator
class VehicleMaintenanceApplyCreate(BaseModel):
license_plate: str = Field(..., alias="licensePlate")
applicant_id: Optional[str] = Field(default=None, alias="applicantId")
apply_date: date = Field(..., alias="applyDate")
maintenance_type: str = Field(..., alias="maintenanceType")
start_date: date = Field(..., alias="startDate")
expected_pickup_date: Optional[date] = Field(default=None, alias="expectedPickupDate")
maintenance_content: Optional[str] = Field(default=None, alias="maintenanceContent")
shop_address: Optional[str] = Field(default=None, alias="shopAddress")
attachment_urls: Optional[str] = Field(default=None, alias="attachmentUrls")
remark: Optional[str] = None
model_config = {"populate_by_name": True}
class VehicleMaintenanceApplyUpdate(VehicleMaintenanceApplyCreate):
pass
class VehicleMaintenanceApplyRead(BaseModel):
id: str
apply_no: str = Field(..., alias="applyNo")
license_plate: str = Field(..., alias="licensePlate")
applicant_id: str = Field(..., alias="applicantId")
applicant_name: Optional[str] = Field(default=None, alias="applicantName")
apply_date: date = Field(..., alias="applyDate")
maintenance_type: str = Field(..., alias="maintenanceType")
start_date: date = Field(..., alias="startDate")
expected_pickup_date: Optional[date] = Field(default=None, alias="expectedPickupDate")
maintenance_content: Optional[str] = Field(default=None, alias="maintenanceContent")
shop_address: Optional[str] = Field(default=None, alias="shopAddress")
attachment_urls: Optional[str] = Field(default=None, alias="attachmentUrls")
status: str
remark: Optional[str] = None
create_time: Optional[datetime] = Field(default=None, alias="createTime")
update_time: Optional[datetime] = Field(default=None, alias="updateTime")
model_config = {"populate_by_name": True}
class VehicleMaintenanceApplyListResponse(BaseModel):
items: list[VehicleMaintenanceApplyRead]
total: int
page: int
page_size: int = Field(..., alias="pageSize")
model_config = {"populate_by_name": True}
class VehicleMaintenanceApplyReminder(BaseModel):
id: str
apply_no: str = Field(..., alias="applyNo")
license_plate: str = Field(..., alias="licensePlate")
maintenance_type: str = Field(..., alias="maintenanceType")
apply_date: date = Field(..., alias="applyDate")
start_date: date = Field(..., alias="startDate")
expected_pickup_date: Optional[date] = Field(default=None, alias="expectedPickupDate")
maintenance_content: Optional[str] = Field(default=None, alias="maintenanceContent")
shop_address: Optional[str] = Field(default=None, alias="shopAddress")
model_config = {"populate_by_name": True}
class VehicleMaintenanceProcessCreate(BaseModel):
apply_id: Optional[str] = Field(default=None, alias="applyId")
license_plate: Optional[str] = Field(default=None, alias="licensePlate")
amount: Optional[Decimal] = None
handler_id: Optional[str] = Field(default=None, alias="handlerId")
process_date: date = Field(..., alias="processDate")
attachment_urls: Optional[str] = Field(default=None, alias="attachmentUrls")
remark: Optional[str] = None
model_config = {"populate_by_name": True}
@field_validator("amount")
@classmethod
def validate_amount(cls, v: Optional[Decimal]) -> Optional[Decimal]:
if v is not None and v <= 0:
raise ValueError("金额须大于 0")
return v
class VehicleMaintenanceProcessUpdate(VehicleMaintenanceProcessCreate):
pass
class VehicleMaintenanceProcessRead(BaseModel):
id: str
process_no: str = Field(..., alias="processNo")
apply_id: Optional[str] = Field(default=None, alias="applyId")
apply_no: Optional[str] = Field(default=None, alias="applyNo")
license_plate: Optional[str] = Field(default=None, alias="licensePlate")
amount: Optional[Decimal] = None
handler_id: str = Field(..., alias="handlerId")
handler_name: Optional[str] = Field(default=None, alias="handlerName")
process_date: date = Field(..., alias="processDate")
attachment_urls: Optional[str] = Field(default=None, alias="attachmentUrls")
remark: Optional[str] = None
create_by: Optional[str] = Field(default=None, alias="createBy")
create_by_name: Optional[str] = Field(default=None, alias="createByName")
create_time: Optional[datetime] = Field(default=None, alias="createTime")
update_time: Optional[datetime] = Field(default=None, alias="updateTime")
model_config = {"populate_by_name": True}
class VehicleMaintenanceProcessListResponse(BaseModel):
items: list[VehicleMaintenanceProcessRead]
total: int
page: int
page_size: int = Field(..., alias="pageSize")
model_config = {"populate_by_name": True}
class VehicleMaintenanceProcessBoardItem(BaseModel):
apply_id: str = Field(..., alias="applyId")
apply_no: str = Field(..., alias="applyNo")
license_plate: str = Field(..., alias="licensePlate")
maintenance_type: str = Field(..., alias="maintenanceType")
apply_date: date = Field(..., alias="applyDate")
start_date: date = Field(..., alias="startDate")
expected_pickup_date: Optional[date] = Field(default=None, alias="expectedPickupDate")
maintenance_content: Optional[str] = Field(default=None, alias="maintenanceContent")
shop_address: Optional[str] = Field(default=None, alias="shopAddress")
applicant_name: Optional[str] = Field(default=None, alias="applicantName")
apply_status: str = Field(..., alias="applyStatus")
apply_remark: Optional[str] = Field(default=None, alias="applyRemark")
process_id: Optional[str] = Field(default=None, alias="processId")
process_no: Optional[str] = Field(default=None, alias="processNo")
process_date: Optional[date] = Field(default=None, alias="processDate")
amount: Optional[Decimal] = None
handler_name: Optional[str] = Field(default=None, alias="handlerName")
process_remark: Optional[str] = Field(default=None, alias="processRemark")
model_config = {"populate_by_name": True}
class VehicleMaintenanceProcessBoardListResponse(BaseModel):
items: list[VehicleMaintenanceProcessBoardItem]
total: int
page: int
page_size: int = Field(..., alias="pageSize")
model_config = {"populate_by_name": True}
+162
View File
@@ -0,0 +1,162 @@
"""车辆 ETC/加油充值 Schema"""
from datetime import date, datetime
from decimal import Decimal
from typing import Optional
from pydantic import BaseModel, Field, field_validator
RECHARGE_TYPES = {"etc", "fuel"}
class VehicleRechargeApplyCreate(BaseModel):
license_plate: str = Field(..., alias="licensePlate")
recharge_type: str = Field(..., alias="rechargeType")
apply_date: date = Field(..., alias="applyDate")
odometer_km: Optional[Decimal] = Field(default=None, alias="odometerKm")
attachment_urls: Optional[str] = Field(default=None, alias="attachmentUrls")
applicant_id: Optional[str] = Field(default=None, alias="applicantId")
remark: Optional[str] = None
model_config = {"populate_by_name": True}
@field_validator("recharge_type")
@classmethod
def validate_recharge_type(cls, v: str) -> str:
if v not in RECHARGE_TYPES:
raise ValueError("充值类型无效")
return v
class VehicleRechargeApplyUpdate(VehicleRechargeApplyCreate):
pass
class VehicleRechargeApplyRead(BaseModel):
id: str
apply_no: str = Field(..., alias="applyNo")
license_plate: str = Field(..., alias="licensePlate")
recharge_type: str = Field(..., alias="rechargeType")
apply_date: date = Field(..., alias="applyDate")
odometer_km: Optional[Decimal] = Field(default=None, alias="odometerKm")
attachment_urls: Optional[str] = Field(default=None, alias="attachmentUrls")
applicant_id: str = Field(..., alias="applicantId")
applicant_name: Optional[str] = Field(default=None, alias="applicantName")
status: str
remark: Optional[str] = None
create_time: Optional[datetime] = Field(default=None, alias="createTime")
update_time: Optional[datetime] = Field(default=None, alias="updateTime")
model_config = {"populate_by_name": True}
class VehicleRechargeApplyListResponse(BaseModel):
items: list[VehicleRechargeApplyRead]
total: int
page: int
page_size: int = Field(..., alias="pageSize")
model_config = {"populate_by_name": True}
class VehicleRechargeApplyOption(BaseModel):
id: str
apply_no: str = Field(..., alias="applyNo")
license_plate: str = Field(..., alias="licensePlate")
recharge_type: str = Field(..., alias="rechargeType")
model_config = {"populate_by_name": True}
class VehicleRechargeRecordCreate(BaseModel):
recharge_date: date = Field(..., alias="rechargeDate")
recharge_type: str = Field(..., alias="rechargeType")
card_no: Optional[str] = Field(default=None, alias="cardNo")
gas_station: Optional[str] = Field(default=None, alias="gasStation")
amount: Decimal
attachment_urls: Optional[str] = Field(default=None, alias="attachmentUrls")
apply_id: Optional[str] = Field(default=None, alias="applyId")
license_plate: Optional[str] = Field(default=None, alias="licensePlate")
remark: Optional[str] = None
model_config = {"populate_by_name": True}
@field_validator("recharge_type")
@classmethod
def validate_recharge_type(cls, v: str) -> str:
if v not in RECHARGE_TYPES:
raise ValueError("充值类型无效")
return v
@field_validator("amount")
@classmethod
def validate_amount(cls, v: Decimal) -> Decimal:
if v is None or v <= 0:
raise ValueError("充值金额须大于 0")
return v
class VehicleRechargeRecordUpdate(VehicleRechargeRecordCreate):
pass
class VehicleRechargeRecordRead(BaseModel):
id: str
recharge_order_no: str = Field(..., alias="rechargeOrderNo")
recharge_date: date = Field(..., alias="rechargeDate")
recharge_type: str = Field(..., alias="rechargeType")
card_no: Optional[str] = Field(default=None, alias="cardNo")
gas_station: Optional[str] = Field(default=None, alias="gasStation")
amount: Decimal
attachment_urls: Optional[str] = Field(default=None, alias="attachmentUrls")
apply_id: Optional[str] = Field(default=None, alias="applyId")
apply_no: Optional[str] = Field(default=None, alias="applyNo")
license_plate: Optional[str] = Field(default=None, alias="licensePlate")
remark: Optional[str] = None
create_by: Optional[str] = Field(default=None, alias="createBy")
create_by_name: Optional[str] = Field(default=None, alias="createByName")
create_time: Optional[datetime] = Field(default=None, alias="createTime")
update_time: Optional[datetime] = Field(default=None, alias="updateTime")
model_config = {"populate_by_name": True}
class VehicleRechargeRecordListResponse(BaseModel):
items: list[VehicleRechargeRecordRead]
total: int
page: int
page_size: int = Field(..., alias="pageSize")
model_config = {"populate_by_name": True}
class VehicleRechargeDisburseBoardItem(BaseModel):
"""发放登记页:申请单 + 关联发放信息(左连接)"""
apply_id: str = Field(..., alias="applyId")
apply_no: str = Field(..., alias="applyNo")
license_plate: str = Field(..., alias="licensePlate")
recharge_type: str = Field(..., alias="rechargeType")
apply_date: date = Field(..., alias="applyDate")
odometer_km: Optional[Decimal] = Field(default=None, alias="odometerKm")
applicant_name: Optional[str] = Field(default=None, alias="applicantName")
apply_status: str = Field(..., alias="applyStatus")
apply_remark: Optional[str] = Field(default=None, alias="applyRemark")
record_id: Optional[str] = Field(default=None, alias="recordId")
recharge_order_no: Optional[str] = Field(default=None, alias="rechargeOrderNo")
recharge_date: Optional[date] = Field(default=None, alias="rechargeDate")
card_no: Optional[str] = Field(default=None, alias="cardNo")
gas_station: Optional[str] = Field(default=None, alias="gasStation")
amount: Optional[Decimal] = None
record_create_by_name: Optional[str] = Field(default=None, alias="recordCreateByName")
record_remark: Optional[str] = Field(default=None, alias="recordRemark")
model_config = {"populate_by_name": True}
class VehicleRechargeDisburseBoardListResponse(BaseModel):
items: list[VehicleRechargeDisburseBoardItem]
total: int
page: int
page_size: int = Field(..., alias="pageSize")
model_config = {"populate_by_name": True}
+150
View File
@@ -0,0 +1,150 @@
"""车辆尿素 Schema"""
from datetime import date, datetime
from decimal import Decimal
from typing import Optional
from pydantic import BaseModel, Field, field_validator
class VehicleUreaApplyCreate(BaseModel):
license_plate: str = Field(..., alias="licensePlate")
applicant_id: Optional[str] = Field(default=None, alias="applicantId")
apply_date: date = Field(..., alias="applyDate")
warehouse_in_date: date = Field(..., alias="warehouseInDate")
unit: str = Field(default="吨桶")
quantity: Decimal
apply_amount: Decimal = Field(..., alias="applyAmount")
attachment_urls: Optional[str] = Field(default=None, alias="attachmentUrls")
remark: Optional[str] = None
model_config = {"populate_by_name": True}
@field_validator("quantity")
@classmethod
def validate_quantity(cls, v: Decimal) -> Decimal:
if v is None or v <= 0:
raise ValueError("入库数须大于 0")
return v
@field_validator("apply_amount")
@classmethod
def validate_apply_amount(cls, v: Decimal) -> Decimal:
if v is None or v <= 0:
raise ValueError("申请金额须大于 0")
return v
class VehicleUreaApplyUpdate(VehicleUreaApplyCreate):
pass
class VehicleUreaApplyRead(BaseModel):
id: str
apply_no: str = Field(..., alias="applyNo")
license_plate: str = Field(..., alias="licensePlate")
applicant_id: str = Field(..., alias="applicantId")
applicant_name: Optional[str] = Field(default=None, alias="applicantName")
apply_date: date = Field(..., alias="applyDate")
warehouse_in_date: date = Field(..., alias="warehouseInDate")
unit: str
quantity: Decimal
apply_amount: Decimal = Field(..., alias="applyAmount")
attachment_urls: Optional[str] = Field(default=None, alias="attachmentUrls")
status: str
remark: Optional[str] = None
create_time: Optional[datetime] = Field(default=None, alias="createTime")
update_time: Optional[datetime] = Field(default=None, alias="updateTime")
model_config = {"populate_by_name": True}
class VehicleUreaApplyListResponse(BaseModel):
items: list[VehicleUreaApplyRead]
total: int
page: int
page_size: int = Field(..., alias="pageSize")
model_config = {"populate_by_name": True}
class VehicleUreaDisburseCreate(BaseModel):
apply_id: Optional[str] = Field(default=None, alias="applyId")
license_plate: Optional[str] = Field(default=None, alias="licensePlate")
amount: Decimal
handler_id: Optional[str] = Field(default=None, alias="handlerId")
disburse_date: date = Field(..., alias="disburseDate")
attachment_urls: Optional[str] = Field(default=None, alias="attachmentUrls")
remark: Optional[str] = None
model_config = {"populate_by_name": True}
@field_validator("amount")
@classmethod
def validate_amount(cls, v: Decimal) -> Decimal:
if v is None or v <= 0:
raise ValueError("发放金额须大于 0")
return v
class VehicleUreaDisburseUpdate(VehicleUreaDisburseCreate):
pass
class VehicleUreaDisburseRead(BaseModel):
id: str
disburse_no: str = Field(..., alias="disburseNo")
apply_id: Optional[str] = Field(default=None, alias="applyId")
apply_no: Optional[str] = Field(default=None, alias="applyNo")
license_plate: Optional[str] = Field(default=None, alias="licensePlate")
amount: Decimal
handler_id: str = Field(..., alias="handlerId")
handler_name: Optional[str] = Field(default=None, alias="handlerName")
disburse_date: date = Field(..., alias="disburseDate")
attachment_urls: Optional[str] = Field(default=None, alias="attachmentUrls")
remark: Optional[str] = None
create_by: Optional[str] = Field(default=None, alias="createBy")
create_by_name: Optional[str] = Field(default=None, alias="createByName")
create_time: Optional[datetime] = Field(default=None, alias="createTime")
update_time: Optional[datetime] = Field(default=None, alias="updateTime")
model_config = {"populate_by_name": True}
class VehicleUreaDisburseListResponse(BaseModel):
items: list[VehicleUreaDisburseRead]
total: int
page: int
page_size: int = Field(..., alias="pageSize")
model_config = {"populate_by_name": True}
class VehicleUreaDisburseBoardItem(BaseModel):
apply_id: str = Field(..., alias="applyId")
apply_no: str = Field(..., alias="applyNo")
license_plate: str = Field(..., alias="licensePlate")
apply_date: date = Field(..., alias="applyDate")
warehouse_in_date: date = Field(..., alias="warehouseInDate")
unit: str
quantity: Decimal
apply_amount: Decimal = Field(..., alias="applyAmount")
applicant_name: Optional[str] = Field(default=None, alias="applicantName")
apply_status: str = Field(..., alias="applyStatus")
apply_remark: Optional[str] = Field(default=None, alias="applyRemark")
disburse_id: Optional[str] = Field(default=None, alias="disburseId")
disburse_no: Optional[str] = Field(default=None, alias="disburseNo")
disburse_date: Optional[date] = Field(default=None, alias="disburseDate")
amount: Optional[Decimal] = None
handler_name: Optional[str] = Field(default=None, alias="handlerName")
disburse_remark: Optional[str] = Field(default=None, alias="disburseRemark")
model_config = {"populate_by_name": True}
class VehicleUreaDisburseBoardListResponse(BaseModel):
items: list[VehicleUreaDisburseBoardItem]
total: int
page: int
page_size: int = Field(..., alias="pageSize")
model_config = {"populate_by_name": True}
+5
View File
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any, List, Optional, Sequence, Tuple, Type, Ty
from sqlalchemy.orm import Session
from app.core.utils import generate_uuid
from app.core.delete_guard import assert_row_deletable_by_creator, user_info_from_context
from app.services.bx_data_scope import apply_bx_scope, assert_bx_row_in_scope
if TYPE_CHECKING:
@@ -119,6 +120,10 @@ class BxCrudService:
)
if not obj:
return False
if user:
assert_row_deletable_by_creator(obj, user_info_from_context(user))
elif user_id:
assert_row_deletable_by_creator(obj, {"user_id": user_id})
obj.del_flag = 1
if hasattr(obj, "update_by") and user_id:
obj.update_by = user_id
+104
View File
@@ -0,0 +1,104 @@
"""运输计划 - 承运商资质与运输资格"""
from __future__ import annotations
from datetime import date
from typing import Optional
from fastapi import HTTPException
from app.models.pl_models import PlCarrier
def _urls_nonempty(value: Optional[str]) -> bool:
return bool(value and str(value).strip())
def carrier_qualification_complete(carrier: PlCarrier) -> bool:
return (
_urls_nonempty(carrier.business_license_urls)
and _urls_nonempty(carrier.road_transport_license_urls)
and _urls_nonempty(carrier.transport_agreement_urls)
and carrier.transport_agreement_expire_date is not None
)
def carrier_agreement_expired(carrier: PlCarrier, today: Optional[date] = None) -> bool:
if not carrier_qualification_complete(carrier):
return False
ref = today or date.today()
expire = carrier.transport_agreement_expire_date
return expire is not None and expire < ref
def carrier_audit_approved(carrier: PlCarrier) -> bool:
return getattr(carrier, "audit_status", None) == "approved"
def carrier_eligible_for_transport(carrier: PlCarrier, today: Optional[date] = None) -> bool:
if carrier.status != "1":
return False
if not carrier_audit_approved(carrier):
return False
if not carrier_qualification_complete(carrier):
return False
if carrier_agreement_expired(carrier, today):
return False
return True
def assert_carrier_eligible_for_transport(
carrier: PlCarrier,
*,
rearrange: bool = False,
) -> None:
if carrier.status != "1":
raise HTTPException(status_code=400, detail="该承运商已终止合作,无法安排运输")
if not carrier_audit_approved(carrier):
raise HTTPException(status_code=400, detail="该承运商尚未审核通过,无法安排运输")
if not carrier_qualification_complete(carrier):
raise HTTPException(status_code=400, detail="该承运商资质附件不完整,无法安排运输")
if carrier_agreement_expired(carrier):
if rearrange:
raise HTTPException(
status_code=400,
detail="该承运商运输协议已过期,请先更新协议后再调整运输",
)
raise HTTPException(status_code=400, detail="该承运商运输协议已过期,无法安排运输")
def normalize_carrier_urls(value: Optional[str]) -> Optional[str]:
if value is None:
return None
trimmed = str(value).strip()
return trimmed or None
def validate_carrier_qualification_fields(
*,
business_license_urls: Optional[str],
road_transport_license_urls: Optional[str],
transport_agreement_urls: Optional[str],
transport_agreement_expire_date: Optional[date],
require_all: bool,
) -> None:
if not require_all:
touched = any(
item is not None
for item in (
business_license_urls,
road_transport_license_urls,
transport_agreement_urls,
transport_agreement_expire_date,
)
)
if not touched:
return
if not _urls_nonempty(business_license_urls):
raise HTTPException(status_code=400, detail="请上传营业执照")
if not _urls_nonempty(road_transport_license_urls):
raise HTTPException(status_code=400, detail="请上传道路经营许可证")
if not _urls_nonempty(transport_agreement_urls):
raise HTTPException(status_code=400, detail="请上传运输协议")
if transport_agreement_expire_date is None:
raise HTTPException(status_code=400, detail="请填写运输协议有效期")
+3
View File
@@ -32,6 +32,7 @@ def certs_to_read(certs: list[PlCert]) -> list[CertRead]:
CertRead(
id=c.id,
cert_id=c.id,
vehicle_part=c.vehicle_part,
cert_name=c.cert_name,
cert_no=c.cert_no,
expire_date=c.expire_date,
@@ -74,6 +75,7 @@ def save_owner_certs(
cert_id = row.cert_id
if cert_id and cert_id in existing_map:
cert = existing_map[cert_id]
cert.vehicle_part = row.vehicle_part
cert.cert_name = row.cert_name
cert.cert_no = row.cert_no
cert.expire_date = row.expire_date
@@ -88,6 +90,7 @@ def save_owner_certs(
tenant_no=tenant_no,
owner_type=owner_type,
owner_id=owner_id,
vehicle_part=row.vehicle_part,
cert_name=row.cert_name,
cert_no=row.cert_no,
expire_date=row.expire_date,
@@ -11,6 +11,7 @@ from sqlalchemy import func, or_
from sqlalchemy.orm import Session
from app.core.utils import generate_uuid
from app.core.delete_guard import assert_row_deletable_by_creator, user_info_from_context
from app.models.models import SysUser
from app.services.pl_data_scope import apply_pl_scope, assert_pl_row_in_scope
from app.models.pl_models import (
@@ -88,6 +89,46 @@ def get_settlement_row(
return row
def get_settlement_by_transport_id(
db: Session,
*,
tenant_no: str,
transport_id: str,
user=None,
) -> Optional[Dict[str, Any]]:
if not transport_id:
return None
row = (
db.query(PlFreightSettlement)
.join(
PlFreightSettlementTransport,
PlFreightSettlementTransport.settlement_id == PlFreightSettlement.id,
)
.filter(
PlFreightSettlementTransport.transport_id == transport_id,
PlFreightSettlementTransport.tenant_no == tenant_no,
PlFreightSettlementTransport.del_flag == 0,
PlFreightSettlement.tenant_no == tenant_no,
PlFreightSettlement.del_flag == 0,
)
.order_by(PlFreightSettlement.create_time.desc())
.first()
)
if not row:
return None
if user:
assert_pl_row_in_scope(db, row, PlFreightSettlement, user, tenant_no)
return {
"id": row.id,
"settlementNo": row.settlement_no,
"invoiceAmount": row.invoice_amount,
"invoiceNo": row.invoice_no,
"invoiceDate": row.invoice_date,
"invoiceAttachmentUrls": row.invoice_attachment_urls,
"settlementStatus": row.settlement_status,
}
def list_unsettled_plans(
db: Session,
*,
@@ -264,6 +305,11 @@ def delete_draft_settlement(
if settlement.settlement_status != "pending":
raise HTTPException(status_code=400, detail="仅草稿结算单可删除")
if user:
assert_row_deletable_by_creator(settlement, user_info_from_context(user))
elif user_id:
assert_row_deletable_by_creator(settlement, {"user_id": user_id})
now = datetime.now()
settlement.del_flag = 1
settlement.update_by = user_id
+3 -2
View File
@@ -379,7 +379,8 @@ def _apply_payload_to_transport_row(
transport.freight_price = transport_payload.freight_price
transport.freight_amount = freight_amount or 0
transport.freight_type = transport_payload.freight_type
transport.has_invoice = transport_payload.has_invoice
if transport_payload.has_invoice is not None:
transport.has_invoice = transport_payload.has_invoice
transport.unit = "吨" if transport_payload.freight_type == "by_weight" else "趟"
if transport_payload.remark is not None:
transport.remark = transport_payload.remark
@@ -651,7 +652,7 @@ def create_transport_for_plan(
freight_price=transport_payload.freight_price,
freight_amount=freight_amount or 0,
freight_type=transport_payload.freight_type,
has_invoice=transport_payload.has_invoice,
has_invoice=transport_payload.has_invoice if transport_payload.has_invoice is not None else 0,
transport_status="draft",
approval_status="none",
remark=transport_payload.remark,
@@ -0,0 +1,683 @@
"""车辆维保业务逻辑"""
from __future__ import annotations
from datetime import date, datetime
from typing import Optional
from fastapi import HTTPException
from sqlalchemy import and_, func, or_
from sqlalchemy.orm import Session
from app.core.db_auth import DbUserContext
from app.core.utils import generate_uuid
from app.models.models import SysUser
from app.models.pl_models import (
PlVehicle,
PlVehicleMaintenanceApply,
PlVehicleMaintenanceProcess,
)
from app.schemas.pl.vehicle_maintenance import (
VehicleMaintenanceApplyCreate,
VehicleMaintenanceApplyRead,
VehicleMaintenanceApplyReminder,
VehicleMaintenanceApplyUpdate,
VehicleMaintenanceProcessBoardItem,
VehicleMaintenanceProcessCreate,
VehicleMaintenanceProcessRead,
VehicleMaintenanceProcessUpdate,
)
from app.services.pl_data_scope import apply_pl_scope, assert_pl_row_in_scope
def _generate_seq_no(db: Session, model, column_attr: str, prefix: str) -> str:
column = getattr(model, column_attr)
last = (
db.query(model)
.filter(column.like(f"{prefix}%"))
.order_by(column.desc())
.first()
)
seq = int(getattr(last, column_attr)[-6:]) + 1 if last else 1
return f"{prefix}{seq:06d}"
def generate_apply_no(db: Session) -> str:
return _generate_seq_no(
db, PlVehicleMaintenanceApply, "apply_no", f"SXWB{date.today():%Y%m%d}"
)
def generate_process_no(db: Session) -> str:
return _generate_seq_no(
db,
PlVehicleMaintenanceProcess,
"process_no",
f"SXWC{date.today():%Y%m%d}",
)
def _resolve_user_name(db: Session, user_id: Optional[str]) -> Optional[str]:
if not user_id:
return None
row = (
db.query(SysUser)
.filter(SysUser.id == user_id, SysUser.del_flag == 0)
.first()
)
return row.nick_name or row.user_no if row else None
def _assert_vehicle_exists(db: Session, tenant_no: str, license_plate: str) -> None:
exists = (
db.query(PlVehicle.id)
.filter(
PlVehicle.tenant_no == tenant_no,
PlVehicle.license_plate == license_plate,
PlVehicle.del_flag == 0,
)
.first()
)
if not exists:
raise HTTPException(status_code=400, detail="车牌号不存在")
def _resolve_user_id(
db: Session,
tenant_no: str,
user_id: Optional[str],
user: DbUserContext,
*,
field_label: str = "用户",
) -> str:
target = (user_id or "").strip() or user.id
row = (
db.query(SysUser.id)
.filter(
SysUser.id == target,
SysUser.tenant_no == tenant_no,
SysUser.del_flag == 0,
SysUser.status == "1",
)
.first()
)
if not row:
raise HTTPException(status_code=400, detail=f"{field_label}不存在或已停用")
return target
def _validate_apply_dates(
apply_date: date,
start_date: date,
expected_pickup_date: Optional[date],
) -> None:
if apply_date > start_date:
raise HTTPException(status_code=400, detail="申请时间不能晚于维保开始时间")
if expected_pickup_date and start_date > expected_pickup_date:
raise HTTPException(status_code=400, detail="维保开始时间不能晚于预计取车时间")
def _get_apply_row(
db: Session, tenant_no: str, apply_id: str, user: DbUserContext
) -> PlVehicleMaintenanceApply:
row = (
db.query(PlVehicleMaintenanceApply)
.filter(
PlVehicleMaintenanceApply.id == apply_id,
PlVehicleMaintenanceApply.tenant_no == tenant_no,
PlVehicleMaintenanceApply.del_flag == 0,
)
.first()
)
if not row:
raise HTTPException(status_code=404, detail="申请单不存在")
assert_pl_row_in_scope(db, row, PlVehicleMaintenanceApply, user, tenant_no)
return row
def _get_process_row(
db: Session, tenant_no: str, process_id: str, user: DbUserContext
) -> PlVehicleMaintenanceProcess:
row = (
db.query(PlVehicleMaintenanceProcess)
.filter(
PlVehicleMaintenanceProcess.id == process_id,
PlVehicleMaintenanceProcess.tenant_no == tenant_no,
PlVehicleMaintenanceProcess.del_flag == 0,
)
.first()
)
if not row:
raise HTTPException(status_code=404, detail="处理登记不存在")
assert_pl_row_in_scope(db, row, PlVehicleMaintenanceProcess, user, tenant_no)
return row
def _has_active_process(db: Session, tenant_no: str, apply_id: str) -> bool:
return (
db.query(PlVehicleMaintenanceProcess.id)
.filter(
PlVehicleMaintenanceProcess.apply_id == apply_id,
PlVehicleMaintenanceProcess.tenant_no == tenant_no,
PlVehicleMaintenanceProcess.del_flag == 0,
)
.first()
is not None
)
def apply_to_read(row: PlVehicleMaintenanceApply, db: Session) -> VehicleMaintenanceApplyRead:
return VehicleMaintenanceApplyRead(
id=row.id,
applyNo=row.apply_no,
licensePlate=row.license_plate,
applicantId=row.applicant_id,
applicantName=_resolve_user_name(db, row.applicant_id),
applyDate=row.apply_date,
maintenanceType=row.maintenance_type,
startDate=row.start_date,
expectedPickupDate=row.expected_pickup_date,
maintenanceContent=row.maintenance_content,
shopAddress=row.shop_address,
attachmentUrls=row.attachment_urls,
status=row.status,
remark=row.remark,
createTime=row.create_time,
updateTime=row.update_time,
)
def process_to_read(
row: PlVehicleMaintenanceProcess, db: Session, apply_no: Optional[str] = None
) -> VehicleMaintenanceProcessRead:
if apply_no is None and row.apply_id:
apply = (
db.query(PlVehicleMaintenanceApply.apply_no)
.filter(
PlVehicleMaintenanceApply.id == row.apply_id,
PlVehicleMaintenanceApply.del_flag == 0,
)
.first()
)
apply_no = apply.apply_no if apply else None
return VehicleMaintenanceProcessRead(
id=row.id,
processNo=row.process_no,
applyId=row.apply_id,
applyNo=apply_no,
licensePlate=row.license_plate,
amount=row.amount,
handlerId=row.handler_id,
handlerName=_resolve_user_name(db, row.handler_id),
processDate=row.process_date,
attachmentUrls=row.attachment_urls,
remark=row.remark,
createBy=row.create_by,
createByName=_resolve_user_name(db, row.create_by),
createTime=row.create_time,
updateTime=row.update_time,
)
def _resolve_apply_for_process(
db: Session,
tenant_no: str,
payload: VehicleMaintenanceProcessCreate | VehicleMaintenanceProcessUpdate,
user: DbUserContext,
*,
current_process_id: Optional[str] = None,
) -> tuple[Optional[PlVehicleMaintenanceApply], str]:
license_plate = (payload.license_plate or "").strip()
if payload.apply_id:
apply = _get_apply_row(db, tenant_no, payload.apply_id, user)
if apply.status != "pending":
raise HTTPException(status_code=400, detail="仅待处理申请可关联")
existing = (
db.query(PlVehicleMaintenanceProcess)
.filter(
PlVehicleMaintenanceProcess.apply_id == apply.id,
PlVehicleMaintenanceProcess.tenant_no == tenant_no,
PlVehicleMaintenanceProcess.del_flag == 0,
)
.first()
)
if existing and existing.id != current_process_id:
raise HTTPException(status_code=400, detail="该申请单已被关联")
return apply, apply.license_plate
if not license_plate:
raise HTTPException(status_code=400, detail="请填写车牌号或关联申请单")
_assert_vehicle_exists(db, tenant_no, license_plate)
return None, license_plate
def list_applies(
db: Session,
tenant_no: str,
user: DbUserContext,
*,
q: Optional[str] = None,
maintenance_type: Optional[str] = None,
status: Optional[str] = None,
apply_date_from: Optional[date] = None,
apply_date_to: Optional[date] = None,
applicant_id: Optional[str] = None,
page: int = 1,
page_size: int = 20,
) -> tuple[list[VehicleMaintenanceApplyRead], int]:
query = db.query(PlVehicleMaintenanceApply).filter(
PlVehicleMaintenanceApply.tenant_no == tenant_no,
PlVehicleMaintenanceApply.del_flag == 0,
)
if q:
query = query.filter(
or_(
PlVehicleMaintenanceApply.apply_no.contains(q),
PlVehicleMaintenanceApply.license_plate.contains(q),
)
)
if maintenance_type:
query = query.filter(
PlVehicleMaintenanceApply.maintenance_type.contains(maintenance_type)
)
if status:
query = query.filter(PlVehicleMaintenanceApply.status == status)
if apply_date_from:
query = query.filter(PlVehicleMaintenanceApply.apply_date >= apply_date_from)
if apply_date_to:
query = query.filter(PlVehicleMaintenanceApply.apply_date <= apply_date_to)
if applicant_id:
query = query.filter(PlVehicleMaintenanceApply.applicant_id == applicant_id)
query = apply_pl_scope(query, PlVehicleMaintenanceApply, user, db, tenant_no)
total = query.count()
rows = (
query.order_by(PlVehicleMaintenanceApply.create_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
return [apply_to_read(row, db) for row in rows], total
def get_apply(
db: Session, tenant_no: str, user: DbUserContext, apply_id: str
) -> VehicleMaintenanceApplyRead:
row = _get_apply_row(db, tenant_no, apply_id, user)
return apply_to_read(row, db)
def create_apply(
db: Session,
tenant_no: str,
user: DbUserContext,
payload: VehicleMaintenanceApplyCreate,
) -> VehicleMaintenanceApplyRead:
license_plate = payload.license_plate.strip()
maintenance_type = payload.maintenance_type.strip()
if not maintenance_type:
raise HTTPException(status_code=400, detail="维保类型不能为空")
_assert_vehicle_exists(db, tenant_no, license_plate)
_validate_apply_dates(
payload.apply_date, payload.start_date, payload.expected_pickup_date
)
applicant_id = _resolve_user_id(
db, tenant_no, payload.applicant_id, user, field_label="申请人"
)
now = datetime.now()
row = PlVehicleMaintenanceApply(
id=generate_uuid(),
apply_no=generate_apply_no(db),
license_plate=license_plate,
applicant_id=applicant_id,
apply_date=payload.apply_date,
maintenance_type=maintenance_type,
start_date=payload.start_date,
expected_pickup_date=payload.expected_pickup_date,
maintenance_content=(payload.maintenance_content or "").strip() or None,
shop_address=(payload.shop_address or "").strip() or None,
attachment_urls=payload.attachment_urls,
status="pending",
tenant_no=tenant_no,
remark=payload.remark,
create_by=user.id,
create_dept_id=user.dept_id or "",
create_time=now,
update_by=user.id,
update_time=now,
del_flag=0,
)
db.add(row)
db.commit()
db.refresh(row)
return apply_to_read(row, db)
def update_apply(
db: Session,
tenant_no: str,
user: DbUserContext,
apply_id: str,
payload: VehicleMaintenanceApplyUpdate,
) -> VehicleMaintenanceApplyRead:
row = _get_apply_row(db, tenant_no, apply_id, user)
if row.status != "pending":
raise HTTPException(status_code=400, detail="仅待处理申请可编辑")
license_plate = payload.license_plate.strip()
maintenance_type = payload.maintenance_type.strip()
if not maintenance_type:
raise HTTPException(status_code=400, detail="维保类型不能为空")
_assert_vehicle_exists(db, tenant_no, license_plate)
_validate_apply_dates(
payload.apply_date, payload.start_date, payload.expected_pickup_date
)
row.license_plate = license_plate
row.applicant_id = _resolve_user_id(
db, tenant_no, payload.applicant_id, user, field_label="申请人"
)
row.apply_date = payload.apply_date
row.maintenance_type = maintenance_type
row.start_date = payload.start_date
row.expected_pickup_date = payload.expected_pickup_date
row.maintenance_content = (payload.maintenance_content or "").strip() or None
row.shop_address = (payload.shop_address or "").strip() or None
row.attachment_urls = payload.attachment_urls
row.remark = payload.remark
row.update_by = user.id
row.update_time = datetime.now()
db.commit()
db.refresh(row)
return apply_to_read(row, db)
def cancel_apply(
db: Session, tenant_no: str, user: DbUserContext, apply_id: str
) -> VehicleMaintenanceApplyRead:
row = _get_apply_row(db, tenant_no, apply_id, user)
if row.status != "pending":
raise HTTPException(status_code=400, detail="仅待处理申请可作废")
row.status = "cancelled"
row.update_by = user.id
row.update_time = datetime.now()
db.commit()
db.refresh(row)
return apply_to_read(row, db)
def delete_applies(
db: Session, tenant_no: str, user: DbUserContext, apply_ids: list[str]
) -> None:
for apply_id in apply_ids:
row = _get_apply_row(db, tenant_no, apply_id, user)
if row.status != "pending":
raise HTTPException(status_code=400, detail="仅待处理申请可删除")
if _has_active_process(db, tenant_no, apply_id):
raise HTTPException(status_code=400, detail="已关联处理记录的申请不可删除")
row.del_flag = 1
row.update_by = user.id
row.update_time = datetime.now()
db.commit()
def list_reminders(
db: Session,
tenant_no: str,
user: DbUserContext,
license_plate: str,
*,
exclude_apply_id: Optional[str] = None,
) -> list[VehicleMaintenanceApplyReminder]:
plate = license_plate.strip()
if not plate:
return []
today = date.today()
query = db.query(PlVehicleMaintenanceApply).filter(
PlVehicleMaintenanceApply.tenant_no == tenant_no,
PlVehicleMaintenanceApply.del_flag == 0,
PlVehicleMaintenanceApply.status == "pending",
PlVehicleMaintenanceApply.license_plate == plate,
)
if exclude_apply_id:
query = query.filter(PlVehicleMaintenanceApply.id != exclude_apply_id)
query = apply_pl_scope(query, PlVehicleMaintenanceApply, user, db, tenant_no)
rows = query.order_by(PlVehicleMaintenanceApply.create_time.desc()).all()
result: list[VehicleMaintenanceApplyReminder] = []
for row in rows:
pickup = row.expected_pickup_date
if pickup is not None and today > pickup:
continue
result.append(
VehicleMaintenanceApplyReminder(
id=row.id,
applyNo=row.apply_no,
licensePlate=row.license_plate,
maintenanceType=row.maintenance_type,
applyDate=row.apply_date,
startDate=row.start_date,
expectedPickupDate=row.expected_pickup_date,
maintenanceContent=row.maintenance_content,
shopAddress=row.shop_address,
)
)
return result
def board_item_to_read(
apply_row: PlVehicleMaintenanceApply,
process_row: Optional[PlVehicleMaintenanceProcess],
db: Session,
) -> VehicleMaintenanceProcessBoardItem:
return VehicleMaintenanceProcessBoardItem(
applyId=apply_row.id,
applyNo=apply_row.apply_no,
licensePlate=apply_row.license_plate,
maintenanceType=apply_row.maintenance_type,
applyDate=apply_row.apply_date,
startDate=apply_row.start_date,
expectedPickupDate=apply_row.expected_pickup_date,
maintenanceContent=apply_row.maintenance_content,
shopAddress=apply_row.shop_address,
applicantName=_resolve_user_name(db, apply_row.applicant_id),
applyStatus=apply_row.status,
applyRemark=apply_row.remark,
processId=process_row.id if process_row else None,
processNo=process_row.process_no if process_row else None,
processDate=process_row.process_date if process_row else None,
amount=process_row.amount if process_row else None,
handlerName=_resolve_user_name(db, process_row.handler_id) if process_row else None,
processRemark=process_row.remark if process_row else None,
)
def list_process_board(
db: Session,
tenant_no: str,
user: DbUserContext,
*,
q: Optional[str] = None,
maintenance_type: Optional[str] = None,
apply_status: Optional[str] = None,
apply_date_from: Optional[date] = None,
apply_date_to: Optional[date] = None,
process_date_from: Optional[date] = None,
process_date_to: Optional[date] = None,
page: int = 1,
page_size: int = 20,
) -> tuple[list[VehicleMaintenanceProcessBoardItem], int]:
query = (
db.query(PlVehicleMaintenanceApply, PlVehicleMaintenanceProcess)
.outerjoin(
PlVehicleMaintenanceProcess,
and_(
PlVehicleMaintenanceProcess.apply_id == PlVehicleMaintenanceApply.id,
PlVehicleMaintenanceProcess.tenant_no == tenant_no,
PlVehicleMaintenanceProcess.del_flag == 0,
),
)
.filter(
PlVehicleMaintenanceApply.tenant_no == tenant_no,
PlVehicleMaintenanceApply.del_flag == 0,
)
)
if q:
query = query.filter(
or_(
PlVehicleMaintenanceApply.apply_no.contains(q),
PlVehicleMaintenanceApply.license_plate.contains(q),
func.coalesce(PlVehicleMaintenanceProcess.process_no, "").contains(q),
)
)
if maintenance_type:
query = query.filter(
PlVehicleMaintenanceApply.maintenance_type.contains(maintenance_type)
)
if apply_status:
query = query.filter(PlVehicleMaintenanceApply.status == apply_status)
if apply_date_from:
query = query.filter(PlVehicleMaintenanceApply.apply_date >= apply_date_from)
if apply_date_to:
query = query.filter(PlVehicleMaintenanceApply.apply_date <= apply_date_to)
if process_date_from:
query = query.filter(
PlVehicleMaintenanceProcess.process_date >= process_date_from
)
if process_date_to:
query = query.filter(PlVehicleMaintenanceProcess.process_date <= process_date_to)
query = apply_pl_scope(query, PlVehicleMaintenanceApply, user, db, tenant_no)
total = query.count()
rows = (
query.order_by(PlVehicleMaintenanceApply.create_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
return [
board_item_to_read(apply_row, process_row, db)
for apply_row, process_row in rows
], total
def get_process(
db: Session, tenant_no: str, user: DbUserContext, process_id: str
) -> VehicleMaintenanceProcessRead:
row = _get_process_row(db, tenant_no, process_id, user)
return process_to_read(row, db)
def create_process(
db: Session,
tenant_no: str,
user: DbUserContext,
payload: VehicleMaintenanceProcessCreate,
) -> VehicleMaintenanceProcessRead:
apply_row, license_plate = _resolve_apply_for_process(db, tenant_no, payload, user)
handler_id = _resolve_user_id(
db, tenant_no, payload.handler_id, user, field_label="处理人"
)
now = datetime.now()
row = PlVehicleMaintenanceProcess(
id=generate_uuid(),
process_no=generate_process_no(db),
apply_id=apply_row.id if apply_row else None,
license_plate=license_plate,
amount=payload.amount,
handler_id=handler_id,
process_date=payload.process_date,
attachment_urls=payload.attachment_urls,
tenant_no=tenant_no,
remark=payload.remark,
create_by=user.id,
create_dept_id=user.dept_id or "",
create_time=now,
update_by=user.id,
update_time=now,
del_flag=0,
)
db.add(row)
if apply_row:
apply_row.status = "processed"
apply_row.update_by = user.id
apply_row.update_time = now
db.commit()
db.refresh(row)
return process_to_read(row, db)
def update_process(
db: Session,
tenant_no: str,
user: DbUserContext,
process_id: str,
payload: VehicleMaintenanceProcessUpdate,
) -> VehicleMaintenanceProcessRead:
row = _get_process_row(db, tenant_no, process_id, user)
old_apply_id = row.apply_id
apply_row, license_plate = _resolve_apply_for_process(
db, tenant_no, payload, user, current_process_id=process_id
)
handler_id = _resolve_user_id(
db, tenant_no, payload.handler_id, user, field_label="处理人"
)
now = datetime.now()
if old_apply_id and old_apply_id != (apply_row.id if apply_row else None):
old_apply = (
db.query(PlVehicleMaintenanceApply)
.filter(
PlVehicleMaintenanceApply.id == old_apply_id,
PlVehicleMaintenanceApply.tenant_no == tenant_no,
PlVehicleMaintenanceApply.del_flag == 0,
)
.first()
)
if old_apply:
old_apply.status = "pending"
old_apply.update_by = user.id
old_apply.update_time = now
row.apply_id = apply_row.id if apply_row else None
row.license_plate = license_plate
row.amount = payload.amount
row.handler_id = handler_id
row.process_date = payload.process_date
row.attachment_urls = payload.attachment_urls
row.remark = payload.remark
row.update_by = user.id
row.update_time = now
if apply_row:
apply_row.status = "processed"
apply_row.update_by = user.id
apply_row.update_time = now
db.commit()
db.refresh(row)
return process_to_read(row, db)
def delete_processes(
db: Session, tenant_no: str, user: DbUserContext, process_ids: list[str]
) -> None:
now = datetime.now()
for process_id in process_ids:
row = _get_process_row(db, tenant_no, process_id, user)
apply_id = row.apply_id
row.del_flag = 1
row.update_by = user.id
row.update_time = now
if apply_id:
apply = (
db.query(PlVehicleMaintenanceApply)
.filter(
PlVehicleMaintenanceApply.id == apply_id,
PlVehicleMaintenanceApply.tenant_no == tenant_no,
PlVehicleMaintenanceApply.del_flag == 0,
)
.first()
)
if apply and apply.status == "processed":
apply.status = "pending"
apply.update_by = user.id
apply.update_time = now
db.commit()
@@ -0,0 +1,670 @@
"""车辆 ETC/加油充值业务逻辑"""
from __future__ import annotations
from datetime import date, datetime
from typing import Optional
from fastapi import HTTPException
from sqlalchemy import and_, func, or_
from sqlalchemy.orm import Session
from app.core.db_auth import DbUserContext
from app.core.utils import generate_uuid
from app.models.models import SysUser
from app.models.pl_models import (
PlVehicle,
PlVehicleRechargeApply,
PlVehicleRechargeRecord,
)
from app.schemas.pl.vehicle_recharge import (
VehicleRechargeApplyCreate,
VehicleRechargeApplyRead,
VehicleRechargeApplyUpdate,
VehicleRechargeDisburseBoardItem,
VehicleRechargeRecordCreate,
VehicleRechargeRecordRead,
VehicleRechargeRecordUpdate,
)
from app.services.pl_data_scope import apply_pl_scope, assert_pl_row_in_scope
def _generate_seq_no(db: Session, model, column_attr: str, prefix: str) -> str:
column = getattr(model, column_attr)
last = (
db.query(model)
.filter(column.like(f"{prefix}%"))
.order_by(column.desc())
.first()
)
seq = int(getattr(last, column_attr)[-6:]) + 1 if last else 1
return f"{prefix}{seq:06d}"
def generate_apply_no(db: Session) -> str:
return _generate_seq_no(
db, PlVehicleRechargeApply, "apply_no", f"SXSQ{date.today():%Y%m%d}"
)
def generate_recharge_order_no(db: Session) -> str:
return _generate_seq_no(
db,
PlVehicleRechargeRecord,
"recharge_order_no",
f"SXCL{date.today():%Y%m%d}",
)
def _resolve_user_name(db: Session, user_id: Optional[str]) -> Optional[str]:
if not user_id:
return None
row = (
db.query(SysUser)
.filter(SysUser.id == user_id, SysUser.del_flag == 0)
.first()
)
return row.nick_name or row.user_no if row else None
def _assert_vehicle_exists(db: Session, tenant_no: str, license_plate: str) -> None:
exists = (
db.query(PlVehicle.id)
.filter(
PlVehicle.tenant_no == tenant_no,
PlVehicle.license_plate == license_plate,
PlVehicle.del_flag == 0,
)
.first()
)
if not exists:
raise HTTPException(status_code=400, detail="车牌号不存在")
def _resolve_applicant_id(
db: Session,
tenant_no: str,
applicant_id: Optional[str],
user: DbUserContext,
) -> str:
target = (applicant_id or "").strip() or user.id
row = (
db.query(SysUser.id)
.filter(
SysUser.id == target,
SysUser.tenant_no == tenant_no,
SysUser.del_flag == 0,
SysUser.status == "1",
)
.first()
)
if not row:
raise HTTPException(status_code=400, detail="申请人不存在或已停用")
return target
def _get_apply_row(
db: Session, tenant_no: str, apply_id: str, user: DbUserContext
) -> PlVehicleRechargeApply:
row = (
db.query(PlVehicleRechargeApply)
.filter(
PlVehicleRechargeApply.id == apply_id,
PlVehicleRechargeApply.tenant_no == tenant_no,
PlVehicleRechargeApply.del_flag == 0,
)
.first()
)
if not row:
raise HTTPException(status_code=404, detail="申请单不存在")
assert_pl_row_in_scope(db, row, PlVehicleRechargeApply, user, tenant_no)
return row
def _get_record_row(
db: Session, tenant_no: str, record_id: str, user: DbUserContext
) -> PlVehicleRechargeRecord:
row = (
db.query(PlVehicleRechargeRecord)
.filter(
PlVehicleRechargeRecord.id == record_id,
PlVehicleRechargeRecord.tenant_no == tenant_no,
PlVehicleRechargeRecord.del_flag == 0,
)
.first()
)
if not row:
raise HTTPException(status_code=404, detail="发放登记不存在")
assert_pl_row_in_scope(db, row, PlVehicleRechargeRecord, user, tenant_no)
return row
def _has_active_record(db: Session, tenant_no: str, apply_id: str) -> bool:
return (
db.query(PlVehicleRechargeRecord.id)
.filter(
PlVehicleRechargeRecord.apply_id == apply_id,
PlVehicleRechargeRecord.tenant_no == tenant_no,
PlVehicleRechargeRecord.del_flag == 0,
)
.first()
is not None
)
def apply_to_read(row: PlVehicleRechargeApply, db: Session) -> VehicleRechargeApplyRead:
return VehicleRechargeApplyRead(
id=row.id,
applyNo=row.apply_no,
licensePlate=row.license_plate,
rechargeType=row.recharge_type,
applyDate=row.apply_date,
odometerKm=row.odometer_km,
attachmentUrls=row.attachment_urls,
applicantId=row.applicant_id,
applicantName=_resolve_user_name(db, row.applicant_id),
status=row.status,
remark=row.remark,
createTime=row.create_time,
updateTime=row.update_time,
)
def record_to_read(
row: PlVehicleRechargeRecord, db: Session, apply_no: Optional[str] = None
) -> VehicleRechargeRecordRead:
if apply_no is None and row.apply_id:
apply = (
db.query(PlVehicleRechargeApply.apply_no)
.filter(
PlVehicleRechargeApply.id == row.apply_id,
PlVehicleRechargeApply.del_flag == 0,
)
.first()
)
apply_no = apply.apply_no if apply else None
return VehicleRechargeRecordRead(
id=row.id,
rechargeOrderNo=row.recharge_order_no,
rechargeDate=row.recharge_date,
rechargeType=row.recharge_type,
cardNo=row.card_no,
gasStation=row.gas_station,
amount=row.amount,
attachmentUrls=row.attachment_urls,
applyId=row.apply_id,
applyNo=apply_no,
licensePlate=row.license_plate,
remark=row.remark,
createBy=row.create_by,
createByName=_resolve_user_name(db, row.create_by),
createTime=row.create_time,
updateTime=row.update_time,
)
def _resolve_apply_for_record(
db: Session,
tenant_no: str,
payload: VehicleRechargeRecordCreate | VehicleRechargeRecordUpdate,
user: DbUserContext,
*,
current_record_id: Optional[str] = None,
) -> tuple[Optional[PlVehicleRechargeApply], str, str]:
license_plate = (payload.license_plate or "").strip()
recharge_type = payload.recharge_type
if payload.apply_id:
apply = _get_apply_row(db, tenant_no, payload.apply_id, user)
if apply.status != "pending":
raise HTTPException(status_code=400, detail="仅待发放申请可关联")
existing = (
db.query(PlVehicleRechargeRecord)
.filter(
PlVehicleRechargeRecord.apply_id == apply.id,
PlVehicleRechargeRecord.tenant_no == tenant_no,
PlVehicleRechargeRecord.del_flag == 0,
)
.first()
)
if existing and existing.id != current_record_id:
raise HTTPException(status_code=400, detail="该申请单已被关联")
if apply.recharge_type != recharge_type:
raise HTTPException(status_code=400, detail="充值类型与申请单不一致")
return apply, apply.license_plate, apply.recharge_type
if not license_plate:
raise HTTPException(status_code=400, detail="请填写车牌号或关联申请单")
_assert_vehicle_exists(db, tenant_no, license_plate)
return None, license_plate, recharge_type
def list_applies(
db: Session,
tenant_no: str,
user: DbUserContext,
*,
q: Optional[str] = None,
recharge_type: Optional[str] = None,
status: Optional[str] = None,
apply_date_from: Optional[date] = None,
apply_date_to: Optional[date] = None,
applicant_id: Optional[str] = None,
page: int = 1,
page_size: int = 20,
) -> tuple[list[VehicleRechargeApplyRead], int]:
query = db.query(PlVehicleRechargeApply).filter(
PlVehicleRechargeApply.tenant_no == tenant_no,
PlVehicleRechargeApply.del_flag == 0,
)
if q:
query = query.filter(
or_(
PlVehicleRechargeApply.apply_no.contains(q),
PlVehicleRechargeApply.license_plate.contains(q),
)
)
if recharge_type:
query = query.filter(PlVehicleRechargeApply.recharge_type == recharge_type)
if status:
query = query.filter(PlVehicleRechargeApply.status == status)
if apply_date_from:
query = query.filter(PlVehicleRechargeApply.apply_date >= apply_date_from)
if apply_date_to:
query = query.filter(PlVehicleRechargeApply.apply_date <= apply_date_to)
if applicant_id:
query = query.filter(PlVehicleRechargeApply.applicant_id == applicant_id)
query = apply_pl_scope(query, PlVehicleRechargeApply, user, db, tenant_no)
total = query.count()
rows = (
query.order_by(PlVehicleRechargeApply.create_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
return [apply_to_read(row, db) for row in rows], total
def get_apply(
db: Session, tenant_no: str, user: DbUserContext, apply_id: str
) -> VehicleRechargeApplyRead:
row = _get_apply_row(db, tenant_no, apply_id, user)
return apply_to_read(row, db)
def create_apply(
db: Session,
tenant_no: str,
user: DbUserContext,
payload: VehicleRechargeApplyCreate,
) -> VehicleRechargeApplyRead:
license_plate = payload.license_plate.strip()
_assert_vehicle_exists(db, tenant_no, license_plate)
applicant_id = _resolve_applicant_id(db, tenant_no, payload.applicant_id, user)
now = datetime.now()
row = PlVehicleRechargeApply(
id=generate_uuid(),
apply_no=generate_apply_no(db),
license_plate=license_plate,
recharge_type=payload.recharge_type,
apply_date=payload.apply_date,
odometer_km=payload.odometer_km,
attachment_urls=payload.attachment_urls,
applicant_id=applicant_id,
status="pending",
tenant_no=tenant_no,
remark=payload.remark,
create_by=user.id,
create_dept_id=user.dept_id or "",
create_time=now,
update_by=user.id,
update_time=now,
del_flag=0,
)
db.add(row)
db.commit()
db.refresh(row)
return apply_to_read(row, db)
def update_apply(
db: Session,
tenant_no: str,
user: DbUserContext,
apply_id: str,
payload: VehicleRechargeApplyUpdate,
) -> VehicleRechargeApplyRead:
row = _get_apply_row(db, tenant_no, apply_id, user)
if row.status != "pending":
raise HTTPException(status_code=400, detail="仅待发放申请可编辑")
license_plate = payload.license_plate.strip()
_assert_vehicle_exists(db, tenant_no, license_plate)
row.license_plate = license_plate
row.recharge_type = payload.recharge_type
row.apply_date = payload.apply_date
row.odometer_km = payload.odometer_km
row.attachment_urls = payload.attachment_urls
row.applicant_id = _resolve_applicant_id(db, tenant_no, payload.applicant_id, user)
row.remark = payload.remark
row.update_by = user.id
row.update_time = datetime.now()
db.commit()
db.refresh(row)
return apply_to_read(row, db)
def cancel_apply(
db: Session, tenant_no: str, user: DbUserContext, apply_id: str
) -> VehicleRechargeApplyRead:
row = _get_apply_row(db, tenant_no, apply_id, user)
if row.status != "pending":
raise HTTPException(status_code=400, detail="仅待发放申请可作废")
row.status = "cancelled"
row.update_by = user.id
row.update_time = datetime.now()
db.commit()
db.refresh(row)
return apply_to_read(row, db)
def delete_applies(
db: Session, tenant_no: str, user: DbUserContext, apply_ids: list[str]
) -> None:
for apply_id in apply_ids:
row = _get_apply_row(db, tenant_no, apply_id, user)
if row.status != "pending":
raise HTTPException(status_code=400, detail="仅待发放申请可删除")
if _has_active_record(db, tenant_no, apply_id):
raise HTTPException(status_code=400, detail="已关联发放记录的申请不可删除")
row.del_flag = 1
row.update_by = user.id
row.update_time = datetime.now()
db.commit()
def list_pending_apply_options(
db: Session, tenant_no: str, user: DbUserContext
) -> list[dict]:
query = db.query(PlVehicleRechargeApply).filter(
PlVehicleRechargeApply.tenant_no == tenant_no,
PlVehicleRechargeApply.del_flag == 0,
PlVehicleRechargeApply.status == "pending",
)
query = apply_pl_scope(query, PlVehicleRechargeApply, user, db, tenant_no)
rows = query.order_by(PlVehicleRechargeApply.create_time.desc()).all()
return [
{
"id": row.id,
"applyNo": row.apply_no,
"licensePlate": row.license_plate,
"rechargeType": row.recharge_type,
}
for row in rows
]
def list_records(
db: Session,
tenant_no: str,
user: DbUserContext,
*,
q: Optional[str] = None,
recharge_type: Optional[str] = None,
recharge_date_from: Optional[date] = None,
recharge_date_to: Optional[date] = None,
gas_station: Optional[str] = None,
apply_no: Optional[str] = None,
page: int = 1,
page_size: int = 20,
) -> tuple[list[VehicleRechargeRecordRead], int]:
query = db.query(PlVehicleRechargeRecord).filter(
PlVehicleRechargeRecord.tenant_no == tenant_no,
PlVehicleRechargeRecord.del_flag == 0,
)
if q:
query = query.filter(
or_(
PlVehicleRechargeRecord.recharge_order_no.contains(q),
func.coalesce(PlVehicleRechargeRecord.license_plate, "").contains(q),
func.coalesce(PlVehicleRechargeRecord.card_no, "").contains(q),
)
)
if recharge_type:
query = query.filter(PlVehicleRechargeRecord.recharge_type == recharge_type)
if recharge_date_from:
query = query.filter(PlVehicleRechargeRecord.recharge_date >= recharge_date_from)
if recharge_date_to:
query = query.filter(PlVehicleRechargeRecord.recharge_date <= recharge_date_to)
if gas_station:
query = query.filter(
func.coalesce(PlVehicleRechargeRecord.gas_station, "").contains(gas_station)
)
if apply_no:
query = query.join(
PlVehicleRechargeApply,
PlVehicleRechargeApply.id == PlVehicleRechargeRecord.apply_id,
).filter(PlVehicleRechargeApply.apply_no.contains(apply_no))
query = apply_pl_scope(query, PlVehicleRechargeRecord, user, db, tenant_no)
total = query.count()
rows = (
query.order_by(PlVehicleRechargeRecord.create_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
return [record_to_read(row, db) for row in rows], total
def board_item_to_read(
apply_row: PlVehicleRechargeApply,
record_row: Optional[PlVehicleRechargeRecord],
db: Session,
) -> VehicleRechargeDisburseBoardItem:
return VehicleRechargeDisburseBoardItem(
applyId=apply_row.id,
applyNo=apply_row.apply_no,
licensePlate=apply_row.license_plate,
rechargeType=apply_row.recharge_type,
applyDate=apply_row.apply_date,
odometerKm=apply_row.odometer_km,
applicantName=_resolve_user_name(db, apply_row.applicant_id),
applyStatus=apply_row.status,
applyRemark=apply_row.remark,
recordId=record_row.id if record_row else None,
rechargeOrderNo=record_row.recharge_order_no if record_row else None,
rechargeDate=record_row.recharge_date if record_row else None,
cardNo=record_row.card_no if record_row else None,
gasStation=record_row.gas_station if record_row else None,
amount=record_row.amount if record_row else None,
recordCreateByName=_resolve_user_name(db, record_row.create_by) if record_row else None,
recordRemark=record_row.remark if record_row else None,
)
def list_disburse_board(
db: Session,
tenant_no: str,
user: DbUserContext,
*,
q: Optional[str] = None,
recharge_type: Optional[str] = None,
apply_status: Optional[str] = None,
apply_date_from: Optional[date] = None,
apply_date_to: Optional[date] = None,
recharge_date_from: Optional[date] = None,
recharge_date_to: Optional[date] = None,
page: int = 1,
page_size: int = 20,
) -> tuple[list[VehicleRechargeDisburseBoardItem], int]:
query = (
db.query(PlVehicleRechargeApply, PlVehicleRechargeRecord)
.outerjoin(
PlVehicleRechargeRecord,
and_(
PlVehicleRechargeRecord.apply_id == PlVehicleRechargeApply.id,
PlVehicleRechargeRecord.tenant_no == tenant_no,
PlVehicleRechargeRecord.del_flag == 0,
),
)
.filter(
PlVehicleRechargeApply.tenant_no == tenant_no,
PlVehicleRechargeApply.del_flag == 0,
)
)
if q:
query = query.filter(
or_(
PlVehicleRechargeApply.apply_no.contains(q),
PlVehicleRechargeApply.license_plate.contains(q),
func.coalesce(PlVehicleRechargeRecord.recharge_order_no, "").contains(q),
func.coalesce(PlVehicleRechargeRecord.card_no, "").contains(q),
)
)
if recharge_type:
query = query.filter(PlVehicleRechargeApply.recharge_type == recharge_type)
if apply_status:
query = query.filter(PlVehicleRechargeApply.status == apply_status)
if apply_date_from:
query = query.filter(PlVehicleRechargeApply.apply_date >= apply_date_from)
if apply_date_to:
query = query.filter(PlVehicleRechargeApply.apply_date <= apply_date_to)
if recharge_date_from:
query = query.filter(PlVehicleRechargeRecord.recharge_date >= recharge_date_from)
if recharge_date_to:
query = query.filter(PlVehicleRechargeRecord.recharge_date <= recharge_date_to)
query = apply_pl_scope(query, PlVehicleRechargeApply, user, db, tenant_no)
total = query.count()
rows = (
query.order_by(PlVehicleRechargeApply.create_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
return [board_item_to_read(apply_row, record_row, db) for apply_row, record_row in rows], total
def get_record(
db: Session, tenant_no: str, user: DbUserContext, record_id: str
) -> VehicleRechargeRecordRead:
row = _get_record_row(db, tenant_no, record_id, user)
return record_to_read(row, db)
def create_record(
db: Session,
tenant_no: str,
user: DbUserContext,
payload: VehicleRechargeRecordCreate,
) -> VehicleRechargeRecordRead:
apply_row, license_plate, recharge_type = _resolve_apply_for_record(
db, tenant_no, payload, user
)
now = datetime.now()
row = PlVehicleRechargeRecord(
id=generate_uuid(),
recharge_order_no=generate_recharge_order_no(db),
recharge_date=payload.recharge_date,
recharge_type=recharge_type,
card_no=(payload.card_no or "").strip() or None,
gas_station=(payload.gas_station or "").strip() or None,
amount=payload.amount,
attachment_urls=payload.attachment_urls,
apply_id=apply_row.id if apply_row else None,
license_plate=license_plate,
tenant_no=tenant_no,
remark=payload.remark,
create_by=user.id,
create_dept_id=user.dept_id or "",
create_time=now,
update_by=user.id,
update_time=now,
del_flag=0,
)
db.add(row)
if apply_row:
apply_row.status = "disbursed"
apply_row.update_by = user.id
apply_row.update_time = now
db.commit()
db.refresh(row)
return record_to_read(row, db)
def update_record(
db: Session,
tenant_no: str,
user: DbUserContext,
record_id: str,
payload: VehicleRechargeRecordUpdate,
) -> VehicleRechargeRecordRead:
row = _get_record_row(db, tenant_no, record_id, user)
old_apply_id = row.apply_id
apply_row, license_plate, recharge_type = _resolve_apply_for_record(
db, tenant_no, payload, user, current_record_id=record_id
)
now = datetime.now()
if old_apply_id and old_apply_id != (apply_row.id if apply_row else None):
old_apply = (
db.query(PlVehicleRechargeApply)
.filter(
PlVehicleRechargeApply.id == old_apply_id,
PlVehicleRechargeApply.tenant_no == tenant_no,
PlVehicleRechargeApply.del_flag == 0,
)
.first()
)
if old_apply:
old_apply.status = "pending"
old_apply.update_by = user.id
old_apply.update_time = now
row.recharge_date = payload.recharge_date
row.recharge_type = recharge_type
row.card_no = (payload.card_no or "").strip() or None
row.gas_station = (payload.gas_station or "").strip() or None
row.amount = payload.amount
row.attachment_urls = payload.attachment_urls
row.apply_id = apply_row.id if apply_row else None
row.license_plate = license_plate
row.remark = payload.remark
row.update_by = user.id
row.update_time = now
if apply_row:
apply_row.status = "disbursed"
apply_row.update_by = user.id
apply_row.update_time = now
db.commit()
db.refresh(row)
return record_to_read(row, db)
def delete_records(
db: Session, tenant_no: str, user: DbUserContext, record_ids: list[str]
) -> None:
now = datetime.now()
for record_id in record_ids:
row = _get_record_row(db, tenant_no, record_id, user)
apply_id = row.apply_id
row.del_flag = 1
row.update_by = user.id
row.update_time = now
if apply_id:
apply = (
db.query(PlVehicleRechargeApply)
.filter(
PlVehicleRechargeApply.id == apply_id,
PlVehicleRechargeApply.tenant_no == tenant_no,
PlVehicleRechargeApply.del_flag == 0,
)
.first()
)
if apply and apply.status == "disbursed":
apply.status = "pending"
apply.update_by = user.id
apply.update_time = now
db.commit()
@@ -0,0 +1,594 @@
"""车辆尿素业务逻辑"""
from __future__ import annotations
from datetime import date, datetime
from typing import Optional
from fastapi import HTTPException
from sqlalchemy import and_, func, or_
from sqlalchemy.orm import Session
from app.core.db_auth import DbUserContext
from app.core.utils import generate_uuid
from app.models.models import SysUser
from app.models.pl_models import PlVehicle, PlVehicleUreaApply, PlVehicleUreaDisburse
from app.schemas.pl.vehicle_urea import (
VehicleUreaApplyCreate,
VehicleUreaApplyRead,
VehicleUreaApplyUpdate,
VehicleUreaDisburseBoardItem,
VehicleUreaDisburseCreate,
VehicleUreaDisburseRead,
VehicleUreaDisburseUpdate,
)
from app.services.pl_data_scope import apply_pl_scope, assert_pl_row_in_scope
def _generate_seq_no(db: Session, model, column_attr: str, prefix: str) -> str:
column = getattr(model, column_attr)
last = (
db.query(model)
.filter(column.like(f"{prefix}%"))
.order_by(column.desc())
.first()
)
seq = int(getattr(last, column_attr)[-6:]) + 1 if last else 1
return f"{prefix}{seq:06d}"
def generate_apply_no(db: Session) -> str:
return _generate_seq_no(
db, PlVehicleUreaApply, "apply_no", f"SXNS{date.today():%Y%m%d}"
)
def generate_disburse_no(db: Session) -> str:
return _generate_seq_no(
db, PlVehicleUreaDisburse, "disburse_no", f"SXUF{date.today():%Y%m%d}"
)
def _resolve_user_name(db: Session, user_id: Optional[str]) -> Optional[str]:
if not user_id:
return None
row = (
db.query(SysUser)
.filter(SysUser.id == user_id, SysUser.del_flag == 0)
.first()
)
return row.nick_name or row.user_no if row else None
def _assert_vehicle_exists(db: Session, tenant_no: str, license_plate: str) -> None:
exists = (
db.query(PlVehicle.id)
.filter(
PlVehicle.tenant_no == tenant_no,
PlVehicle.license_plate == license_plate,
PlVehicle.del_flag == 0,
)
.first()
)
if not exists:
raise HTTPException(status_code=400, detail="车牌号不存在")
def _resolve_user_id(
db: Session,
tenant_no: str,
user_id: Optional[str],
user: DbUserContext,
*,
field_label: str = "用户",
) -> str:
target = (user_id or "").strip() or user.id
row = (
db.query(SysUser.id)
.filter(
SysUser.id == target,
SysUser.tenant_no == tenant_no,
SysUser.del_flag == 0,
SysUser.status == "1",
)
.first()
)
if not row:
raise HTTPException(status_code=400, detail=f"{field_label}不存在或已停用")
return target
def _validate_apply_dates(apply_date: date, warehouse_in_date: date) -> None:
if apply_date > warehouse_in_date:
raise HTTPException(status_code=400, detail="申请时间不能晚于入库时间")
def _get_apply_row(
db: Session, tenant_no: str, apply_id: str, user: DbUserContext
) -> PlVehicleUreaApply:
row = (
db.query(PlVehicleUreaApply)
.filter(
PlVehicleUreaApply.id == apply_id,
PlVehicleUreaApply.tenant_no == tenant_no,
PlVehicleUreaApply.del_flag == 0,
)
.first()
)
if not row:
raise HTTPException(status_code=404, detail="申请单不存在")
assert_pl_row_in_scope(db, row, PlVehicleUreaApply, user, tenant_no)
return row
def _get_disburse_row(
db: Session, tenant_no: str, disburse_id: str, user: DbUserContext
) -> PlVehicleUreaDisburse:
row = (
db.query(PlVehicleUreaDisburse)
.filter(
PlVehicleUreaDisburse.id == disburse_id,
PlVehicleUreaDisburse.tenant_no == tenant_no,
PlVehicleUreaDisburse.del_flag == 0,
)
.first()
)
if not row:
raise HTTPException(status_code=404, detail="发放登记不存在")
assert_pl_row_in_scope(db, row, PlVehicleUreaDisburse, user, tenant_no)
return row
def _has_active_disburse(db: Session, tenant_no: str, apply_id: str) -> bool:
return (
db.query(PlVehicleUreaDisburse.id)
.filter(
PlVehicleUreaDisburse.apply_id == apply_id,
PlVehicleUreaDisburse.tenant_no == tenant_no,
PlVehicleUreaDisburse.del_flag == 0,
)
.first()
is not None
)
def apply_to_read(row: PlVehicleUreaApply, db: Session) -> VehicleUreaApplyRead:
return VehicleUreaApplyRead(
id=row.id,
applyNo=row.apply_no,
licensePlate=row.license_plate,
applicantId=row.applicant_id,
applicantName=_resolve_user_name(db, row.applicant_id),
applyDate=row.apply_date,
warehouseInDate=row.warehouse_in_date,
unit=row.unit,
quantity=row.quantity,
applyAmount=row.apply_amount,
attachmentUrls=row.attachment_urls,
status=row.status,
remark=row.remark,
createTime=row.create_time,
updateTime=row.update_time,
)
def disburse_to_read(
row: PlVehicleUreaDisburse, db: Session, apply_no: Optional[str] = None
) -> VehicleUreaDisburseRead:
if apply_no is None and row.apply_id:
apply = (
db.query(PlVehicleUreaApply.apply_no)
.filter(
PlVehicleUreaApply.id == row.apply_id,
PlVehicleUreaApply.del_flag == 0,
)
.first()
)
apply_no = apply.apply_no if apply else None
return VehicleUreaDisburseRead(
id=row.id,
disburseNo=row.disburse_no,
applyId=row.apply_id,
applyNo=apply_no,
licensePlate=row.license_plate,
amount=row.amount,
handlerId=row.handler_id,
handlerName=_resolve_user_name(db, row.handler_id),
disburseDate=row.disburse_date,
attachmentUrls=row.attachment_urls,
remark=row.remark,
createBy=row.create_by,
createByName=_resolve_user_name(db, row.create_by),
createTime=row.create_time,
updateTime=row.update_time,
)
def _resolve_apply_for_disburse(
db: Session,
tenant_no: str,
payload: VehicleUreaDisburseCreate | VehicleUreaDisburseUpdate,
user: DbUserContext,
*,
current_disburse_id: Optional[str] = None,
) -> tuple[Optional[PlVehicleUreaApply], str]:
license_plate = (payload.license_plate or "").strip()
if payload.apply_id:
apply = _get_apply_row(db, tenant_no, payload.apply_id, user)
if apply.status != "pending":
raise HTTPException(status_code=400, detail="仅待发放申请可关联")
existing = (
db.query(PlVehicleUreaDisburse)
.filter(
PlVehicleUreaDisburse.apply_id == apply.id,
PlVehicleUreaDisburse.tenant_no == tenant_no,
PlVehicleUreaDisburse.del_flag == 0,
)
.first()
)
if existing and existing.id != current_disburse_id:
raise HTTPException(status_code=400, detail="该申请单已被关联")
return apply, apply.license_plate
if not license_plate:
raise HTTPException(status_code=400, detail="请填写车牌号或关联申请单")
_assert_vehicle_exists(db, tenant_no, license_plate)
return None, license_plate
def list_applies(
db: Session,
tenant_no: str,
user: DbUserContext,
*,
q: Optional[str] = None,
status: Optional[str] = None,
apply_date_from: Optional[date] = None,
apply_date_to: Optional[date] = None,
applicant_id: Optional[str] = None,
page: int = 1,
page_size: int = 20,
) -> tuple[list[VehicleUreaApplyRead], int]:
query = db.query(PlVehicleUreaApply).filter(
PlVehicleUreaApply.tenant_no == tenant_no,
PlVehicleUreaApply.del_flag == 0,
)
if q:
query = query.filter(
or_(
PlVehicleUreaApply.apply_no.contains(q),
PlVehicleUreaApply.license_plate.contains(q),
)
)
if status:
query = query.filter(PlVehicleUreaApply.status == status)
if apply_date_from:
query = query.filter(PlVehicleUreaApply.apply_date >= apply_date_from)
if apply_date_to:
query = query.filter(PlVehicleUreaApply.apply_date <= apply_date_to)
if applicant_id:
query = query.filter(PlVehicleUreaApply.applicant_id == applicant_id)
query = apply_pl_scope(query, PlVehicleUreaApply, user, db, tenant_no)
total = query.count()
rows = (
query.order_by(PlVehicleUreaApply.create_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
return [apply_to_read(row, db) for row in rows], total
def get_apply(
db: Session, tenant_no: str, user: DbUserContext, apply_id: str
) -> VehicleUreaApplyRead:
row = _get_apply_row(db, tenant_no, apply_id, user)
return apply_to_read(row, db)
def create_apply(
db: Session,
tenant_no: str,
user: DbUserContext,
payload: VehicleUreaApplyCreate,
) -> VehicleUreaApplyRead:
license_plate = payload.license_plate.strip()
_assert_vehicle_exists(db, tenant_no, license_plate)
_validate_apply_dates(payload.apply_date, payload.warehouse_in_date)
applicant_id = _resolve_user_id(db, tenant_no, payload.applicant_id, user, field_label="申请人")
unit = (payload.unit or "").strip() or "吨桶"
now = datetime.now()
row = PlVehicleUreaApply(
id=generate_uuid(),
apply_no=generate_apply_no(db),
license_plate=license_plate,
applicant_id=applicant_id,
apply_date=payload.apply_date,
warehouse_in_date=payload.warehouse_in_date,
unit=unit,
quantity=payload.quantity,
apply_amount=payload.apply_amount,
attachment_urls=payload.attachment_urls,
status="pending",
tenant_no=tenant_no,
remark=payload.remark,
create_by=user.id,
create_dept_id=user.dept_id or "",
create_time=now,
update_by=user.id,
update_time=now,
del_flag=0,
)
db.add(row)
db.commit()
db.refresh(row)
return apply_to_read(row, db)
def update_apply(
db: Session,
tenant_no: str,
user: DbUserContext,
apply_id: str,
payload: VehicleUreaApplyUpdate,
) -> VehicleUreaApplyRead:
row = _get_apply_row(db, tenant_no, apply_id, user)
if row.status != "pending":
raise HTTPException(status_code=400, detail="仅待发放申请可编辑")
license_plate = payload.license_plate.strip()
_assert_vehicle_exists(db, tenant_no, license_plate)
_validate_apply_dates(payload.apply_date, payload.warehouse_in_date)
unit = (payload.unit or "").strip() or "吨桶"
row.license_plate = license_plate
row.applicant_id = _resolve_user_id(db, tenant_no, payload.applicant_id, user, field_label="申请人")
row.apply_date = payload.apply_date
row.warehouse_in_date = payload.warehouse_in_date
row.unit = unit
row.quantity = payload.quantity
row.apply_amount = payload.apply_amount
row.attachment_urls = payload.attachment_urls
row.remark = payload.remark
row.update_by = user.id
row.update_time = datetime.now()
db.commit()
db.refresh(row)
return apply_to_read(row, db)
def cancel_apply(
db: Session, tenant_no: str, user: DbUserContext, apply_id: str
) -> VehicleUreaApplyRead:
row = _get_apply_row(db, tenant_no, apply_id, user)
if row.status != "pending":
raise HTTPException(status_code=400, detail="仅待发放申请可作废")
row.status = "cancelled"
row.update_by = user.id
row.update_time = datetime.now()
db.commit()
db.refresh(row)
return apply_to_read(row, db)
def delete_applies(
db: Session, tenant_no: str, user: DbUserContext, apply_ids: list[str]
) -> None:
for apply_id in apply_ids:
row = _get_apply_row(db, tenant_no, apply_id, user)
if row.status != "pending":
raise HTTPException(status_code=400, detail="仅待发放申请可删除")
if _has_active_disburse(db, tenant_no, apply_id):
raise HTTPException(status_code=400, detail="已关联发放记录的申请不可删除")
row.del_flag = 1
row.update_by = user.id
row.update_time = datetime.now()
db.commit()
def board_item_to_read(
apply_row: PlVehicleUreaApply,
disburse_row: Optional[PlVehicleUreaDisburse],
db: Session,
) -> VehicleUreaDisburseBoardItem:
return VehicleUreaDisburseBoardItem(
applyId=apply_row.id,
applyNo=apply_row.apply_no,
licensePlate=apply_row.license_plate,
applyDate=apply_row.apply_date,
warehouseInDate=apply_row.warehouse_in_date,
unit=apply_row.unit,
quantity=apply_row.quantity,
applyAmount=apply_row.apply_amount,
applicantName=_resolve_user_name(db, apply_row.applicant_id),
applyStatus=apply_row.status,
applyRemark=apply_row.remark,
disburseId=disburse_row.id if disburse_row else None,
disburseNo=disburse_row.disburse_no if disburse_row else None,
disburseDate=disburse_row.disburse_date if disburse_row else None,
amount=disburse_row.amount if disburse_row else None,
handlerName=_resolve_user_name(db, disburse_row.handler_id) if disburse_row else None,
disburseRemark=disburse_row.remark if disburse_row else None,
)
def list_disburse_board(
db: Session,
tenant_no: str,
user: DbUserContext,
*,
q: Optional[str] = None,
apply_status: Optional[str] = None,
apply_date_from: Optional[date] = None,
apply_date_to: Optional[date] = None,
disburse_date_from: Optional[date] = None,
disburse_date_to: Optional[date] = None,
page: int = 1,
page_size: int = 20,
) -> tuple[list[VehicleUreaDisburseBoardItem], int]:
query = (
db.query(PlVehicleUreaApply, PlVehicleUreaDisburse)
.outerjoin(
PlVehicleUreaDisburse,
and_(
PlVehicleUreaDisburse.apply_id == PlVehicleUreaApply.id,
PlVehicleUreaDisburse.tenant_no == tenant_no,
PlVehicleUreaDisburse.del_flag == 0,
),
)
.filter(
PlVehicleUreaApply.tenant_no == tenant_no,
PlVehicleUreaApply.del_flag == 0,
)
)
if q:
query = query.filter(
or_(
PlVehicleUreaApply.apply_no.contains(q),
PlVehicleUreaApply.license_plate.contains(q),
func.coalesce(PlVehicleUreaDisburse.disburse_no, "").contains(q),
)
)
if apply_status:
query = query.filter(PlVehicleUreaApply.status == apply_status)
if apply_date_from:
query = query.filter(PlVehicleUreaApply.apply_date >= apply_date_from)
if apply_date_to:
query = query.filter(PlVehicleUreaApply.apply_date <= apply_date_to)
if disburse_date_from:
query = query.filter(PlVehicleUreaDisburse.disburse_date >= disburse_date_from)
if disburse_date_to:
query = query.filter(PlVehicleUreaDisburse.disburse_date <= disburse_date_to)
query = apply_pl_scope(query, PlVehicleUreaApply, user, db, tenant_no)
total = query.count()
rows = (
query.order_by(PlVehicleUreaApply.create_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
return [
board_item_to_read(apply_row, disburse_row, db)
for apply_row, disburse_row in rows
], total
def get_disburse(
db: Session, tenant_no: str, user: DbUserContext, disburse_id: str
) -> VehicleUreaDisburseRead:
row = _get_disburse_row(db, tenant_no, disburse_id, user)
return disburse_to_read(row, db)
def create_disburse(
db: Session,
tenant_no: str,
user: DbUserContext,
payload: VehicleUreaDisburseCreate,
) -> VehicleUreaDisburseRead:
apply_row, license_plate = _resolve_apply_for_disburse(db, tenant_no, payload, user)
handler_id = _resolve_user_id(db, tenant_no, payload.handler_id, user, field_label="处理人")
now = datetime.now()
row = PlVehicleUreaDisburse(
id=generate_uuid(),
disburse_no=generate_disburse_no(db),
apply_id=apply_row.id if apply_row else None,
license_plate=license_plate,
amount=payload.amount,
handler_id=handler_id,
disburse_date=payload.disburse_date,
attachment_urls=payload.attachment_urls,
tenant_no=tenant_no,
remark=payload.remark,
create_by=user.id,
create_dept_id=user.dept_id or "",
create_time=now,
update_by=user.id,
update_time=now,
del_flag=0,
)
db.add(row)
if apply_row:
apply_row.status = "disbursed"
apply_row.update_by = user.id
apply_row.update_time = now
db.commit()
db.refresh(row)
return disburse_to_read(row, db)
def update_disburse(
db: Session,
tenant_no: str,
user: DbUserContext,
disburse_id: str,
payload: VehicleUreaDisburseUpdate,
) -> VehicleUreaDisburseRead:
row = _get_disburse_row(db, tenant_no, disburse_id, user)
old_apply_id = row.apply_id
apply_row, license_plate = _resolve_apply_for_disburse(
db, tenant_no, payload, user, current_disburse_id=disburse_id
)
handler_id = _resolve_user_id(db, tenant_no, payload.handler_id, user, field_label="处理人")
now = datetime.now()
if old_apply_id and old_apply_id != (apply_row.id if apply_row else None):
old_apply = (
db.query(PlVehicleUreaApply)
.filter(
PlVehicleUreaApply.id == old_apply_id,
PlVehicleUreaApply.tenant_no == tenant_no,
PlVehicleUreaApply.del_flag == 0,
)
.first()
)
if old_apply:
old_apply.status = "pending"
old_apply.update_by = user.id
old_apply.update_time = now
row.apply_id = apply_row.id if apply_row else None
row.license_plate = license_plate
row.amount = payload.amount
row.handler_id = handler_id
row.disburse_date = payload.disburse_date
row.attachment_urls = payload.attachment_urls
row.remark = payload.remark
row.update_by = user.id
row.update_time = now
if apply_row:
apply_row.status = "disbursed"
apply_row.update_by = user.id
apply_row.update_time = now
db.commit()
db.refresh(row)
return disburse_to_read(row, db)
def delete_disburses(
db: Session, tenant_no: str, user: DbUserContext, disburse_ids: list[str]
) -> None:
now = datetime.now()
for disburse_id in disburse_ids:
row = _get_disburse_row(db, tenant_no, disburse_id, user)
apply_id = row.apply_id
row.del_flag = 1
row.update_by = user.id
row.update_time = now
if apply_id:
apply = (
db.query(PlVehicleUreaApply)
.filter(
PlVehicleUreaApply.id == apply_id,
PlVehicleUreaApply.tenant_no == tenant_no,
PlVehicleUreaApply.del_flag == 0,
)
.first()
)
if apply and apply.status == "disbursed":
apply.status = "pending"
apply.update_by = user.id
apply.update_time = now
db.commit()
+18 -4
View File
@@ -84,22 +84,28 @@
"82": "Community 82",
"83": "Community 83",
"84": "Community 84",
"85": "Community 85",
"86": "Community 86",
"87": "Community 87",
"88": "Community 88",
"89": "Community 89",
"90": "Community 90",
"91": "Community 91",
"92": "Community 92",
"93": "Community 93",
"94": "Community 94",
"95": "Community 95",
"96": "Community 96",
"97": "Community 97",
"98": "Community 98",
"99": "Community 99",
"100": "Community 100",
"101": "Community 101",
"102": "Community 102",
"103": "Community 103",
"104": "Community 104",
"105": "Community 105",
"107": "Community 107",
"108": "Community 108",
"109": "Community 109",
"110": "Community 110",
"112": "Community 112",
"113": "Community 113",
@@ -112,16 +118,24 @@
"120": "Community 120",
"121": "Community 121",
"122": "Community 122",
"124": "Community 124",
"125": "Community 125",
"126": "Community 126",
"127": "Community 127",
"128": "Community 128",
"129": "Community 129",
"131": "Community 131",
"132": "Community 132",
"133": "Community 133",
"134": "Community 134",
"135": "Community 135",
"136": "Community 136",
"137": "Community 137",
"138": "Community 138",
"139": "Community 139",
"140": "Community 140",
"142": "Community 142",
"143": "Community 143",
"149": "Community 149"
"145": "Community 145",
"146": "Community 146",
"147": "Community 147"
}
+301 -219
View File
@@ -1,16 +1,16 @@
# Graph Report - backend (2026-06-28)
# Graph Report - backend (2026-06-29)
## Corpus Check
- 208 files · ~145,658 words
- 223 files · ~157,012 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 4328 nodes · 31993 edges · 125 communities (121 shown, 4 thin omitted)
- Extraction: 50% EXTRACTED · 50% INFERRED · 0% AMBIGUOUS · INFERRED: 16138 edges (avg confidence: 0.5)
- 4858 nodes · 34385 edges · 139 communities (138 shown, 1 thin omitted)
- Extraction: 50% EXTRACTED · 50% INFERRED · 0% AMBIGUOUS · INFERRED: 17062 edges (avg confidence: 0.5)
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `ee531e9c`
- Built from commit: `493f8d67`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
@@ -100,48 +100,62 @@
- [[_COMMUNITY_Community 82|Community 82]]
- [[_COMMUNITY_Community 83|Community 83]]
- [[_COMMUNITY_Community 84|Community 84]]
- [[_COMMUNITY_Community 85|Community 85]]
- [[_COMMUNITY_Community 86|Community 86]]
- [[_COMMUNITY_Community 87|Community 87]]
- [[_COMMUNITY_Community 88|Community 88]]
- [[_COMMUNITY_Community 89|Community 89]]
- [[_COMMUNITY_Community 90|Community 90]]
- [[_COMMUNITY_Community 91|Community 91]]
- [[_COMMUNITY_Community 92|Community 92]]
- [[_COMMUNITY_Community 93|Community 93]]
- [[_COMMUNITY_Community 94|Community 94]]
- [[_COMMUNITY_Community 95|Community 95]]
- [[_COMMUNITY_Community 96|Community 96]]
- [[_COMMUNITY_Community 97|Community 97]]
- [[_COMMUNITY_Community 98|Community 98]]
- [[_COMMUNITY_Community 99|Community 99]]
- [[_COMMUNITY_Community 100|Community 100]]
- [[_COMMUNITY_Community 101|Community 101]]
- [[_COMMUNITY_Community 102|Community 102]]
- [[_COMMUNITY_Community 103|Community 103]]
- [[_COMMUNITY_Community 104|Community 104]]
- [[_COMMUNITY_Community 105|Community 105]]
- [[_COMMUNITY_Community 107|Community 107]]
- [[_COMMUNITY_Community 108|Community 108]]
- [[_COMMUNITY_Community 109|Community 109]]
- [[_COMMUNITY_Community 110|Community 110]]
- [[_COMMUNITY_Community 112|Community 112]]
- [[_COMMUNITY_Community 122|Community 122]]
- [[_COMMUNITY_Community 124|Community 124]]
- [[_COMMUNITY_Community 125|Community 125]]
- [[_COMMUNITY_Community 126|Community 126]]
- [[_COMMUNITY_Community 127|Community 127]]
- [[_COMMUNITY_Community 128|Community 128]]
- [[_COMMUNITY_Community 129|Community 129]]
- [[_COMMUNITY_Community 131|Community 131]]
- [[_COMMUNITY_Community 132|Community 132]]
- [[_COMMUNITY_Community 133|Community 133]]
- [[_COMMUNITY_Community 134|Community 134]]
- [[_COMMUNITY_Community 135|Community 135]]
- [[_COMMUNITY_Community 136|Community 136]]
- [[_COMMUNITY_Community 137|Community 137]]
- [[_COMMUNITY_Community 138|Community 138]]
- [[_COMMUNITY_Community 139|Community 139]]
- [[_COMMUNITY_Community 140|Community 140]]
- [[_COMMUNITY_Community 143|Community 143]]
- [[_COMMUNITY_Community 149|Community 149]]
- [[_COMMUNITY_Community 145|Community 145]]
- [[_COMMUNITY_Community 146|Community 146]]
- [[_COMMUNITY_Community 147|Community 147]]
## God Nodes (most connected - your core abstractions)
1. `Session` - 1123 edges
2. `BaseModel` - 383 edges
2. `BaseModel` - 412 edges
3. `int` - 336 edges
4. `Any` - 331 edges
5. `get_tenant_no()` - 284 edges
6. `success_response()` - 258 edges
7. `Session` - 257 edges
8. `bool` - 255 edges
9. `error_response()` - 243 edges
10. `SysUser` - 232 edges
5. `get_tenant_no()` - 326 edges
6. `SysUser` - 281 edges
7. `DbUserContext` - 267 edges
8. `success_response()` - 258 edges
9. `Session` - 257 edges
10. `bool` - 255 edges
## Surprising Connections (you probably didn't know these)
- `bool` --uses--> `DbUserContext` [INFERRED]
@@ -155,15 +169,15 @@
- `int` --uses--> `LocalStorage` [INFERRED]
scripts/run_db_auto_generate_plans.py → app/core/storage.py
## Communities (125 total, 4 thin omitted)
## Communities (139 total, 1 thin omitted)
### Community 0 - "Community 0"
Cohesion: 0.07
Nodes (52): DbUserContext, Session, str, PlDocumentReminder, PlInventoryTransaction, PlInventoryWarning, 运输审批记录表 pl_transport_approval, 运费结算表 pl_freight_settlement (+44 more)
Cohesion: 0.13
Nodes (102): date, DbUserContext, int, Session, str, DocumentReminderRead, DocumentReminderUpdate, InventoryLedgerResponse (+94 more)
### Community 1 - "Community 1"
Cohesion: 0.15
Nodes (26): operlog_detail(), operlog_list(), 操作日志分页列表(基于 API 请求日志适配 RuoYi OperLog 结构), 操作日志分页列表(基于 API 请求日志适配 RuoYi OperLog 结构), _user_info(), Session, Any, bool (+18 more)
Cohesion: 0.04
Nodes (48): 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll (+40 more)
### Community 2 - "Community 2"
Cohesion: 0.36
@@ -171,215 +185,215 @@ Nodes (218): Any, bool, str, UploadFile, CRM 线索管理 Pydantic Schemas, CRM
### Community 3 - "Community 3"
Cohesion: 0.07
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)
Nodes (172): _apply_default_invoice_info_to_customers(), category_add(), category_delete(), category_tree(), category_update(), clue_assign(), clue_check_convert(), clue_check_duplicate() (+164 more)
### Community 4 - "Community 4"
Cohesion: 0.09
Nodes (149): DbInboundOutbound, DbUserContext, InboundOutboundRead, Session, str, bool, DbPurchasePlan, DbUserContext (+141 more)
Cohesion: 0.21
Nodes (35): DbInboundOutbound, DbUserContext, Session, str, CreateLinkForPlanPayload, _apply_inventory_product_update(), _apply_inventory_update(), _base_query() (+27 more)
### Community 5 - "Community 5"
Cohesion: 0.08
Nodes (36): _error_json(), lifespan(), _mask_secret(), Exception, int, Request, str, 未捕获异常:统一 JSON 500,确保跨域场景下仍有 Access-Control-Allow-Origin (+28 more)
Cohesion: 0.09
Nodes (34): _error_json(), lifespan(), _mask_secret(), Exception, int, Request, str, 未捕获异常:统一 JSON 500,确保跨域场景下仍有 Access-Control-Allow-Origin (+26 more)
### Community 6 - "Community 6"
Cohesion: 0.07
Nodes (98): attachment_download_url(), AttachmentDownloadUrlBody, create_dept(), create_dict_data(), create_dict_type(), create_menu(), create_post(), create_role_management() (+90 more)
Cohesion: 0.12
Nodes (62): Any, bool, date, int, Session, str, Any, bool (+54 more)
### Community 7 - "Community 7"
Cohesion: 0.17
Nodes (34): Any, bool, int, Session, str, SysUser, create_user(), get_user() (+26 more)
Cohesion: 0.14
Nodes (39): Any, bool, int, Session, str, SysUser, create_user(), get_user() (+31 more)
### Community 8 - "Community 8"
Cohesion: 0.43
Nodes (13): Any, Session, str, _customer_base_query(), get_follow_statistics(), get_industry_statistics(), get_level_statistics(), get_overview_statistics() (+5 more)
Cohesion: 0.16
Nodes (33): Any, bool, int, Session, str, Any, Session, str (+25 more)
### Community 9 - "Community 9"
Cohesion: 0.23
Nodes (10): Any, bool, int, Session, str, camel_to_snake(), CRUDBase, get_model_attr() (+2 more)
Cohesion: 0.10
Nodes (22): bool, bool, Any, bool, int, Session, str, bool (+14 more)
### Community 10 - "Community 10"
Cohesion: 0.38
Nodes (15): Any, bool, int, Session, str, create_post(), get_post(), list_posts() (+7 more)
Cohesion: 0.17
Nodes (20): Any, int, str, get_oper_log_meta(), OperLogMeta, 请求级操作日志元数据(ContextVar), 供异步落库线程使用,避免 ContextVar 跨线程丢失。, set_oper_log_meta() (+12 more)
### Community 11 - "Community 11"
Cohesion: 0.17
Nodes (33): Any, bool, BxUserContext, BxWorkflowNode, float, int, ReimburseApplyOut, Session (+25 more)
Cohesion: 0.16
Nodes (36): Any, bool, BxUserContext, BxWorkflowNode, float, int, ReimburseApplyOut, Session (+28 more)
### Community 12 - "Community 12"
Cohesion: 0.16
Nodes (39): DbUserContext, int, Session, str, FreightSettlementApprovalActionBody, FreightSettlementRead, approve_settlement(), approve_settlement_legacy() (+31 more)
Cohesion: 0.13
Nodes (79): date, DbUserContext, int, Session, str, VehicleUreaApplyCreate, VehicleUreaApplyRead, VehicleUreaApplyUpdate (+71 more)
### Community 13 - "Community 13"
Cohesion: 0.10
Nodes (17): Exception, bool, bytes, int, str, BytesIO, BaseStorage, convert_heic_to_jpeg() (+9 more)
Cohesion: 0.11
Nodes (17): Exception, bytes, int, Path, str, BytesIO, BaseStorage, convert_heic_to_jpeg() (+9 more)
### Community 14 - "Community 14"
Cohesion: 0.09
Nodes (55): Any, bool, datetime, int, Session, str, _advance_contract_after_approval(), approve_contract() (+47 more)
Nodes (61): Any, bool, CrmContract, datetime, int, Query, Session, str (+53 more)
### Community 15 - "Community 15"
Cohesion: 0.18
Nodes (24): bool, date, datetime, int, Session, str, ReimburseApplyOut, allocate_reimburse_title_seq() (+16 more)
Cohesion: 0.14
Nodes (28): str, bool, date, datetime, int, Session, str, filter_current_cycle_approvals() (+20 more)
### Community 16 - "Community 16"
Cohesion: 0.02
Nodes (175): CRM 报价单 API 路由 — 挂接到 crm.router, str, str, CRM 合同 Pydantic Schemas — 对照 CrmContractBo / CrmContractVo, CRM 客户管理 Pydantic Schemas, CRM 报价单 Pydantic Schemas, BaseModel, AdvanceApplyCreate (+167 more)
Cohesion: 0.08
Nodes (40): int, LeaveCreate, LeaveUpdate, AttachmentUploadResponse, DeleteInput, IdInput, PaginationParams, PaginationResult (+32 more)
### Community 17 - "Community 17"
Cohesion: 0.18
Nodes (50): Any, CRM 商机 CRUD(主表 + 阶段/赢输单/统计/看板/漏斗), CrmOpportunity, date, datetime, Decimal, int, Session (+42 more)
Cohesion: 0.19
Nodes (50): Any, Any, CRM 商机 CRUD(主表 + 阶段/赢输单/统计/看板/漏斗), CrmOpportunity, date, datetime, Decimal, int (+42 more)
### Community 18 - "Community 18"
Cohesion: 0.13
Nodes (27): Any, bool, BxUserContext, BxWorkflowNode, float, int, Session, str (+19 more)
Cohesion: 0.15
Nodes (24): Any, bool, BxUserContext, BxWorkflowNode, float, int, Session, str (+16 more)
### Community 19 - "Community 19"
Cohesion: 0.03
Nodes (161): register_quote_routes(), Any, bool, int, Session, str, Any, bool (+153 more)
Nodes (182): register_quote_routes(), Any, bool, int, Session, str, Any, bool (+174 more)
### Community 20 - "Community 20"
Cohesion: 0.33
Nodes (30): AdvanceApplyCreate, AdvanceApplyOut, AdvanceApplyUpdate, AdvanceApprovalCreate, BxUserContext, int, Session, str (+22 more)
Cohesion: 0.35
Nodes (30): BxUserContext, int, Session, str, approve_reimburse(), approved_by_me(), _build_details_data(), create_reimburse() (+22 more)
### Community 21 - "Community 21"
Cohesion: 0.07
Nodes (113): DbUserContext, 运输计划管理 - 基础数据 API(承运商、车辆、人员), PlVehicle, Session, str, VehicleCreate, VehicleRead, VehicleUpdate (+105 more)
Cohesion: 0.06
Nodes (95): DbUserContext, 运输计划管理 - 基础数据 API(承运商、车辆、人员), PlVehicle, Session, str, VehicleRead, date, 运输计划管理 - 基础数据 Schema(承运商、车辆、人员、证书) (+87 more)
### Community 22 - "Community 22"
Cohesion: 0.41
Nodes (13): BxUserContext, Session, str, UploadFile, bind_biz(), delete_attachment(), download_file(), list_by_biz() (+5 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 23 - "Community 23"
Cohesion: 0.24
Nodes (36): AdvanceIssueRequest, BxUserContext, int, ReimburseApplyOut, Session, str, datetime, str (+28 more)
Cohesion: 0.19
Nodes (40): AdvanceIssueRequest, BxUserContext, int, ReimburseApplyOut, Session, str, str, BatchOffsetRequest (+32 more)
### Community 24 - "Community 24"
Cohesion: 0.11
Nodes (36): Session, str, Any, bool, int, Session, str, Session (+28 more)
Cohesion: 0.14
Nodes (84): date, DbUserContext, int, Session, str, VehicleMaintenanceApplyCreate, VehicleMaintenanceApplyRead, VehicleMaintenanceApplyReminder (+76 more)
### Community 25 - "Community 25"
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.07
Nodes (98): attachment_download_url(), AttachmentDownloadUrlBody, create_dept(), create_dict_data(), create_dict_type(), create_menu(), create_post(), create_role_management() (+90 more)
### Community 26 - "Community 26"
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)
Cohesion: 0.21
Nodes (18): bool, int, Redis, Request, str, _claims_from_auth(), _coerce_post_method_override_to_get(), _get_redis_client() (+10 more)
### Community 27 - "Community 27"
Cohesion: 0.12
Nodes (54): Any, bool, int, Session, str, Any, bool, CRM 线索 CRUD(线索主表 + 转化/分配/无效/等级操作) (+46 more)
Cohesion: 0.11
Nodes (57): Any, bool, int, Session, str, Any, bool, CRM 线索 CRUD(线索主表 + 转化/分配/无效/等级操作) (+49 more)
### Community 28 - "Community 28"
Cohesion: 0.09
Nodes (41): 运输计划管理 - 基础数据 Schema(承运商、车辆、人员、证书), CarrierBase, CarrierCreate, CarrierRead, CarrierUpdate, DriverBase, DriverCreate, DriverRead (+33 more)
Cohesion: 0.11
Nodes (40): date, int, Session, str, CertRead, CertRow, PlCert, PlDocumentReminder (+32 more)
### Community 29 - "Community 29"
Cohesion: 0.10
Nodes (36): Any, bool, CRM 供方信息 CRUD — 对照 CrmSupplierInfoServiceImpl.java, int, Session, str, generate_contract_no(), Java: generateContractNo — {供方编码}/X-JT + yyyyMMdd + 3位流水 (+28 more)
Cohesion: 0.05
Nodes (43): invoice_info_list(), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list (+35 more)
### Community 30 - "Community 30"
Cohesion: 0.23
Nodes (46): BxUserContext, int, Session, str, BxUserContext, Session, str, ApiResponse (+38 more)
Cohesion: 0.08
Nodes (43): 删除供方 — Java: CrmSupplierInfoController.remove, 删除供方 — Java: CrmSupplierInfoController.remove, 删除供方 — Java: CrmSupplierInfoController.remove, 删除供方 — Java: CrmSupplierInfoController.remove, 删除供方 — Java: CrmSupplierInfoController.remove, 删除供方 — Java: CrmSupplierInfoController.remove, 删除供方 — Java: CrmSupplierInfoController.remove, 删除供方 — Java: CrmSupplierInfoController.remove (+35 more)
### Community 31 - "Community 31"
Cohesion: 0.34
Nodes (19): int, PlTransport, Session, str, UploadFile, _build_public_page(), _format_file_size(), _format_plan_date() (+11 more)
### Community 32 - "Community 32"
Cohesion: 0.12
Nodes (17): invoice_info_list(), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价) (+9 more)
Cohesion: 0.10
Nodes (96): date, DbUserContext, int, Session, str, VehicleRechargeApplyCreate, VehicleRechargeApplyRead, VehicleRechargeApplyUpdate (+88 more)
### Community 33 - "Community 33"
Cohesion: 0.12
Nodes (60): Any, bool, int, Session, str, Any, bool, CRM 客户 CRUD(客户主表 + 转移/成交/公海池操作) (+52 more)
Cohesion: 0.11
Nodes (55): pool_rule_toggle(), Any, int, Session, str, Any, bool, CRM 公海池规则 CRUD + 规则执行逻辑 (+47 more)
### Community 34 - "Community 34"
Cohesion: 0.23
Nodes (28): Engine, build_id_maps(), clip_text(), count_rows(), export_mapping(), fetch_rows(), legacy_id_to_str(), main() (+20 more)
Cohesion: 0.22
Nodes (29): Engine, Path, build_id_maps(), clip_text(), count_rows(), export_mapping(), fetch_rows(), legacy_id_to_str() (+21 more)
### Community 35 - "Community 35"
Cohesion: 0.38
Nodes (15): Any, bool, int, Session, str, create_site(), get_site(), list_sites() (+7 more)
Cohesion: 0.05
Nodes (37): str, AttachmentOut, ReimburseApplyCreate, Config, PageResult, ResponseModel, ContactByCustomerBody, ContactDetailBody (+29 more)
### Community 36 - "Community 36"
Cohesion: 0.13
Nodes (45): pool_rule_toggle(), Any, int, Session, str, Any, bool, CRM 公海池规则 CRUD + 规则执行逻辑 (+37 more)
Cohesion: 0.12
Nodes (26): API 路由包 — 山峡化工生产一体化门户, create_leave(), delete_leave(), get_leave(), _leave_local_callback(), leave_workflow_callback(), list_leaves(), submit_leave() (+18 more)
### Community 37 - "Community 37"
Cohesion: 0.33
Nodes (14): Any, int, Session, str, create_dict_data(), get_dict_data(), list_dict_data(), list_dict_data_options() (+6 more)
Cohesion: 0.31
Nodes (15): Any, int, Session, str, assert_row_deletable_by_creator(), create_dict_data(), get_dict_data(), list_dict_data() (+7 more)
### Community 38 - "Community 38"
Cohesion: 0.21
Nodes (18): bool, int, Redis, Request, str, _claims_from_auth(), _coerce_post_method_override_to_get(), _get_redis_client() (+10 more)
Cohesion: 0.15
Nodes (14): Any, 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, 申请退款 Args: out_trade_no: 商户订单号 total_amount: 订单 (+6 more)
### Community 39 - "Community 39"
Cohesion: 0.43
Nodes (7): datetime, int, str, generate_random_password(), get_current_time(), get_file_size_display(), parse_datetime()
Cohesion: 0.13
Nodes (33): datetime, int, str, date, datetime, str, format_datetime(), generate_id() (+25 more)
### Community 40 - "Community 40"
Cohesion: 0.16
Nodes (32): Any, bool, Decimal, int, Session, str, batch_insert_quote_products(), calculate_subtotal() (+24 more)
Cohesion: 0.13
Nodes (37): Any, bool, Decimal, int, Session, str, batch_insert_quote_products(), calculate_subtotal() (+29 more)
### Community 41 - "Community 41"
Cohesion: 0.25
Nodes (18): BxUserContext, Session, str, bool, BxUserContext, Session, str, create_expense_item() (+10 more)
Cohesion: 0.14
Nodes (11): Any, Session, str, _auto_fill_create_dept_id(), get_db(), ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID, get_db_current_user_optional(), 地磅模块认证适配:门户 JWT 用户 → db 业务上下文。 (+3 more)
### Community 42 - "Community 42"
Cohesion: 0.08
Nodes (25): 技术栈, 1. 创建虚拟环境, 项目结构, 1. 创建虚拟环境, 2. 安装依赖, 2. 安装依赖, 3. 配置环境变量, 快速开始 (+17 more)
### Community 43 - "Community 43"
Cohesion: 0.13
Nodes (32): Any, int, str, str, Any, str, BusinessException, InvalidStatusTransitionException (+24 more)
Cohesion: 0.07
Nodes (68): Any, int, str, Any, int, Session, str, Any (+60 more)
### Community 44 - "Community 44"
Cohesion: 0.25
Nodes (23): Any, bool, int, Session, str, create_role(), get_role(), get_role_data_scope() (+15 more)
Cohesion: 0.15
Nodes (33): CRM 报价单 API 路由 — 挂接到 crm.router, CRM 报价单 Pydantic Schemas, BaseModel, QuoteAcceptBody, QuoteApprovalApproveBody, QuoteApprovalListBody, QuoteApprovalRefreshNamesBody, QuoteApprovalWithdrawBody (+25 more)
### Community 45 - "Community 45"
Cohesion: 0.25
Nodes (8): 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
Cohesion: 0.32
Nodes (22): Any, CrmOpportunity, Decimal, int, Session, str, batch_add_opportunity_products(), _calc_subtotal() (+14 more)
### Community 46 - "Community 46"
Cohesion: 0.08
Nodes (37): Any, bool, str, Decimal, str, str, Any, bool (+29 more)
Cohesion: 0.19
Nodes (18): str, Any, bool, Session, str, CRM 模块 Excel 导出列定义(字段与字典类型对齐 Java *Vo @ExcelDictFormat)。, _yes_no(), _build_workbook() (+10 more)
### Community 47 - "Community 47"
Cohesion: 0.11
Nodes (70): ProcessDefCreateBody, ProcessDefUpdateBody, PublishVersionBody, 审批意见:前端传 commentText,兼容 comment。, 审批意见:前端传 commentText,兼容 comment。, StartInstanceBody, TaskActionBody, Any (+62 more)
Cohesion: 0.24
Nodes (24): approve_task(), _assert_wf_instance_in_scope(), _business_type_label(), create_process_def(), delete_process_def(), _enrich_task_list(), get_business_all_tasks(), _get_business_form_fields() (+16 more)
### Community 48 - "Community 48"
Cohesion: 0.25
Nodes (8): Java: withdrawApproval, Java: withdrawApproval, Java: withdrawApproval, Java: withdrawApproval, Java: withdrawApproval, Java: withdrawApproval, Java: withdrawApproval, withdraw_contract_approval()
Cohesion: 0.39
Nodes (8): bool, BxUserContext, Session, str, 按业务类型获取工作流;已知类型不存在时自动创建空定义(无节点)。, 按业务类型获取工作流;已知类型不存在时自动创建空定义(无节点)。, WorkflowService, BxWorkflowDef
### Community 49 - "Community 49"
Cohesion: 0.43
Nodes (7): _avatar_url_field(), _build_user_info_payload(), _jwt_user_payload(), 与 web-admin `userInfo` 字段约定一致。, 与 web-admin `userInfo` 字段约定一致。, str, SysUser
Cohesion: 0.07
Nodes (46): CRM 合同 API 路由 — GET/PUT/DELETE/POST 对齐原 Java CRM, CRM 合同 Pydantic Schemas — 对照 CrmContractBo / CrmContractVo, ContractApprovalActionBody, ContractApprovalApproveBody, ContractApprovalByContractBody, ContractApprovalExportBody, ContractApprovalListBody, ContractApprovalListQuery (+38 more)
### Community 50 - "Community 50"
Cohesion: 0.06
Nodes (150): date, DbUserContext, int, PlanCreateFull, PlPlan, Session, str, DbInboundOutbound (+142 more)
Cohesion: 0.08
Nodes (105): bool, PlCarrier, VehicleCreate, VehicleUpdate, bool, Decimal, int, PlanCreateFull (+97 more)
### Community 51 - "Community 51"
Cohesion: 0.08
Nodes (88): FreightSettlementDisburseBody, FreightSettlementSubmitApprovalBody, FreightSettlementUpdate, Any, bool, datetime, FreightSettlementDisburseBody, FreightSettlementSubmitApprovalBody (+80 more)
Cohesion: 0.05
Nodes (167): DbUserContext, FreightSettlementDisburseBody, FreightSettlementSubmitApprovalBody, FreightSettlementUpdate, int, Session, str, Any (+159 more)
### Community 52 - "Community 52"
Cohesion: 0.11
Nodes (58): CrmContract, Query, Any, bool, CrmContractProductPriceTier, date, Decimal, int (+50 more)
Nodes (46): Decimal, str, Any, bool, CrmContractProductPriceTier, date, Decimal, int (+38 more)
### Community 53 - "Community 53"
Cohesion: 0.50
Nodes (52): date, int, DocumentReminderRead, DocumentReminderUpdate, FreightSettlementCreate, FreightSettlementListResponse, FreightSettlementUpdate, FreightSettlementWithTransports (+44 more)
Cohesion: 0.18
Nodes (19): bytes, int, str, download_file_bytes(), file_stream(), get_access_url(), get_download_url(), get_preview_url() (+11 more)
### Community 54 - "Community 54"
Cohesion: 0.03
Nodes (67): 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list (+59 more)
Cohesion: 0.22
Nodes (17): Any, bool, Session, str, list_share_logs(), _parse_user_agent(), CRM 报价单分享访问日志 CRUD — 对照 CrmQuoteShareLogServiceImpl.java, Java: statisticsVisits (+9 more)
### Community 55 - "Community 55"
Cohesion: 0.29
Nodes (7): download_contract_pdf(), Java: downloadContractPdf — 返回合同文件 URL/路径, Java: batchAssignOwner, Java: downloadContractPdf — 返回合同文件 URL/路径, Java: downloadContractPdf — 返回合同文件 URL/路径, Java: downloadContractPdf — 返回合同文件 URL/路径, Java: downloadContractPdf — 返回合同文件 URL/路径
Cohesion: 0.28
Nodes (22): bool, CRM 产品模块 CRUD(产品分类 + 产品 + 附件), Session, str, barcode_exists(), category_code_exists(), create_category(), create_product() (+14 more)
### Community 56 - "Community 56"
Cohesion: 0.26
@@ -387,75 +401,79 @@ Nodes (22): Any, bool, datetime, int, Session, str, BizSampleCode, batch_generat
### Community 57 - "Community 57"
Cohesion: 0.10
Nodes (20): 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) (+12 more)
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)
### Community 58 - "Community 58"
Cohesion: 0.37
Nodes (17): check_env(), check_python(), create_venv(), init_env(), install_dependencies(), log_error(), log_info(), log_success() (+9 more)
### Community 59 - "Community 59"
Cohesion: 0.34
Nodes (16): Any, bool, int, Session, str, create_tenant(), get_tenant(), list_tenants() (+8 more)
Cohesion: 0.11
Nodes (22): bool, date, Session, str, get_last_month_range(), month_range(), 地磅计划按月自动生成 — 迁移自 simple-dibang-xiaoshou/auto-create-plan/auto_create_monthly_pla, 按月自动生成采购计划。 - 默认仅在每月 1 日执行(force=True 时跳过该限制,供手动触发验收) - 遍历所有租户,按上月计划模板生 (+14 more)
### Community 60 - "Community 60"
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)
### Community 61 - "Community 61"
Cohesion: 0.12
Nodes (46): Session, str, bool, date, DbInboundOutbound, DbPurchasePlan, float, PlPlan (+38 more)
### Community 62 - "Community 62"
Cohesion: 0.12
Nodes (45): bool, DbInboundOutbound, int, PublicInventoryFeedback, PublicInventoryPageRead, Session, str, UploadFile (+37 more)
### Community 63 - "Community 63"
Cohesion: 0.19
Nodes (48): AdvanceApplyCreate, AdvanceApplyOut, AdvanceApplyUpdate, AdvanceApprovalCreate, BxUserContext, int, Session, str (+40 more)
### Community 64 - "Community 64"
Cohesion: 0.19
Nodes (20): int, str, Any, bool, int, Session, str, business_type_from_path() (+12 more)
### Community 65 - "Community 65"
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.34
Nodes (16): Any, bool, int, Session, str, create_dict_type(), dict_type_code_exists(), get_dict_type() (+8 more)
### Community 61 - "Community 61"
Cohesion: 0.24
Nodes (24): approve_task(), _assert_wf_instance_in_scope(), _business_type_label(), create_process_def(), delete_process_def(), _enrich_task_list(), get_business_all_tasks(), _get_business_form_fields() (+16 more)
### Community 62 - "Community 62"
Cohesion: 0.10
Nodes (49): bool, DbInboundOutbound, int, PublicInventoryFeedback, PublicInventoryPageRead, Session, str, UploadFile (+41 more)
### Community 63 - "Community 63"
Cohesion: 0.07
Nodes (63): bool, date, Session, str, Session, str, bool, date (+55 more)
### Community 64 - "Community 64"
Cohesion: 0.33
Nodes (15): Any, int, Session, str, BizLeaveRequest, create_leave(), get_leave(), list_leaves() (+7 more)
### Community 65 - "Community 65"
Cohesion: 0.29
Nodes (10): datetime, int, Session, str, BizNoKind, allocate_next_biz_no(), _format_biz_no(), _prefix_for() (+2 more)
### Community 66 - "Community 66"
Cohesion: 0.31
Nodes (11): bool, BxUserContext, BxWorkflowNode, int, Session, str, 按业务类型获取工作流;已知类型不存在时自动创建空定义(无节点)。, 按业务类型获取工作流;已知类型不存在时自动创建空定义(无节点)。 (+3 more)
### Community 67 - "Community 67"
Cohesion: 0.14
Nodes (24): Any, bool, Session, str, Java: viewByShareCode, Java: viewByShareCode, Java: viewByShareCode, Java: viewByShareCode (+16 more)
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)
### Community 68 - "Community 68"
Cohesion: 0.40
Nodes (3): API 路由包 — 山峡化工生产一体化门户, get_menu_list(), 运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,14 个一级菜单)
Cohesion: 0.25
Nodes (18): BxUserContext, Session, str, bool, BxUserContext, Session, str, create_expense_item() (+10 more)
### Community 69 - "Community 69"
Cohesion: 0.35
Nodes (20): DbUserContext, float, Session, str, DashboardCargoStat, DashboardPartnerCargoStat, DashboardStats, _cargo_stats() (+12 more)
Cohesion: 0.27
Nodes (24): DbUserContext, float, Session, str, Session, str, DashboardCargoStat, DashboardPartnerCargoStat (+16 more)
### Community 70 - "Community 70"
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 71 - "Community 71"
Cohesion: 0.25
Nodes (11): check_customer_protect_time(), _execute_single_rule(), _get_all_tenant_nos(), pool_recovery_job(), 公海池自动回收任务(由 APScheduler 调度) 遍历所有租户的启用规则,执行自动回收。, 每天凌晨1点:检查保护期到期客户,跟进次数不足则退回公海, 每天早上9点:保护期到期提醒(3天内到期且跟进不足), remind_customer_recycle() (+3 more)
Cohesion: 0.22
Nodes (19): Any, bool, str, _actor_ids(), _actor_names(), assert_rows_deletable_by_creator(), can_delete_by_creator(), is_super_admin() (+11 more)
### Community 72 - "Community 72"
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 73 - "Community 73"
Cohesion: 0.13
Nodes (39): Any, bool, date, int, Session, str, Session, str (+31 more)
Cohesion: 0.30
Nodes (14): Any, bool, bytes, int, str, bytes, ApiCryptoError, _b64url_decode() (+6 more)
### Community 74 - "Community 74"
Cohesion: 0.22
Nodes (12): Any, bool, str, WfProcessInstance, _LocalCallbackFn, _dispatch_local(), 工作流回调服务 —— 流程状态变更时通知业务系统, 注册本地回调函数。当 callback_url 以 path 开头时直接调用 fn。 (+4 more)
### Community 75 - "Community 75"
Cohesion: 0.05
Nodes (152): Any, CRM 合同 API 路由 — GET/PUT/DELETE/POST 对齐原 Java CRM, register_contract_routes(), CRM 合同子模块路由 — 变更/模板/审批独立页, register_contract_sub_routes(), Any, bool, CRM 合同 CRUD — 对照 CrmContractServiceImpl.java(列表/详情 P1) (+144 more)
Nodes (135): date, DbUserContext, int, PlanCreateFull, PlPlan, Session, str, date (+127 more)
### Community 76 - "Community 76"
Cohesion: 0.18
@@ -466,116 +484,168 @@ 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 78 - "Community 78"
Cohesion: 0.05
Nodes (101): Any, bool, CRM 产品模块 CRUD(产品分类 + 产品 + 附件), int, Session, str, SysOss, int (+93 more)
Cohesion: 0.30
Nodes (21): Any, bool, CrmOpportunity, datetime, int, Session, str, ensure_crm_create_by_row_in_scope() (+13 more)
### Community 79 - "Community 79"
Cohesion: 0.36
Nodes (10): add_scope_before_order(), ensure_import(), patch_base_data(), patch_get_helper(), patch_inventory(), patch_plan(), patch_transport(), str (+2 more)
### Community 80 - "Community 80"
Cohesion: 0.24
Nodes (10): get_current_user_profile(), get_web_auth_menu_codes(), public_quote_share(), 获取当前用户信息(与 sys_user 字段对应)。, 获取当前用户信息(与 sys_user 字段对应)。, 获取当前用户菜单权限标识(sys_menu.perms,经角色继承)。, 获取当前用户菜单权限标识(sys_menu.perms,经角色继承)。, 公开分享查看报价单 — Java: GET /share/{shareCode}(无 JWT) (+2 more)
Cohesion: 0.14
Nodes (46): ProcessDefCreateBody, ProcessDefUpdateBody, PublishVersionBody, 审批意见:前端传 commentText,兼容 comment。, 审批意见:前端传 commentText,兼容 comment。, StartInstanceBody, TaskActionBody, Any (+38 more)
### Community 81 - "Community 81"
Cohesion: 0.31
Nodes (20): Any, bool, CrmOpportunity, datetime, int, Session, str, create_opportunity_follow() (+12 more)
Cohesion: 0.34
Nodes (16): Any, bool, int, Session, str, create_tenant(), get_tenant(), list_tenants() (+8 more)
### Community 82 - "Community 82"
Cohesion: 0.29
Nodes (9): AST, expr, field_has_alias(), iter_field_assignments(), main(), bool, int, str (+1 more)
### Community 83 - "Community 83"
Cohesion: 0.22
Nodes (9): 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 (+1 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 84 - "Community 84"
Cohesion: 0.29
Nodes (7): 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位序号}
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位序号}
### Community 85 - "Community 85"
Cohesion: 0.22
Nodes (57): InboundOutboundRead, bool, DbPurchasePlan, DbUserContext, PurchasePlanRead, PurchasePlanWithFillLink, Session, str (+49 more)
### Community 86 - "Community 86"
Cohesion: 0.03
Nodes (199): register_contract_routes(), CRM 合同子模块路由 — 变更/模板/审批独立页, register_contract_sub_routes(), Any, bool, CRM 合同 CRUD — 对照 CrmContractServiceImpl.java(列表/详情 P1), CrmContract, datetime (+191 more)
### Community 87 - "Community 87"
Cohesion: 0.15
Nodes (27): create_leave(), delete_leave(), get_leave(), _leave_local_callback(), leave_workflow_callback(), list_leaves(), submit_leave(), update_leave() (+19 more)
Cohesion: 0.13
Nodes (18): Any, _build_category_tree(), _collect_category_with_ancestors(), list_category_tree(), list_oss_by_biz(), list_product_options(), _oss_row_to_dict(), 获取产品分类树(嵌套 children,供树形表格/树选择器使用) (+10 more)
### Community 88 - "Community 88"
Cohesion: 0.18
Nodes (19): bytes, int, str, download_file_bytes(), file_stream(), get_access_url(), get_download_url(), get_preview_url() (+11 more)
Cohesion: 0.29
Nodes (16): Any, int, Session, str, BizLeaveRequest, create_leave(), get_leave(), list_leaves() (+8 more)
### Community 89 - "Community 89"
Cohesion: 0.32
Nodes (22): Any, CrmOpportunity, Decimal, int, Session, str, batch_add_opportunity_products(), _calc_subtotal() (+14 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 90 - "Community 90"
Cohesion: 0.17
Nodes (23): Any, bool, bytes, int, Request, str, bool, int (+15 more)
Cohesion: 0.16
Nodes (25): Any, bool, bytes, int, Request, str, bool, int (+17 more)
### Community 91 - "Community 91"
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 94 - "Community 94"
### Community 92 - "Community 92"
Cohesion: 0.20
Nodes (10): generate_contract_name(), Java: generateContractName, Java: generateContractName, Java: generateContractName, Java: generateContractName, Java: generateContractName, Java: generateContractName, Java: generateContractName (+2 more)
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)
### Community 93 - "Community 93"
Cohesion: 0.22
Nodes (8): ChangePasswordRequest, Config, LoginRequest, LoginResponse, Pydantic schemas - 用户认证, ResetPasswordRequest, UserCreate, UserResponse
### Community 94 - "Community 94"
Cohesion: 0.18
Nodes (11): generate_contract_name(), Java: generateContractName, Java: generateContractName, Java: generateContractName, Java: generateContractName, Java: generateContractName, Java: generateContractName, Java: generateContractName (+3 more)
### Community 95 - "Community 95"
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)
Cohesion: 0.22
Nodes (9): _category_row_to_dict(), list_categories(), 分类 ORM 对象转 dict(camelCase 键名), 分类 ORM 对象转 dict(camelCase 键名), 分类 ORM 对象转 dict(camelCase 键名), 分类 ORM 对象转 dict(camelCase 键名), 获取分类(扁平列表,支持名称模糊与状态筛选), 获取分类(扁平列表,支持名称模糊与状态筛选) (+1 more)
### Community 96 - "Community 96"
Cohesion: 0.38
Nodes (15): Any, bool, int, Session, str, create_post(), get_post(), list_posts() (+7 more)
### Community 97 - "Community 97"
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 98 - "Community 98"
Cohesion: 0.37
Nodes (16): Any, bool, int, Session, str, create_site(), get_site(), list_sites() (+8 more)
### Community 99 - "Community 99"
Cohesion: 0.30
Nodes (14): Any, bool, bytes, int, str, bytes, ApiCryptoError, _b64url_decode() (+6 more)
Cohesion: 0.33
Nodes (3): 地磅 API 路由聚合 — REST 风格,协议对齐原 simple-dibang-xiaoshou。, 地磅(db)Pydantic schemas — 对齐原 simple-dibang-xiaoshou 契约。, 地磅 API 路由聚合 — REST 风格,协议对齐原 simple-dibang-xiaoshou。
### Community 100 - "Community 100"
Cohesion: 0.25
Nodes (23): Any, bool, int, Session, str, create_role(), get_role(), get_role_data_scope() (+15 more)
### Community 101 - "Community 101"
Cohesion: 0.40
Nodes (3): BaseSettings, Config, Settings
### Community 102 - "Community 102"
Cohesion: 0.15
Nodes (13): _generate_plan_no(), 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD###### (+5 more)
Cohesion: 0.34
Nodes (23): DbUserContext, DbWeighingSlip, Session, str, weighing_to_read(), apply_product_snapshot_to_weighing(), _apply_weighing_product_update(), _apply_weighing_update() (+15 more)
### Community 103 - "Community 103"
Cohesion: 0.12
Nodes (87): bool, DbUserContext, float, int, PlPlan, PlTransport, Session, str (+79 more)
Cohesion: 0.09
Nodes (99): bool, DbUserContext, float, int, PlPlan, PlTransport, Session, str (+91 more)
### Community 104 - "Community 104"
Cohesion: 0.15
Nodes (35): logout(), 公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/shanxia2.0.sql 基线), 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。, 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。, Web 端登录:账号为 sys_user.user_no,密码为 sys_user.password(bcrypt)。, Web 端登录:账号为 sys_user.user_no,密码为 sys_user.password(bcrypt)。, refresh_token(), web_login() (+27 more)
Cohesion: 0.10
Nodes (50): get_current_user_profile(), get_web_auth_menu_codes(), logout(), 公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/shanxia2.0.sql 基线), 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。, 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。, 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。, 获取当前用户信息(与 sys_user 字段对应)。 (+42 more)
### Community 105 - "Community 105"
Cohesion: 0.17
Nodes (12): 删除供方 — Java: CrmSupplierInfoController.remove, 删除供方 — Java: CrmSupplierInfoController.remove, 删除供方 — Java: CrmSupplierInfoController.remove, 删除供方 — Java: CrmSupplierInfoController.remove, 删除供方 — Java: CrmSupplierInfoController.remove, 删除供方 — Java: CrmSupplierInfoController.remove, 删除供方 — Java: CrmSupplierInfoController.remove, 删除供方 — Java: CrmSupplierInfoController.remove (+4 more)
### Community 107 - "Community 107"
Cohesion: 0.33
Nodes (10): Any, int, Session, str, list_ethnics(), 民族码表 CRUD(全局 sys_ethnic), _row_to_dict(), 民族码表 sys_ethnic(国标 GB/T 3304,全局) (+2 more)
Nodes (5): CRM 客户管理 Pydantic Schemas, CustomerAssignPoolBody, CustomerCheckDuplicateBody, CustomerDetailBody, CRM 客户 CRUD(客户主表 + 转移/成交/公海池操作)
### Community 108 - "Community 108"
Cohesion: 0.34
Nodes (16): Any, int, Session, str, _build_tree(), _cascade_soft_delete(), _collect_with_ancestors(), create_dept() (+8 more)
### Community 109 - "Community 109"
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 112 - "Community 112"
Cohesion: 0.25
Nodes (8): batch_submit_contract_approval(), Java: batchSubmitApproval, Java: batchSubmitApproval, Java: batchSubmitApproval, Java: batchSubmitApproval, Java: batchSubmitApproval, Java: batchSubmitApproval, Java: batchSubmitApproval
Cohesion: 0.14
Nodes (16): Session, str, Session, str, get_user_menu_codes(), 菜单 code 权限守卫(V0.3:通过 sys_user_role → sys_role_menu → sys_menu.perms), 读取用户授权的菜单权限标识集合(与前端 meta.menuCode / perms 对齐)。, 接口菜单权限守卫。super_admin 直接放行。 (+8 more)
### Community 122 - "Community 122"
Cohesion: 0.24
Nodes (20): Session, str, Any, bool, Session, str, apply_crm_owner_scope(), ensure_crm_customer_linked_row_in_scope() (+12 more)
### Community 124 - "Community 124"
Cohesion: 0.41
Nodes (13): BxUserContext, Session, str, UploadFile, bind_biz(), delete_attachment(), download_file(), list_by_biz() (+5 more)
### Community 125 - "Community 125"
Cohesion: 0.40
Nodes (5): bool, str, get_password_hash(), 验证密码(哈希非 bcrypt 或损坏时返回 False,避免抛错), verify_password()
### Community 126 - "Community 126"
Cohesion: 0.43
Nodes (7): _avatar_url_field(), _build_user_info_payload(), _jwt_user_payload(), 与 web-admin `userInfo` 字段约定一致。, 与 web-admin `userInfo` 字段约定一致。, str, SysUser
### Community 127 - "Community 127"
Cohesion: 0.29
Nodes (10): datetime, int, Session, str, BizNoKind, allocate_next_biz_no(), _format_biz_no(), _prefix_for() (+2 more)
### Community 128 - "Community 128"
Cohesion: 0.40
Nodes (10): SysOss, int, Session, str, SysOss, create_oss(), create_oss(), get_oss_by_url() (+2 more)
### Community 129 - "Community 129"
Cohesion: 0.38
Nodes (6): date, datetime, str, _date_to_str(), _dt_to_str(), bx 原系统 API 响应包装(code=0 / msg / data.rows),与门户 success_response 分离。
Cohesion: 0.40
Nodes (3): AdvanceApplyCreate, AdvanceApplyUpdate, _coerce_optional_id()
### Community 131 - "Community 131"
Cohesion: 0.11
Nodes (33): bool, Session, str, _append_user_scope_conditions(), apply_data_scope(), assert_row_in_data_scope(), _collect_scope_ids(), _get_child_dept_ids() (+25 more)
Nodes (34): bool, Session, str, _append_user_scope_conditions(), apply_data_scope(), assert_row_in_data_scope(), _collect_scope_ids(), _get_child_dept_ids() (+26 more)
### Community 132 - "Community 132"
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()
### Community 133 - "Community 133"
Cohesion: 0.25
Nodes (8): Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签, renew_contract()
Cohesion: 0.18
Nodes (11): generate_product_no(), 生成产品编号(格式:CP + yyyyMMdd + 3位序号,如 CP20260530001), 生成产品编号(格式:CP + yyyyMMdd + 3位序号,如 CP20260530001), 生成产品编号(格式:CP + yyyyMMdd + 3位序号,如 CP20260530001), 生成产品编号(格式:CP + yyyyMMdd + 3位序号,如 CP20260530001), 生成产品编号(格式:CP + yyyyMMdd + 3位序号,如 CP20260530001), 生成产品编号(格式:CP + yyyyMMdd + 3位序号,如 CP20260530001), 生成产品编号(格式:CP + yyyyMMdd + 3位序号,如 CP20260530001) (+3 more)
### Community 134 - "Community 134"
Cohesion: 0.38
Nodes (4): str, _coerce_id(), ExpenseItemCreate, ExpenseItemUpdate
### Community 135 - "Community 135"
Cohesion: 0.25
@@ -583,38 +653,50 @@ Nodes (7): 1. 上传到服务器, 2. 服务器首次启动, 3. 日常运维, 4.
### Community 136 - "Community 136"
Cohesion: 0.11
Nodes (33): int, Any, str, bool, BxUserContext, int, Session, str (+25 more)
Nodes (32): Any, str, bool, BxUserContext, int, Session, str, Any (+24 more)
### Community 137 - "Community 137"
Cohesion: 0.18
Nodes (11): _get_quote_item_field(), Java: resolveProductIdForQuoteItem(CrmProduct 无 tax_rate,跳过税率匹配), Java: resolveProductIdForQuoteItem(CrmProduct 无 tax_rate,跳过税率匹配), Java: resolveProductIdForQuoteItem(CrmProduct 无 tax_rate,跳过税率匹配), Java: resolveProductIdForQuoteItem(CrmProduct 无 tax_rate,跳过税率匹配), Java: resolveProductIdForQuoteItem(CrmProduct 无 tax_rate,跳过税率匹配), Java: resolveProductIdForQuoteItem(CrmProduct 无 tax_rate,跳过税率匹配), Java: resolveProductIdForQuoteItem(CrmProduct 无 tax_rate,跳过税率匹配) (+3 more)
### Community 138 - "Community 138"
Cohesion: 0.39
Nodes (6): _inject_user_info_after_tenant(), patch_contract_routes(), patch_quote_routes(), patch_route_file(), Path, str
### Community 139 - "Community 139"
Cohesion: 0.33
Nodes (3): 地磅 API 路由聚合 — REST 风格,协议对齐原 simple-dibang-xiaoshou。, 地磅(db)Pydantic schemas — 对齐原 simple-dibang-xiaoshou 契约。, 地磅 API 路由聚合 — REST 风格,协议对齐原 simple-dibang-xiaoshou。
Cohesion: 0.20
Nodes (10): int, list_products(), _product_row_to_dict(), 产品 ORM 对象转 dict(camelCase 键名), 产品 ORM 对象转 dict(camelCase 键名), 产品 ORM 对象转 dict(camelCase 键名), 产品 ORM 对象转 dict(camelCase 键名), 产品 ORM 对象转 dict(camelCase 键名) (+2 more)
### Community 140 - "Community 140"
Cohesion: 0.83
Nodes (3): _add_user_info_kwarg(), patch_file(), str
### Community 143 - "Community 143"
Cohesion: 0.67
Nodes (3): Web 端登录用 RSA 公钥(PKCS#1 v1.5,与 JSEncrypt 一致), Web 端登录用 RSA 公钥(PKCS#1 v1.5,与 JSEncrypt 一致), web_login_rsa_public_key()
### Community 145 - "Community 145"
Cohesion: 0.33
Nodes (5): _coerce_optional_id(), WorkflowDefCreate, WorkflowDefUpdate, WorkflowNodeCreate, WorkflowNodeOut
### Community 146 - "Community 146"
Cohesion: 0.20
Nodes (10): generate_auto_product_no(), Java: generateAutoProductNo — SXWL + 8位随机大写字符, Java: generateAutoProductNo — SXWL + 8位随机大写字符, Java: generateAutoProductNo — SXWL + 8位随机大写字符, Java: generateAutoProductNo — SXWL + 8位随机大写字符, Java: generateAutoProductNo — SXWL + 8位随机大写字符, Java: generateAutoProductNo — SXWL + 8位随机大写字符, Java: generateAutoProductNo — SXWL + 8位随机大写字符 (+2 more)
### Community 147 - "Community 147"
Cohesion: 0.25
Nodes (8): generate_barcode(), 生成唯一条码(纯数字:yyyyMMdd + 6位序号,如 20260606000001), 生成唯一条码(纯数字:yyyyMMdd + 6位序号,如 20260606000001), 生成唯一条码(纯数字:yyyyMMdd + 6位序号,如 20260606000001), 生成唯一条码(纯数字:yyyyMMdd + 6位序号,如 20260606000001), 生成唯一条码(纯数字:yyyyMMdd + 6位序号,如 20260606000001), 生成唯一条码(纯数字:yyyyMMdd + 6位序号,如 20260606000001), 生成唯一条码(纯数字:yyyyMMdd + 6位序号,如 20260606000001)
## Knowledge Gaps
- **64 isolated node(s):** `Session`, `bool`, `bool`, `Config`, `bool` (+59 more)
- **66 isolated node(s):** `Session`, `bool`, `bool`, `Config`, `bool` (+61 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **4 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
- **1 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 1`, `Community 2`, `Community 131`, `Community 4`, `Community 133`, `Community 6`, `Community 7`, `Community 136`, `Community 8`, `Community 9`, `Community 11`, `Community 10`, `Community 12`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 21`, `Community 22`, `Community 23`, `Community 24`, `Community 27`, `Community 29`, `Community 30`, `Community 32`, `Community 33`, `Community 35`, `Community 36`, `Community 37`, `Community 40`, `Community 41`, `Community 44`, `Community 45`, `Community 46`, `Community 47`, `Community 48`, `Community 50`, `Community 51`, `Community 52`, `Community 53`, `Community 54`, `Community 55`, `Community 56`, `Community 57`, `Community 59`, `Community 60`, `Community 61`, `Community 63`, `Community 64`, `Community 65`, `Community 66`, `Community 67`, `Community 69`, `Community 73`, `Community 74`, `Community 77`, `Community 78`, `Community 80`, `Community 81`, `Community 83`, `Community 84`, `Community 87`, `Community 89`, `Community 90`, `Community 97`, `Community 102`, `Community 103`, `Community 104`, `Community 105`, `Community 107`, `Community 108`, `Community 112`?**
_High betweenness centrality (0.237) - this node is a cross-community bridge._
- **Why does `BaseModel` connect `Community 16` to `Community 0`, `Community 1`, `Community 2`, `Community 4`, `Community 6`, `Community 11`, `Community 12`, `Community 15`, `Community 20`, `Community 21`, `Community 149`, `Community 23`, `Community 28`, `Community 30`, `Community 31`, `Community 41`, `Community 47`, `Community 50`, `Community 53`, `Community 62`, `Community 69`, `Community 74`, `Community 87`, `Community 103`?**
_High betweenness centrality (0.060) - this node is a cross-community bridge._
- **Why does `generate_uuid()` connect `Community 63` to `Community 0`, `Community 4`, `Community 7`, `Community 136`, `Community 10`, `Community 11`, `Community 12`, `Community 14`, `Community 15`, `Community 17`, `Community 18`, `Community 19`, `Community 21`, `Community 24`, `Community 27`, `Community 29`, `Community 33`, `Community 35`, `Community 36`, `Community 37`, `Community 39`, `Community 40`, `Community 43`, `Community 44`, `Community 47`, `Community 50`, `Community 51`, `Community 52`, `Community 56`, `Community 59`, `Community 60`, `Community 64`, `Community 65`, `Community 66`, `Community 67`, `Community 71`, `Community 73`, `Community 74`, `Community 78`, `Community 81`, `Community 89`, `Community 97`, `Community 103`, `Community 104`, `Community 108`?**
_High betweenness centrality (0.042) - this node is a cross-community bridge._
- **Why does `Session` connect `Community 3` to `Community 1`, `Community 2`, `Community 4`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 11`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 22`, `Community 23`, `Community 25`, `Community 27`, `Community 29`, `Community 30`, `Community 33`, `Community 35`, `Community 36`, `Community 37`, `Community 39`, `Community 40`, `Community 41`, `Community 43`, `Community 45`, `Community 46`, `Community 47`, `Community 48`, `Community 50`, `Community 51`, `Community 52`, `Community 54`, `Community 55`, `Community 56`, `Community 57`, `Community 59`, `Community 60`, `Community 61`, `Community 63`, `Community 64`, `Community 66`, `Community 68`, `Community 69`, `Community 75`, `Community 77`, `Community 78`, `Community 80`, `Community 81`, `Community 84`, `Community 85`, `Community 86`, `Community 87`, `Community 88`, `Community 89`, `Community 90`, `Community 92`, `Community 95`, `Community 96`, `Community 97`, `Community 98`, `Community 100`, `Community 102`, `Community 103`, `Community 104`, `Community 112`, `Community 122`, `Community 124`, `Community 127`, `Community 128`, `Community 131`, `Community 133`, `Community 136`, `Community 137`, `Community 139`, `Community 146`, `Community 147`?**
_High betweenness centrality (0.212) - this node is a cross-community bridge._
- **Why does `BaseModel` connect `Community 35` to `Community 0`, `Community 129`, `Community 2`, `Community 4`, `Community 134`, `Community 11`, `Community 12`, `Community 16`, `Community 145`, `Community 20`, `Community 21`, `Community 23`, `Community 24`, `Community 25`, `Community 28`, `Community 31`, `Community 32`, `Community 36`, `Community 44`, `Community 49`, `Community 50`, `Community 51`, `Community 62`, `Community 63`, `Community 65`, `Community 68`, `Community 69`, `Community 75`, `Community 80`, `Community 85`, `Community 93`, `Community 102`, `Community 103`, `Community 105`?**
_High betweenness centrality (0.066) - this node is a cross-community bridge._
- **Why does `SysUser` connect `Community 80` to `Community 2`, `Community 131`, `Community 3`, `Community 6`, `Community 7`, `Community 9`, `Community 11`, `Community 12`, `Community 14`, `Community 17`, `Community 19`, `Community 22`, `Community 24`, `Community 25`, `Community 26`, `Community 27`, `Community 32`, `Community 33`, `Community 39`, `Community 43`, `Community 47`, `Community 50`, `Community 51`, `Community 52`, `Community 56`, `Community 64`, `Community 86`, `Community 98`, `Community 103`, `Community 104`, `Community 126`?**
_High betweenness centrality (0.050) - this node is a cross-community bridge._
- **Are the 251 inferred relationships involving `str` (e.g. with `AdvanceApplyCreate` and `AdvanceApplyOut`) actually correct?**
_`str` has 251 INFERRED edges - model-reasoned connections that need verification._
- **Are the 248 inferred relationships involving `Session` (e.g. with `AdvanceApplyCreate` and `AdvanceApplyOut`) actually correct?**
@@ -622,4 +704,4 @@ _Questions this graph is uniquely positioned to answer:_
- **Are the 140 inferred relationships involving `int` (e.g. with `AdvanceApplyCreate` and `AdvanceApplyOut`) actually correct?**
_`int` has 140 INFERRED edges - model-reasoned connections that need verification._
- **What connects `数据库异常:返回 JSON 并保留 CORS 头,避免浏览器只报跨域错误`, `未捕获异常:统一 JSON 500,确保跨域场景下仍有 Access-Control-Allow-Origin`, `每月 1 日凌晨执行:按上月计划模板生成本月计划。` to the rest of the system?**
_995 weakly-connected nodes found - possible documentation gaps or missing edges._
_1184 weakly-connected nodes found - possible documentation gaps or missing edges._
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_role_py", "label": "role.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/role.py", "source_location": "L1"}, {"id": "crud_role_row_to_dict", "label": "_row_to_dict()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/role.py", "source_location": "L21"}, {"id": "sysrole", "label": "SysRole", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/role.py", "source_location": "L21"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/role.py", "source_location": "L21"}, {"id": "any", "label": "Any", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/role.py", "source_location": "L21"}, {"id": "crud_role_total_pages", "label": "total_pages()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/role.py", "source_location": "L36"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/role.py", "source_location": "L36"}, {"id": "crud_role_list_roles", "label": "list_roles()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/role.py", "source_location": "L47"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/role.py", "source_location": "L47"}, {"id": "crud_role_get_role", "label": "get_role()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/role.py", "source_location": "L82"}, {"id": "crud_role_role_name_exists", "label": "role_name_exists()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/role.py", "source_location": "L90"}, {"id": "bool", "label": "bool", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/role.py", "source_location": "L90"}, {"id": "crud_role_create_role", "label": "create_role()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/role.py", "source_location": "L104"}, {"id": "crud_role_update_role", "label": "update_role()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/role.py", "source_location": "L141"}, {"id": "crud_role_soft_delete_role", "label": "soft_delete_role()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/role.py", "source_location": "L178"}, {"id": "crud_role_get_role_menu_ids", "label": "get_role_menu_ids()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/role.py", "source_location": "L201"}, {"id": "crud_role_update_role_menus", "label": "update_role_menus()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/role.py", "source_location": "L210"}, {"id": "crud_role_get_role_data_scope", "label": "get_role_data_scope()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/role.py", "source_location": "L245"}, {"id": "crud_role_update_role_data_scope", "label": "update_role_data_scope()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/role.py", "source_location": "L254"}, {"id": "crud_role_rationale_1", "label": "\u89d2\u8272 CRUD\uff08sys_role / sys_role_menu / sys_role_data_scope_detail\uff09", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/role.py", "source_location": "L1"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_role_py", "target": "math", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/role.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_role_py", "target": "datetime", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/role.py", "source_location": "L5", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_role_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/role.py", "source_location": "L6", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_role_py", "target": "sqlalchemy", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/role.py", "source_location": "L8", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_role_py", "target": "sqlalchemy_orm", "relation"Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_pl_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/__init__.py", "source_location": "L1"}, {"id": "pl_init_rationale_1", "label": "\u8fd0\u8f93\u8ba1\u5212\u7ba1\u7406 - Pydantic Schema", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/__init__.py", "source_location": "L1"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_pl_init_py", "target": "app_schemas_pl_base_data", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/__init__.py", "source_location": "L2", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_pl_init_py", "target": "app_schemas_pl_plan", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/__init__.py", "source_location": "L28", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_pl_init_py", "target": "app_schemas_pl_transport", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/__init__.py", "source_location": "L44", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_pl_init_py", "target": "app_schemas_pl_freight", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/__init__.py", "source_location": "L57", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_pl_init_py", "target": "app_schemas_pl_inventory", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/__init__.py", "source_location": "L69", "weight": 1.0}, {"source": "pl_init_rationale_1", "target": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_pl_init_py", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/__init__.py", "source_location": "L1", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_tenant_py", "label": "tenant.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/tenant.py", "source_location": "L1"}, {"id": "crud_tenant_row_to_dict", "label": "_row_to_dict()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/tenant.py", "source_location": "L17"}, {"id": "systenant", "label": "SysTenant", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/tenant.py", "source_location": "L17"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/tenant.py", "source_location": "L17"}, {"id": "any", "label": "Any", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/tenant.py", "source_location": "L17"}, {"id": "crud_tenant_list_tenants", "label": "list_tenants()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/tenant.py", "source_location": "L32"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/tenant.py", "source_location": "L32"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/tenant.py", "source_location": "L32"}, {"id": "crud_tenant_get_tenant", "label": "get_tenant()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/tenant.py", "source_location": "L64"}, {"id": "crud_tenant_tenant_no_exists", "label": "tenant_no_exists()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/tenant.py", "source_location": "L72"}, {"id": "bool", "label": "bool", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/tenant.py", "source_location": "L72"}, {"id": "crud_tenant_create_tenant", "label": "create_tenant()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/tenant.py", "source_location": "L79"}, {"id": "crud_tenant_update_tenant", "label": "update_tenant()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/tenant.py", "source_location": "L116"}, {"id": "crud_tenant_soft_delete_tenant", "label": "soft_delete_tenant()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/tenant.py", "source_location": "L158"}, {"id": "crud_tenant_total_pages", "label": "total_pages()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/tenant.py", "source_location": "L171"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_tenant_py", "target": "math", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/tenant.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_tenant_py", "target": "datetime", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/tenant.py", "source_location": "L5", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_tenant_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/tenant.py", "source_location": "L6", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_tenant_py", "target": "sqlalchemy", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/tenant.py", "source_location": "L8", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_tenant_py", "target": "sqlalchemy_orm", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/tenant.py", "source_location": "L9", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_tenant_py", "target": "app_core_config", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/tenant.py", "source_location": "L11", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_tenant_py", "target": "app_core_delete_guard", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/tenant.py", "source_location": "L12", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_tenant_py", "target": "app_core_utils", "relation": "imports_from", "context": "import", "confidence": "EXTLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_models_models_py", "label": "models.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py", "source_location": "L1"}, {"id": "models_models_userrole", "label": "UserRole", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py", "source_location": "L11"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "models_models_sysuser", "label": "SysUser", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py", "source_location": "L29"}, {"id": "base", "label": "Base", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "models_models_sysrole", "label": "SysRole", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py", "source_location": "L56"}, {"id": "models_models_sysuserrole", "label": "SysUserRole", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py", "source_location": "L77"}, {"id": "models_models_sysrolemenu", "label": "SysRoleMenu", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py", "source_location": "L97"}, {"id": "models_models_sysroledatascopedetail", "label": "SysRoleDataScopeDetail", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py", "source_location": "L117"}, {"id": "models_models_sysmenu", "label": "SysMenu", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py", "source_location": "L138"}, {"id": "models_models_sysdept", "label": "SysDept", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py", "source_location": "L165"}, {"id": "models_models_systenant", "label": "SysTenant", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py", "source_location": "L189"}, {"id": "models_models_syssite", "label": "SysSite", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py", "source_location": "L210"}, {"id": "models_models_syspost", "label": "SysPost", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py", "source_location": "L236"}, {"id": "models_models_sysuserpost", "label": "SysUserPost", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py", "source_location": "L257"}, {"id": "models_models_sysdicttype", "label": "SysDictType", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py", "source_location": "L282"}, {"id": "models_models_sysethnic", "label": "SysEthnic", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py", "source_location": "L302"}, {"id": "models_models_sysdictdata", "label": "SysDictData", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py", "source_location": "L322"}, {"id": "models_models_sysapirequestlog", "label": "SysApiRequestLog", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py", "source_location": "L354"}, {"id": "models_models_sysapirequestloghis", "label": "SysApiRequestLogHis", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py", "source_location": "L386"}, {"id": "models_models_bizleaverequest", "label": "BizLeaveRequest", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py", "source_location": "L428"}, {"id": "models_models_bizattachment", "label": "BizAttachment", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py", "source_location": "L458"}, {"id": "models_models_wfprocessdef", "label": "WfProcessDef", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py", "source_location": "L488"}, {"id": "models_models_wfprocessversion", "label": "WfProcessVersion", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py", "source_location": "L509"}, {"id": "models_models_wfnode", "label": "WfNode", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py", "source_location": "L531"}, {"id": "models_models_wftransition", "label": "WfTransition", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py", "source_location": "L554"}, {"id": "models_models_wfprocessinstance", "label": "WfProcessInstance", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py", "source_lLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_admin_py", "label": "admin.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py", "source_location": "L1"}, {"id": "api_admin_list_roles_for_select", "label": "list_roles_for_select()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py", "source_location": "L42"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py", "source_location": "L42"}, {"id": "api_admin_list_users_for_select", "label": "list_users_for_select()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py", "source_location": "L74"}, {"id": "api_admin_list_roles_for_designer", "label": "list_roles_for_designer()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py", "source_location": "L97"}, {"id": "api_admin_tenantcreatebody", "label": "TenantCreateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py", "source_location": "L117"}, {"id": "basemodel", "label": "BaseModel", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "api_admin_tenantupdatebody", "label": "TenantUpdateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py", "source_location": "L128"}, {"id": "api_admin_list_tenants", "label": "list_tenants()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py", "source_location": "L140"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py", "source_location": "L140"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py", "source_location": "L140"}, {"id": "api_admin_create_tenant", "label": "create_tenant()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py", "source_location": "L161"}, {"id": "api_admin_update_tenant", "label": "update_tenant()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py", "source_location": "L183"}, {"id": "api_admin_delete_tenant", "label": "delete_tenant()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py", "source_location": "L208"}, {"id": "api_admin_get_tenant_no", "label": "_get_tenant_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py", "source_location": "L225"}, {"id": "api_admin_usercreatebody", "label": "UserCreateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py", "source_location": "L238"}, {"id": "api_admin_userupdatebody", "label": "UserUpdateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py", "source_location": "L255"}, {"id": "api_admin_list_users", "label": "list_users()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py", "source_location": "L271"}, {"id": "api_admin_get_user_by_id", "label": "get_user_by_id()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py", "source_location": "L315"}, {"id": "api_admin_create_user", "label": "create_user()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py", "source_location": "L335"}, {"id": "api_admin_update_user", "label": "update_user()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py", "source_location": "L368"}, {"id": "api_admin_delete_user", "label": "delete_user()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py", "source_location": "L398"}, {"id": "api_admin_reset_user_password", "label": "reset_user_password()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py", "source_location": "L411"}, {"id": "api_admin_dicttypecreatebody", "label": "DictTypeCreateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py", "source_location": "L428"}, {"id": "api_admin_dicttypeupdatebody", "label": "DictTypeUpdateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py", "source_location": "L437"}, {"id": "api_admin_list_dict_types", "label": "list_dict_types()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py", "source_location": "L447"}, {"id": "api_admin_create_dict_type", "label": "create_dict_type()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_vehicle_urea_service_py", "label": "pl_vehicle_urea_service.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_urea_service.py", "source_location": "L1"}, {"id": "services_pl_vehicle_urea_service_generate_seq_no", "label": "_generate_seq_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_urea_service.py", "source_location": "L27"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_urea_service.py", "source_location": "L27"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_urea_service.py", "source_location": "L27"}, {"id": "services_pl_vehicle_urea_service_generate_apply_no", "label": "generate_apply_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_urea_service.py", "source_location": "L39"}, {"id": "services_pl_vehicle_urea_service_generate_disburse_no", "label": "generate_disburse_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_urea_service.py", "source_location": "L45"}, {"id": "services_pl_vehicle_urea_service_resolve_user_name", "label": "_resolve_user_name()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_urea_service.py", "source_location": "L51"}, {"id": "services_pl_vehicle_urea_service_assert_vehicle_exists", "label": "_assert_vehicle_exists()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_urea_service.py", "source_location": "L62"}, {"id": "services_pl_vehicle_urea_service_resolve_user_id", "label": "_resolve_user_id()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_urea_service.py", "source_location": "L76"}, {"id": "dbusercontext", "label": "DbUserContext", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_urea_service.py", "source_location": "L76"}, {"id": "services_pl_vehicle_urea_service_validate_apply_dates", "label": "_validate_apply_dates()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_urea_service.py", "source_location": "L100"}, {"id": "date", "label": "date", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_urea_service.py", "source_location": "L100"}, {"id": "services_pl_vehicle_urea_service_get_apply_row", "label": "_get_apply_row()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_urea_service.py", "source_location": "L105"}, {"id": "plvehicleureaapply", "label": "PlVehicleUreaApply", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_urea_service.py", "source_location": "L105"}, {"id": "services_pl_vehicle_urea_service_get_disburse_row", "label": "_get_disburse_row()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_urea_service.py", "source_location": "L123"}, {"id": "plvehicleureadisburse", "label": "PlVehicleUreaDisburse", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_urea_service.py", "source_location": "L123"}, {"id": "services_pl_vehicle_urea_service_has_active_disburse", "label": "_has_active_disburse()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_urea_service.py", "source_location": "L141"}, {"id": "bool", "label": "bool", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_urea_service.py", "source_location": "L141"}, {"id": "services_pl_vehicle_urea_service_apply_to_read", "label": "apply_to_read()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_urea_service.py", "source_location": "L154"}, {"id": "vehicleureaapplyread", "label": "VehicleUreaApplyRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_urea_service.py", "source_location": "L154"}, {"id": "services_pl_vehicle_urea_service_disburse_to_read", "label": "disburse_to_read()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_urea_service.py", "source_location": "L174"}, {"id": "vehicleureadisburseread", "label": "VehicleUreaDisburseRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_urea_service.py", "source_location": "L174"}, {"id":Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_pl_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/__init__.py", "source_location": "L1"}, {"id": "pl_init_rationale_1", "label": "\u8fd0\u8f93\u8ba1\u5212\u7ba1\u7406 - Pydantic Schema", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/__init__.py", "source_location": "L1"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_pl_init_py", "target": "app_schemas_pl_base_data", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/__init__.py", "source_location": "L2", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_pl_init_py", "target": "app_schemas_pl_plan", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/__init__.py", "source_location": "L29", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_pl_init_py", "target": "app_schemas_pl_transport", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/__init__.py", "source_location": "L45", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_pl_init_py", "target": "app_schemas_pl_freight", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/__init__.py", "source_location": "L58", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_pl_init_py", "target": "app_schemas_pl_inventory", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/__init__.py", "source_location": "L70", "weight": 1.0}, {"source": "pl_init_rationale_1", "target": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_pl_init_py", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/__init__.py", "source_location": "L1", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_monitor_oper_log_py", "label": "oper_log.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/monitor/oper_log.py", "source_location": "L1"}, {"id": "monitor_oper_log_oper_status", "label": "_oper_status()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/monitor/oper_log.py", "source_location": "L16"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/monitor/oper_log.py", "source_location": "L16"}, {"id": "monitor_oper_log_row_to_oper_log", "label": "_row_to_oper_log()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/monitor/oper_log.py", "source_location": "L20"}, {"id": "any", "label": "Any", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/monitor/oper_log.py", "source_location": "L20"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/monitor/oper_log.py", "source_location": "L20"}, {"id": "monitor_oper_log_apply_oper_log_scope", "label": "_apply_oper_log_scope()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/monitor/oper_log.py", "source_location": "L56"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/monitor/oper_log.py", "source_location": "L56"}, {"id": "monitor_oper_log_oper_log_row_in_scope", "label": "_oper_log_row_in_scope()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/monitor/oper_log.py", "source_location": "L102"}, {"id": "bool", "label": "bool", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/monitor/oper_log.py", "source_location": "L102"}, {"id": "monitor_oper_log_list_oper_logs", "label": "list_oper_logs()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/monitor/oper_log.py", "source_location": "L112"}, {"id": "monitor_oper_log_get_oper_log", "label": "get_oper_log()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/monitor/oper_log.py", "source_location": "L180"}, {"id": "monitor_oper_log_rationale_1", "label": "\u64cd\u4f5c\u65e5\u5fd7\uff1a\u57fa\u4e8e sys_api_request_log \u9002\u914d RuoYi OperLog \u89c6\u56fe", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/monitor/oper_log.py", "source_location": "L1"}, {"id": "monitor_oper_log_rationale_57", "label": "\u64cd\u4f5c\u65e5\u5fd7\u6309 oper_user_id\uff08user_id/create_by\uff09\u5e94\u7528\u6570\u636e\u6743\u9650\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/monitor/oper_log.py", "source_location": "L57"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_monitor_oper_log_py", "target": "datetime", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/monitor/oper_log.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_monitor_oper_log_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/monitor/oper_log.py", "source_location": "L5", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_monitor_oper_log_py", "target": "sqlalchemy", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/monitor/oper_log.py", "source_location": "L7", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_monitor_oper_log_py", "target": "sqlalchemy_orm", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/monitor/oper_log.py", "source_location": "L8", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_monitor_oper_log_py", "target": "app_core_oper_log_infer", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/monitor/oper_log.py", "source_location": "L10", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_monitor_oper_log_py", "target": "app_models_models", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/monitor/oper_log.py", "source_location": "L11", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_monitor_oper_log_py"Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_core_oper_log_context_py", "label": "oper_log_context.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/oper_log_context.py", "source_location": "L1"}, {"id": "core_oper_log_context_operlogmeta", "label": "OperLogMeta", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/oper_log_context.py", "source_location": "L10"}, {"id": "core_oper_log_context_set_oper_log_meta", "label": "set_oper_log_meta()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/oper_log_context.py", "source_location": "L20"}, {"id": "core_oper_log_context_get_oper_log_meta", "label": "get_oper_log_meta()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/oper_log_context.py", "source_location": "L24"}, {"id": "core_oper_log_context_clear_oper_log_meta", "label": "clear_oper_log_meta()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/oper_log_context.py", "source_location": "L28"}, {"id": "core_oper_log_context_snapshot_oper_log_meta", "label": "snapshot_oper_log_meta()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/oper_log_context.py", "source_location": "L32"}, {"id": "core_oper_log_context_rationale_1", "label": "\u8bf7\u6c42\u7ea7\u64cd\u4f5c\u65e5\u5fd7\u5143\u6570\u636e\uff08ContextVar\uff09", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/oper_log_context.py", "source_location": "L1"}, {"id": "core_oper_log_context_rationale_33", "label": "\u4f9b\u5f02\u6b65\u843d\u5e93\u7ebf\u7a0b\u4f7f\u7528\uff0c\u907f\u514d ContextVar \u8de8\u7ebf\u7a0b\u4e22\u5931\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/oper_log_context.py", "source_location": "L33"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_core_oper_log_context_py", "target": "contextvars", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/oper_log_context.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_core_oper_log_context_py", "target": "dataclasses", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/oper_log_context.py", "source_location": "L5", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_core_oper_log_context_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/oper_log_context.py", "source_location": "L6", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_core_oper_log_context_py", "target": "core_oper_log_context_operlogmeta", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/oper_log_context.py", "source_location": "L10", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_core_oper_log_context_py", "target": "core_oper_log_context_set_oper_log_meta", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/oper_log_context.py", "source_location": "L20", "weight": 1.0}, {"source": "core_oper_log_context_set_oper_log_meta", "target": "core_oper_log_context_operlogmeta", "relation": "references", "context": "parameter_type", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/oper_log_context.py", "source_location": "L20", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_core_oper_log_context_py", "target": "core_oper_log_context_get_oper_log_meta", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/oper_log_context.py", "source_location": "L24", "weight": 1.0}, {"source": "core_oper_log_context_get_oper_log_meta", "target": "core_oper_log_context_operlogmeta", "relation": "references", "context": "generic_arg", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/oper_log_context.py", "source_location": "L24", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_core_oper_log_context_py", "target": "core_oper_log_context_clear_oper_log_meta", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/oper_log_context.py", "source_location": "L28", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_core_oper_log_context_py", "target": "core_oper_log_context_snapshot_oper_log_meta", "relation": "contains", "confLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_db_plans_py", "label": "plans.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L1"}, {"id": "db_plans_base_query", "label": "_base_query()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L45"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L45"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L45"}, {"id": "db_plans_list_plans", "label": "list_plans()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L53"}, {"id": "dbusercontext", "label": "DbUserContext", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L53"}, {"id": "purchaseplanwithfilllink", "label": "PurchasePlanWithFillLink", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L53"}, {"id": "db_plans_generate_plan_no", "label": "_generate_plan_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L191"}, {"id": "db_plans_create_plan", "label": "create_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L198"}, {"id": "purchaseplancreate", "label": "PurchasePlanCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L198"}, {"id": "purchaseplanread", "label": "PurchasePlanRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L198"}, {"id": "db_plans_autogeneratemonthlyresult", "label": "AutoGenerateMonthlyResult", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L241"}, {"id": "basemodel", "label": "BaseModel", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "db_plans_run_auto_generate_monthly", "label": "run_auto_generate_monthly()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L248"}, {"id": "bool", "label": "bool", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L248"}, {"id": "db_plans_get_plan", "label": "get_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L262"}, {"id": "db_plans_update_plan", "label": "update_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L273"}, {"id": "purchaseplanupdate", "label": "PurchasePlanUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L273"}, {"id": "db_plans_delete_plan", "label": "delete_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L291"}, {"id": "dbpurchaseplan", "label": "DbPurchasePlan", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L303"}, {"id": "db_plans_get_plan_by_id_only", "label": "_get_plan_by_id_only()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L320"}, {"id": "db_plans_apply_plan_product_update", "label": "_apply_plan_product_update()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L334"}, {"id": "db_plans_apply_plan_update", "label": "_apply_plan_update()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L350"}, {"id": "db_plans_planrelateddata", "label": "PlanRelatedData", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L371"}, {"id": "db_plans_get_plan_related", "label": "get_plan_related()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L377"}, {"id": "db_plans_thirdpartyplanpagedata", "label": "ThirdPartyPlanPageData", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L418"}, {"id": "db_plans_get_third_party_plan_page", "label": "get_third_party_plan_page()", "file_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": "L326"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L326"}, {"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": "L357"}, {"id": "customerlistbody", "label": "CustomerListBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L357"}, {"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": "L391"}, {"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": "L417"}, {"id": "customercreatebody", "label": "CustomerCreateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L417"}, {"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": "L446"}, {"id": "customerupdatebody", "label": "CustomerUpdateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L446"}, {"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": "L472"}, {"id": "customerdeletebody", "label": "CustomerDeleteBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L472"}, {"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": "L500"}, {"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": "L516"}, {"id": "productcategorycreatebody", "label": "ProductCategoryCreateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L516"}, {"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": "L536"}, {"id": "productcategoryupdatebody", "label": "ProductCategoryUpdateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L536"}, {"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": "L555"}, {"id": "categorydeletebody", "label": "CategoryDeleteBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L555"}, {"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": "L574"}, {"id": "productlistbody", "label": "ProductListBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L574"}, {"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": "L610"}, {"id": "productdetailbody", "label": "ProductDetailBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L610"}, {"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": "L631"}, {"id": "productcreatebody", "label": "ProductCreateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L631"}, {"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": "L655"}, {"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_crud_crm_supplier_info_py", "label": "supplier_info.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/supplier_info.py", "source_location": "L1"}, {"id": "crm_supplier_info_total_pages", "label": "total_pages()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/supplier_info.py", "source_location": "L36"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/supplier_info.py", "source_location": "L36"}, {"id": "crm_supplier_info_row_to_dict", "label": "_row_to_dict()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/supplier_info.py", "source_location": "L42"}, {"id": "crmsupplierinfo", "label": "CrmSupplierInfo", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/supplier_info.py", "source_location": "L42"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/supplier_info.py", "source_location": "L42"}, {"id": "any", "label": "Any", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/supplier_info.py", "source_location": "L42"}, {"id": "crm_supplier_info_apply_list_filters", "label": "_apply_list_filters()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/supplier_info.py", "source_location": "L65"}, {"id": "crm_supplier_info_list_supplier_info", "label": "list_supplier_info()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/supplier_info.py", "source_location": "L74"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/supplier_info.py", "source_location": "L74"}, {"id": "crm_supplier_info_list_all_supplier_info", "label": "list_all_supplier_info()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/supplier_info.py", "source_location": "L102"}, {"id": "crm_supplier_info_get_supplier_info", "label": "get_supplier_info()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/supplier_info.py", "source_location": "L122"}, {"id": "crm_supplier_info_create_supplier_info", "label": "create_supplier_info()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/supplier_info.py", "source_location": "L138"}, {"id": "crm_supplier_info_update_supplier_info", "label": "update_supplier_info()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/supplier_info.py", "source_location": "L176"}, {"id": "crm_supplier_info_soft_delete_supplier_info", "label": "soft_delete_supplier_info()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/supplier_info.py", "source_location": "L212"}, {"id": "bool", "label": "bool", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/supplier_info.py", "source_location": "L212"}, {"id": "crm_supplier_info_rationale_1", "label": "CRM \u4f9b\u65b9\u4fe1\u606f CRUD \u2014 \u5bf9\u7167 CrmSupplierInfoServiceImpl.java", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/supplier_info.py", "source_location": "L1"}, {"id": "crm_supplier_info_rationale_43", "label": "ORM \u2192 camelCase\uff08\u5355\u70b9\u771f\u76f8\uff09", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/supplier_info.py", "source_location": "L43"}, {"id": "crm_supplier_info_rationale_66", "label": "Java: buildQueryWrapper", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/supplier_info.py", "source_location": "L66"}, {"id": "crm_supplier_info_rationale_110", "label": "Java: queryList\uff08\u4e0d\u5206\u9875\uff0c\u62a5\u4ef7\u5355\u4e0b\u62c9\uff09", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/supplier_info.py", "source_location": "L110"}, {"id": "crm_supplier_info_rationale_220", "label": "Java: deleteWithValidByIds\uff08\u95e8\u6237\u8f6f\u5220\uff09", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/supplier_info.py", "source_location": "L220"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_crm_supplier_info_py", "target": "math", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/supplier_info.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_crm_supplier_info_py", "target":Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_core_api_request_log_middleware_py", "label": "api_request_log_middleware.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py", "source_location": "L1"}, {"id": "core_api_request_log_middleware_truncate", "label": "_truncate()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py", "source_location": "L37"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py", "source_location": "L37"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py", "source_location": "L37"}, {"id": "core_api_request_log_middleware_truncate_utf8_bytes", "label": "_truncate_utf8_bytes()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py", "source_location": "L49"}, {"id": "core_api_request_log_middleware_redact_obj", "label": "_redact_obj()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py", "source_location": "L65"}, {"id": "any", "label": "Any", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py", "source_location": "L65"}, {"id": "core_api_request_log_middleware_format_body_bytes", "label": "_format_body_bytes()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py", "source_location": "L84"}, {"id": "bytes", "label": "bytes", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py", "source_location": "L84"}, {"id": "core_api_request_log_middleware_claims_from_auth", "label": "_claims_from_auth()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py", "source_location": "L111"}, {"id": "core_api_request_log_middleware_should_skip_exclude", "label": "_should_skip_exclude()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py", "source_location": "L137"}, {"id": "bool", "label": "bool", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py", "source_location": "L137"}, {"id": "core_api_request_log_middleware_should_skip", "label": "_should_skip()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py", "source_location": "L150"}, {"id": "request", "label": "Request", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py", "source_location": "L150"}, {"id": "core_api_request_log_middleware_persist_log", "label": "_persist_log()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py", "source_location": "L163"}, {"id": "core_api_request_log_middleware_schedule_persist_log", "label": "_schedule_persist_log()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py", "source_location": "L224"}, {"id": "core_api_request_log_middleware_apirequestlogmiddleware", "label": "ApiRequestLogMiddleware", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py", "source_location": "L247"}, {"id": "basehttpmiddleware", "label": "BaseHTTPMiddleware", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "core_api_request_log_middleware_apirequestlogmiddleware_dispatch", "label": ".dispatch()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py", "source_location": "L248"}, {"id": "core_api_request_log_middleware_rationale_1", "label": "\u7edf\u4e00\u8bb0\u5f55 API \u8bf7\u6c42/\u54cd\u5e94\u51fa\u5165\u53c2\u7684\u4e2d\u95f4\u4ef6", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py", "source_location": "L1"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_core_api_request_log_middleware_py", "target": "asyncio", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py", "source_location": "L2", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_core_api_request_log_middleware_py", "target": "json", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_vehicle_maintenance_service_py", "label": "pl_vehicle_maintenance_service.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_maintenance_service.py", "source_location": "L1"}, {"id": "services_pl_vehicle_maintenance_service_generate_seq_no", "label": "_generate_seq_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_maintenance_service.py", "source_location": "L32"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_maintenance_service.py", "source_location": "L32"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_maintenance_service.py", "source_location": "L32"}, {"id": "services_pl_vehicle_maintenance_service_generate_apply_no", "label": "generate_apply_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_maintenance_service.py", "source_location": "L44"}, {"id": "services_pl_vehicle_maintenance_service_generate_process_no", "label": "generate_process_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_maintenance_service.py", "source_location": "L50"}, {"id": "services_pl_vehicle_maintenance_service_resolve_user_name", "label": "_resolve_user_name()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_maintenance_service.py", "source_location": "L59"}, {"id": "services_pl_vehicle_maintenance_service_assert_vehicle_exists", "label": "_assert_vehicle_exists()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_maintenance_service.py", "source_location": "L70"}, {"id": "services_pl_vehicle_maintenance_service_resolve_user_id", "label": "_resolve_user_id()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_maintenance_service.py", "source_location": "L84"}, {"id": "dbusercontext", "label": "DbUserContext", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_maintenance_service.py", "source_location": "L84"}, {"id": "services_pl_vehicle_maintenance_service_validate_apply_dates", "label": "_validate_apply_dates()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_maintenance_service.py", "source_location": "L108"}, {"id": "date", "label": "date", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_maintenance_service.py", "source_location": "L108"}, {"id": "services_pl_vehicle_maintenance_service_get_apply_row", "label": "_get_apply_row()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_maintenance_service.py", "source_location": "L119"}, {"id": "plvehiclemaintenanceapply", "label": "PlVehicleMaintenanceApply", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_maintenance_service.py", "source_location": "L119"}, {"id": "services_pl_vehicle_maintenance_service_get_process_row", "label": "_get_process_row()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_maintenance_service.py", "source_location": "L137"}, {"id": "plvehiclemaintenanceprocess", "label": "PlVehicleMaintenanceProcess", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_maintenance_service.py", "source_location": "L137"}, {"id": "services_pl_vehicle_maintenance_service_has_active_process", "label": "_has_active_process()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_maintenance_service.py", "source_location": "L155"}, {"id": "bool", "label": "bool", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_maintenance_service.py", "source_location": "L155"}, {"id": "services_pl_vehicle_maintenance_service_apply_to_read", "label": "apply_to_read()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_maintenance_service.py", "source_location": "L168"}, {"id": "vehiclemaintenanceapplyread", "label": "VehicleMaintenanceApplyRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_maintenance_service.py", "source_location": "L168"}, {"id": "services_pl_vehicle_maintenance_service_process_to_read", "label": "process_to_read()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_maLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_data_scope_py", "label": "pl_data_scope.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_data_scope.py", "source_location": "L1"}, {"id": "services_pl_data_scope_apply_pl_scope", "label": "apply_pl_scope()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_data_scope.py", "source_location": "L14"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_data_scope.py", "source_location": "L14"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_data_scope.py", "source_location": "L14"}, {"id": "services_pl_data_scope_assert_pl_row_in_scope", "label": "assert_pl_row_in_scope()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_data_scope.py", "source_location": "L19"}, {"id": "services_pl_data_scope_rationale_1", "label": "PL \u6a21\u5757\u6570\u636e\u6743\u9650\u8f85\u52a9\uff08create_by / create_dept_id\uff0c\u65e0 owner_field\uff09\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_data_scope.py", "source_location": "L1"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_data_scope_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_data_scope.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_data_scope_py", "target": "sqlalchemy_orm", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_data_scope.py", "source_location": "L6", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_data_scope_py", "target": "app_core_data_scope", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_data_scope.py", "source_location": "L8", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_data_scope_py", "target": "app_core_db_auth", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_data_scope.py", "source_location": "L11", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_data_scope_py", "target": "services_pl_data_scope_apply_pl_scope", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_data_scope.py", "source_location": "L14", "weight": 1.0}, {"source": "services_pl_data_scope_apply_pl_scope", "target": "session", "relation": "references", "context": "parameter_type", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_data_scope.py", "source_location": "L14", "weight": 1.0}, {"source": "services_pl_data_scope_apply_pl_scope", "target": "str", "relation": "references", "context": "parameter_type", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_data_scope.py", "source_location": "L14", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_data_scope_py", "target": "services_pl_data_scope_assert_pl_row_in_scope", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_data_scope.py", "source_location": "L19", "weight": 1.0}, {"source": "services_pl_data_scope_assert_pl_row_in_scope", "target": "session", "relation": "references", "context": "parameter_type", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_data_scope.py", "source_location": "L19", "weight": 1.0}, {"source": "services_pl_data_scope_assert_pl_row_in_scope", "target": "str", "relation": "references", "context": "parameter_type", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_data_scope.py", "source_location": "L19", "weight": 1.0}, {"source": "services_pl_data_scope_rationale_1", "target": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_data_scope_py", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_data_scope.py", "source_location": "L1", "weight": 1.0}], "raw_calls": [{"caller_nid": "services_pl_data_scope_apply_pl_scope", "callee": "apply_data_scope", "is_member_call": false, "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/serviLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_freight_py", "label": "freight.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py", "source_location": "L1"}, {"id": "pl_freight_generate_settlement_no", "label": "_generate_settlement_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py", "source_location": "L29"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py", "source_location": "L29"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py", "source_location": "L29"}, {"id": "pl_freight_list_unsettled_plans", "label": "list_unsettled_plans()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py", "source_location": "L48"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py", "source_location": "L48"}, {"id": "dbusercontext", "label": "DbUserContext", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py", "source_location": "L48"}, {"id": "unsettledplanlistresponse", "label": "UnsettledPlanListResponse", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py", "source_location": "L48"}, {"id": "pl_freight_list_settlements", "label": "list_settlements()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py", "source_location": "L71"}, {"id": "freightsettlementlistresponse", "label": "FreightSettlementListResponse", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py", "source_location": "L71"}, {"id": "pl_freight_create_settlement", "label": "create_settlement()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py", "source_location": "L98"}, {"id": "freightsettlementcreate", "label": "FreightSettlementCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py", "source_location": "L98"}, {"id": "freightsettlementread", "label": "FreightSettlementRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py", "source_location": "L98"}, {"id": "pl_freight_get_settlement", "label": "get_settlement()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py", "source_location": "L117"}, {"id": "freightsettlementwithtransports", "label": "FreightSettlementWithTransports", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py", "source_location": "L117"}, {"id": "pl_freight_update_settlement", "label": "update_settlement()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py", "source_location": "L130"}, {"id": "freightsettlementupdate", "label": "FreightSettlementUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py", "source_location": "L130"}, {"id": "pl_freight_delete_settlement", "label": "delete_settlement()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py", "source_location": "L143"}, {"id": "pl_freight_submit_settlement_approval", "label": "submit_settlement_approval()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py", "source_location": "L155"}, {"id": "freightsettlementsubmitapprovalbody", "label": "FreightSettlementSubmitApprovalBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py", "source_location": "L155"}, {"id": "pl_freight_process_settlement_approval", "label": "process_settlement_approval()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py", "source_location": "L173"}, {"id": "freightsettlementapprovalactionbody", "label": "FreightSettlementApprovalActionBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py", "source_location": "L173"}, {"id": "pl_freight_withdraw_settlement_approval", "label": "withdraw_settlement_approval()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py", "source_location": "L193"}, {"id": "pl_freight_get_settlement_approval_flow", "label": "get_settlement_approval_flow()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py", "source_location": "L205"}, {"id": "pl_freight_disburse_settlement", "label": "disburse_settlement()", "file_Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_crm_contact_py", "label": "contact.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py", "source_location": "L1"}, {"id": "crm_contact_total_pages", "label": "total_pages()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py", "source_location": "L20"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py", "source_location": "L20"}, {"id": "crm_contact_parse_optional_date", "label": "_parse_optional_date()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py", "source_location": "L30"}, {"id": "any", "label": "Any", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py", "source_location": "L30"}, {"id": "date", "label": "date", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py", "source_location": "L30"}, {"id": "crm_contact_row_to_dict", "label": "_row_to_dict()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py", "source_location": "L48"}, {"id": "crmcontact", "label": "CrmContact", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py", "source_location": "L48"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py", "source_location": "L48"}, {"id": "crm_contact_list_contacts", "label": "list_contacts()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py", "source_location": "L77"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py", "source_location": "L77"}, {"id": "bool", "label": "bool", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py", "source_location": "L77"}, {"id": "crm_contact_list_contacts_by_customer", "label": "list_contacts_by_customer()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py", "source_location": "L153"}, {"id": "crm_contact_get_contact", "label": "get_contact()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py", "source_location": "L182"}, {"id": "crm_contact_create_contact", "label": "create_contact()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py", "source_location": "L193"}, {"id": "crm_contact_update_contact", "label": "update_contact()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py", "source_location": "L254"}, {"id": "crm_contact_set_primary_contact", "label": "set_primary_contact()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py", "source_location": "L295"}, {"id": "crm_contact_soft_delete_contact", "label": "soft_delete_contact()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py", "source_location": "L324"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_crm_contact_py", "target": "math", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_crm_contact_py", "target": "datetime", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py", "source_location": "L5", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_crm_contact_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py", "source_location": "L6", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_crm_contact_py", "target": "sqlalchemy", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py", "source_location": "L8", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_crm_contact_py", "target": "sqlalchemy_orm", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py", "source_location": "L9", "weight": 1.0}, {"source": "users_zlwu_work_00coLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_plan_py", "label": "plan.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L1"}, {"id": "pl_plan_generate_plan_no", "label": "_generate_plan_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L66"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L66"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L66"}, {"id": "pl_plan_determine_plan_type", "label": "_determine_plan_type()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L88"}, {"id": "pl_plan_get_plan_type_label", "label": "_get_plan_type_label()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L93"}, {"id": "pl_plan_generate_notify_message", "label": "_generate_notify_message()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L102"}, {"id": "plplan", "label": "PlPlan", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L102"}, {"id": "pl_plan_list_plans", "label": "list_plans()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L123"}, {"id": "date", "label": "date", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L123"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L123"}, {"id": "dbusercontext", "label": "DbUserContext", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L123"}, {"id": "planlistresponse", "label": "PlanListResponse", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L123"}, {"id": "pl_plan_count_plans_by_contract", "label": "count_plans_by_contract()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L224"}, {"id": "pl_plan_create_plan", "label": "create_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L252"}, {"id": "plancreate", "label": "PlanCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L252"}, {"id": "planread", "label": "PlanRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L252"}, {"id": "pl_plan_create_plan_full", "label": "create_plan_full()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L331"}, {"id": "plancreatefull", "label": "PlanCreateFull", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L331"}, {"id": "planwithproducts", "label": "PlanWithProducts", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L331"}, {"id": "pl_plan_create_from_contract", "label": "create_from_contract()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L435"}, {"id": "createfromcontractrequest", "label": "CreateFromContractRequest", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L435"}, {"id": "pl_plan_get_plan", "label": "get_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L544"}, {"id": "pl_plan_update_plan", "label": "update_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L587"}, {"id": "planupdate", "label": "PlanUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L587"}, {"id": "pl_plan_patch_plan_product_certs", "label": "patch_plan_product_certs()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L696"}, {"id": "planproductcertsupdate", "label": "PlanProductCertsUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_db_inventory_py", "label": "inventory.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L1"}, {"id": "db_inventory_base_query", "label": "_base_query()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L47"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L47"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L47"}, {"id": "db_inventory_generate_batch_no", "label": "_generate_batch_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L54"}, {"id": "db_inventory_resolve_tenant_no", "label": "_resolve_tenant_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L74"}, {"id": "dbusercontext", "label": "DbUserContext", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L74"}, {"id": "db_inventory_get_inventory_by_id", "label": "_get_inventory_by_id()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L92"}, {"id": "dbinboundoutbound", "label": "DbInboundOutbound", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L92"}, {"id": "db_inventory_get_inventory", "label": "_get_inventory()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L106"}, {"id": "db_inventory_apply_inventory_product_update", "label": "_apply_inventory_product_update()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L123"}, {"id": "db_inventory_apply_inventory_update", "label": "_apply_inventory_update()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L139"}, {"id": "db_inventory_maybe_promote_third_party_status", "label": "_maybe_promote_third_party_status()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L157"}, {"id": "db_inventory_list_inventory", "label": "list_inventory()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L168"}, {"id": "inboundoutboundread", "label": "InboundOutboundRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L168"}, {"id": "db_inventory_create_inventory", "label": "create_inventory()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L273"}, {"id": "inboundoutboundcreate", "label": "InboundOutboundCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L273"}, {"id": "db_inventory_create_link_for_plan", "label": "create_link_for_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L321"}, {"id": "createlinkforplanpayload", "label": "CreateLinkForPlanPayload", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L321"}, {"id": "db_inventory_get_inventory_site_url", "label": "get_inventory_site_url()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L372"}, {"id": "inventorysiteurlread", "label": "InventorySiteUrlRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L372"}, {"id": "db_inventory_update_inventory", "label": "update_inventory()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L405"}, {"id": "inboundoutboundupdate", "label": "InboundOutboundUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L405"}, {"id": "db_inventory_delete_inventory", "label": "delete_inventory()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L423"}, {"id": "db_inventory_inventoryrelateddata", "label": "InventoryRelatedData", "file_type": "coLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_services_bx_expense_item_py", "label": "expense_item.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/expense_item.py", "source_location": "L1"}, {"id": "bx_expense_item_expenseitemservice", "label": "ExpenseItemService", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/expense_item.py", "source_location": "L14"}, {"id": "bx_expense_item_expenseitemservice_tenant_no", "label": "._tenant_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/expense_item.py", "source_location": "L16"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/expense_item.py", "source_location": "L16"}, {"id": "bxusercontext", "label": "BxUserContext", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/expense_item.py", "source_location": "L16"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/expense_item.py", "source_location": "L16"}, {"id": "bx_expense_item_expenseitemservice_get_by_id", "label": ".get_by_id()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/expense_item.py", "source_location": "L20"}, {"id": "bool", "label": "bool", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/expense_item.py", "source_location": "L20"}, {"id": "bxexpenseitem", "label": "BxExpenseItem", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/expense_item.py", "source_location": "L20"}, {"id": "bx_expense_item_expenseitemservice_create", "label": ".create()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/expense_item.py", "source_location": "L33"}, {"id": "bx_expense_item_expenseitemservice_update", "label": ".update()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/expense_item.py", "source_location": "L61"}, {"id": "bx_expense_item_expenseitemservice_soft_delete", "label": ".soft_delete()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/expense_item.py", "source_location": "L90"}, {"id": "bx_expense_item_expenseitemservice_get_item_tree", "label": ".get_item_tree()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/expense_item.py", "source_location": "L97"}, {"id": "bx_expense_item_expenseitemservice_build_tree", "label": "._build_tree()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/expense_item.py", "source_location": "L108"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_bx_expense_item_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/expense_item.py", "source_location": "L3", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_bx_expense_item_py", "target": "sqlalchemy_orm", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/expense_item.py", "source_location": "L5", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_bx_expense_item_py", "target": "app_core_bx_auth", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/expense_item.py", "source_location": "L7", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_bx_expense_item_py", "target": "app_core_tenant_context", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/expense_item.py", "source_location": "L8", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_bx_expense_item_py", "target": "app_models_bx_models", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/expense_item.py", "source_location": "L9", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_bx_expense_item_py", "target": "app_services_bx_base", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/expense_item.py", "source_location": "L10", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_bx_expense_iLine 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": "L324"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L324"}, {"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": "L352"}, {"id": "customerlistbody", "label": "CustomerListBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L352"}, {"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": "L386"}, {"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": "L412"}, {"id": "customercreatebody", "label": "CustomerCreateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L412"}, {"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": "L441"}, {"id": "customerupdatebody", "label": "CustomerUpdateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L441"}, {"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": "L467"}, {"id": "customerdeletebody", "label": "CustomerDeleteBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L467"}, {"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": "L495"}, {"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": "L511"}, {"id": "productcategorycreatebody", "label": "ProductCategoryCreateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L511"}, {"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": "L530"}, {"id": "productcategoryupdatebody", "label": "ProductCategoryUpdateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L530"}, {"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": "L549"}, {"id": "categorydeletebody", "label": "CategoryDeleteBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L549"}, {"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": "L568"}, {"id": "productlistbody", "label": "ProductListBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L568"}, {"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": "L602"}, {"id": "productdetailbody", "label": "ProductDetailBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L602"}, {"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": "L620"}, {"id": "productcreatebody", "label": "ProductCreateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py", "source_location": "L620"}, {"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": "L643"}, {"id": "productupdatebody", "label": "ProductUpdateBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.Line truncated
Loaded 100 of 406 files, more files were not shown because too many files have changed in this diff. Show more