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:
hankin
2026-05-11 07:24:19 +00:00
parent 0f4c6b7924
commit 815cbf9d8c
2526 changed files with 11875 additions and 804148 deletions
+116 -19
View File
@@ -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_