v0.2.0: CRM/ERP 系统升级 - 清理 .gitignore 并移除误提交的 venv/env/db 文件
- 更新 .gitignore:全面覆盖环境变量、数据库、日志、缓存、上传文件 - 移除误跟踪的 server/venv/、crm_data.db、.env 文件 - 新增 server/.env.example 模板 - 新增合同管理、利润核算、AI教练等功能模块 - 新增 Playwright e2e 测试套件 - 前后端多项功能升级和 bug 修复
This commit is contained in:
@@ -13,6 +13,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.exceptions import BizException, ForbiddenException, NotFoundException
|
||||
from app.models.crm import CrmCustomer
|
||||
from app.models.sys import SysUser
|
||||
from app.schemas.auth import CurrentUserPayload
|
||||
from app.schemas.crm import (
|
||||
CustomerCreate,
|
||||
@@ -35,6 +36,7 @@ def _to_response(c: CrmCustomer) -> CustomerResponse:
|
||||
address=c.address,
|
||||
ai_score=float(c.ai_score or 0),
|
||||
ai_persona=c.ai_persona,
|
||||
billing_info=c.billing_info,
|
||||
owner_id=c.owner_id,
|
||||
owner_name=c.owner.real_name if c.owner else None,
|
||||
status=c.status,
|
||||
@@ -44,12 +46,48 @@ def _to_response(c: CrmCustomer) -> CustomerResponse:
|
||||
)
|
||||
|
||||
|
||||
# ── 递归查询本部门 + 子部门所有用户 ID ────────────────────
|
||||
async def _get_dept_and_sub_user_ids(
|
||||
db: AsyncSession, dept_id: uuid.UUID
|
||||
) -> list[uuid.UUID]:
|
||||
"""递归获取指定部门及其所有子部门下的用户 ID 列表"""
|
||||
from app.models.sys import SysDepartment, SysUser
|
||||
|
||||
# 收集所有目标部门 ID(递归子部门)
|
||||
dept_ids: list[uuid.UUID] = [dept_id]
|
||||
queue = [dept_id]
|
||||
while queue:
|
||||
current = queue.pop(0)
|
||||
children = (await db.execute(
|
||||
select(SysDepartment.id).where(
|
||||
SysDepartment.parent_id == current,
|
||||
SysDepartment.is_deleted.is_(False),
|
||||
)
|
||||
)).scalars().all()
|
||||
for child_id in children:
|
||||
dept_ids.append(child_id)
|
||||
queue.append(child_id)
|
||||
|
||||
# 查询这些部门下的所有用户 ID
|
||||
user_ids = (await db.execute(
|
||||
select(SysUser.id).where(
|
||||
SysUser.dept_id.in_(dept_ids),
|
||||
SysUser.is_deleted.is_(False),
|
||||
)
|
||||
)).scalars().all()
|
||||
return list(user_ids)
|
||||
|
||||
|
||||
# ── 权限校验 ─────────────────────────────────────────────
|
||||
def _check_access(customer: CrmCustomer, user: CurrentUserPayload) -> None:
|
||||
def _check_access(customer: CrmCustomer, user: CurrentUserPayload, *, dept_user_ids: list[uuid.UUID] | None = None) -> None:
|
||||
if user.data_scope == "all":
|
||||
return
|
||||
if user.data_scope == "dept_and_sub":
|
||||
return # 简化版:放通本部门
|
||||
# 如果有预查询的部门用户列表,校验 owner 是否在列表内
|
||||
if dept_user_ids is not None:
|
||||
if customer.owner_id not in dept_user_ids:
|
||||
raise ForbiddenException("无权访问该客户(数据权限:本部门及子部门)")
|
||||
return
|
||||
# data_scope == 'self'
|
||||
if customer.owner_id != user.user_id:
|
||||
raise ForbiddenException("无权访问该客户(数据权限:仅本人)")
|
||||
@@ -70,6 +108,7 @@ async def create_customer(
|
||||
phone=body.phone,
|
||||
email=body.email,
|
||||
address=body.address,
|
||||
billing_info=body.billing_info.model_dump() if body.billing_info else None,
|
||||
status=body.status,
|
||||
owner_id=user.user_id,
|
||||
)
|
||||
@@ -98,12 +137,12 @@ async def list_customers(
|
||||
base_where.append(CrmCustomer.owner_id == user.user_id)
|
||||
elif user.data_scope == "dept_and_sub":
|
||||
if user.dept_id is not None:
|
||||
from app.models.sys import SysUser
|
||||
sub = select(SysUser.id).where(
|
||||
SysUser.dept_id == user.dept_id,
|
||||
SysUser.is_deleted.is_(False),
|
||||
)
|
||||
base_where.append(CrmCustomer.owner_id.in_(sub))
|
||||
dept_user_ids = await _get_dept_and_sub_user_ids(db, user.dept_id)
|
||||
if dept_user_ids:
|
||||
base_where.append(CrmCustomer.owner_id.in_(dept_user_ids))
|
||||
else:
|
||||
# 部门无用户 → 仅显示自己的
|
||||
base_where.append(CrmCustomer.owner_id == user.user_id)
|
||||
|
||||
if keyword:
|
||||
base_where.append(CrmCustomer.name.ilike(f"%{keyword}%"))
|
||||
@@ -144,7 +183,11 @@ async def get_customer(
|
||||
if customer is None:
|
||||
raise NotFoundException("客户不存在或已被删除")
|
||||
|
||||
_check_access(customer, user)
|
||||
# dept_and_sub 需要先查询部门用户列表
|
||||
dept_user_ids = None
|
||||
if user.data_scope == "dept_and_sub" and user.dept_id:
|
||||
dept_user_ids = await _get_dept_and_sub_user_ids(db, user.dept_id)
|
||||
_check_access(customer, user, dept_user_ids=dept_user_ids)
|
||||
return _to_response(customer)
|
||||
|
||||
|
||||
@@ -162,7 +205,10 @@ async def update_customer(
|
||||
if customer is None:
|
||||
raise NotFoundException("客户不存在或已被删除")
|
||||
|
||||
_check_access(customer, user)
|
||||
dept_user_ids = None
|
||||
if user.data_scope == "dept_and_sub" and user.dept_id:
|
||||
dept_user_ids = await _get_dept_and_sub_user_ids(db, user.dept_id)
|
||||
_check_access(customer, user, dept_user_ids=dept_user_ids)
|
||||
|
||||
update_data = body.model_dump(exclude_unset=True)
|
||||
if not update_data:
|
||||
@@ -193,7 +239,10 @@ async def delete_customer(
|
||||
if customer is None:
|
||||
raise NotFoundException("客户不存在或已被删除")
|
||||
|
||||
_check_access(customer, user)
|
||||
dept_user_ids = None
|
||||
if user.data_scope == "dept_and_sub" and user.dept_id:
|
||||
dept_user_ids = await _get_dept_and_sub_user_ids(db, user.dept_id)
|
||||
_check_access(customer, user, dept_user_ids=dept_user_ids)
|
||||
|
||||
await db.execute(
|
||||
update(CrmCustomer)
|
||||
@@ -216,7 +265,10 @@ async def restore_customer(
|
||||
if customer is None:
|
||||
raise NotFoundException("客户不存在或未被归档")
|
||||
|
||||
_check_access(customer, user)
|
||||
dept_user_ids = None
|
||||
if user.data_scope == "dept_and_sub" and user.dept_id:
|
||||
dept_user_ids = await _get_dept_and_sub_user_ids(db, user.dept_id)
|
||||
_check_access(customer, user, dept_user_ids=dept_user_ids)
|
||||
|
||||
await db.execute(
|
||||
update(CrmCustomer)
|
||||
@@ -226,6 +278,49 @@ async def restore_customer(
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def transfer_customer(
|
||||
db: AsyncSession,
|
||||
user: CurrentUserPayload,
|
||||
customer_id: uuid.UUID,
|
||||
new_owner_id: uuid.UUID,
|
||||
) -> CustomerResponse:
|
||||
"""将客户转移至指定人员名下(仅管理员)"""
|
||||
if user.data_scope != "all":
|
||||
raise ForbiddenException("仅管理员可执行客户转移操作")
|
||||
|
||||
stmt = select(CrmCustomer).where(
|
||||
CrmCustomer.id == customer_id,
|
||||
CrmCustomer.is_deleted.is_(False),
|
||||
)
|
||||
customer = (await db.execute(stmt)).scalar_one_or_none()
|
||||
if customer is None:
|
||||
raise NotFoundException("客户不存在或已被归档")
|
||||
|
||||
if customer.owner_id == new_owner_id:
|
||||
raise BizException(message="目标负责人与当前负责人相同,无需转移")
|
||||
|
||||
# 校验目标用户是否存在
|
||||
from app.models.sys import SysUser
|
||||
target = (await db.execute(
|
||||
select(SysUser).where(SysUser.id == new_owner_id)
|
||||
)).scalar_one_or_none()
|
||||
if target is None:
|
||||
raise NotFoundException("目标负责人不存在")
|
||||
|
||||
old_owner_name = customer.owner.real_name if customer.owner else "(无)"
|
||||
|
||||
await db.execute(
|
||||
update(CrmCustomer)
|
||||
.where(CrmCustomer.id == customer_id)
|
||||
.values(owner_id=new_owner_id, updated_at=datetime.utcnow())
|
||||
)
|
||||
await db.commit()
|
||||
await db.refresh(customer)
|
||||
|
||||
print(f"[客户转移] {customer.name}: {old_owner_name} → {target.real_name} (操作人: {user.real_name})")
|
||||
return _to_response(customer)
|
||||
|
||||
|
||||
async def get_customer_products(
|
||||
db: AsyncSession,
|
||||
user: CurrentUserPayload,
|
||||
@@ -241,7 +336,10 @@ async def get_customer_products(
|
||||
customer = (await db.execute(stmt)).scalar_one_or_none()
|
||||
if customer is None:
|
||||
raise NotFoundException("客户不存在")
|
||||
_check_access(customer, user)
|
||||
dept_user_ids = None
|
||||
if user.data_scope == "dept_and_sub" and user.dept_id:
|
||||
dept_user_ids = await _get_dept_and_sub_user_ids(db, user.dept_id)
|
||||
_check_access(customer, user, dept_user_ids=dept_user_ids)
|
||||
|
||||
# 聚合: 该客户所有订单中的 SKU,含总数量、最近下单时间
|
||||
agg_stmt = (
|
||||
@@ -299,12 +397,11 @@ async def search_customers(
|
||||
base_where.append(CrmCustomer.owner_id == user.user_id)
|
||||
elif user.data_scope == "dept_and_sub":
|
||||
if user.dept_id is not None:
|
||||
from app.models.sys import SysUser
|
||||
sub = select(SysUser.id).where(
|
||||
SysUser.dept_id == user.dept_id,
|
||||
SysUser.is_deleted.is_(False),
|
||||
)
|
||||
base_where.append(CrmCustomer.owner_id.in_(sub))
|
||||
dept_user_ids = await _get_dept_and_sub_user_ids(db, user.dept_id)
|
||||
if dept_user_ids:
|
||||
base_where.append(CrmCustomer.owner_id.in_(dept_user_ids))
|
||||
else:
|
||||
base_where.append(CrmCustomer.owner_id == user.user_id)
|
||||
|
||||
# 模糊搜索(名称 / 联系人 / 电话)
|
||||
from sqlalchemy import or_
|
||||
|
||||
Reference in New Issue
Block a user