From 412ec0771f5b37d62053eb941d253a766e3774ce Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Fri, 18 Sep 2026 11:24:03 +0800 Subject: [PATCH] feat(backend): add collection region staff CRUD and list summaries Co-authored-by: Cursor --- backend/app/crud/collection_region.py | 290 +++++++++++++++++- .../test_collection_region_staff_helpers.py | 26 ++ 2 files changed, 313 insertions(+), 3 deletions(-) create mode 100644 backend/tests/test_collection_region_staff_helpers.py diff --git a/backend/app/crud/collection_region.py b/backend/app/crud/collection_region.py index bc5b2f10..f4875829 100644 --- a/backend/app/crud/collection_region.py +++ b/backend/app/crud/collection_region.py @@ -9,14 +9,43 @@ from sqlalchemy import and_, func, or_ from sqlalchemy.orm import Session from app.core.utils import generate_uuid -from app.models.models import BizCollectionHospital, BizCollectionRegion +from app.models.models import ( + BizCollectionHospital, + BizCollectionRegion, + BizCollectionRegionStaff, + SysDept, + SysUser, +) -def _region_to_dict(r: BizCollectionRegion, hospital_names: Optional[List[str]] = None) -> Dict[str, Any]: +def staff_display_name(nick_name: Optional[str], user_no: Optional[str]) -> str: + n = (nick_name or "").strip() + if n: + return n + return (user_no or "").strip() + + +def build_staff_leader_name_lists(rows: List[Dict[str, Any]]) -> Tuple[List[str], List[str]]: + """rows: dicts with nickName/isLeader (already sorted leaders-first preferred).""" + leaders = [staff_display_name(r.get("nickName"), r.get("userNo")) for r in rows if r.get("isLeader")] + # Keep leader-first order if caller sorted; otherwise partition + non_leaders = [staff_display_name(r.get("nickName"), r.get("userNo")) for r in rows if not r.get("isLeader")] + staff_names = leaders + non_leaders + return staff_names, leaders + + +def _region_to_dict( + r: BizCollectionRegion, + hospital_names: Optional[List[str]] = None, + staff_names: Optional[List[str]] = None, + leader_names: Optional[List[str]] = None, +) -> Dict[str, Any]: return { "id": r.id, "regionName": r.region_name, "hospitalNames": hospital_names or [], + "staffNames": staff_names or [], + "leaderNames": leader_names or [], "tenantNo": r.tenant_no, "orderNum": r.order_num if r.order_num is not None else 0, "remark": r.remark or "", @@ -46,6 +75,43 @@ def _hospital_names_by_region_ids(db: Session, region_ids: List[str]) -> Dict[st return hospital_map +def _staff_names_by_region_ids( + db: Session, region_ids: List[str] +) -> Dict[str, Tuple[List[str], List[str]]]: + """Return region_id -> (staffNames, leaderNames). Leaders first in staffNames.""" + out: Dict[str, Tuple[List[str], List[str]]] = {rid: ([], []) for rid in region_ids} + if not region_ids: + return out + rows = ( + db.query(BizCollectionRegionStaff, SysUser) + .outerjoin(SysUser, SysUser.id == BizCollectionRegionStaff.user_id) + .filter( + and_( + BizCollectionRegionStaff.region_id.in_(region_ids), + BizCollectionRegionStaff.del_flag == 0, + ) + ) + .order_by( + BizCollectionRegionStaff.is_leader.desc(), + BizCollectionRegionStaff.order_num.asc(), + BizCollectionRegionStaff.create_time.desc(), + ) + .all() + ) + buckets: Dict[str, List[Dict[str, Any]]] = {rid: [] for rid in region_ids} + for staff, user in rows: + buckets.setdefault(staff.region_id, []).append( + { + "nickName": (user.nick_name if user else "") or "", + "userNo": (user.user_no if user else "") or "", + "isLeader": bool(staff.is_leader), + } + ) + for rid, items in buckets.items(): + out[rid] = build_staff_leader_name_lists(items) + return out + + def _hospital_to_dict(h: BizCollectionHospital) -> Dict[str, Any]: return { "id": h.id, @@ -59,6 +125,30 @@ def _hospital_to_dict(h: BizCollectionHospital) -> Dict[str, Any]: } +def _staff_to_dict( + s: BizCollectionRegionStaff, + *, + nick_name: str = "", + user_no: str = "", + dept_name: str = "", +) -> Dict[str, Any]: + return { + "id": s.id, + "regionId": s.region_id, + "userId": s.user_id, + "userName": staff_display_name(nick_name, user_no), + "nickName": nick_name or "", + "userNo": user_no or "", + "deptName": dept_name or "", + "isLeader": bool(s.is_leader), + "tenantNo": s.tenant_no, + "orderNum": s.order_num if s.order_num is not None else 0, + "remark": s.remark 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 "", + } + + def list_collection_regions( db: Session, *, @@ -90,7 +180,16 @@ def list_collection_regions( region_ids = [r.id for r in rows] hospital_map = _hospital_names_by_region_ids(db, region_ids) - return [_region_to_dict(x, hospital_names=hospital_map.get(x.id, [])) for x in rows], int(total) + staff_map = _staff_names_by_region_ids(db, region_ids) + return [ + _region_to_dict( + x, + hospital_names=hospital_map.get(x.id, []), + staff_names=staff_map.get(x.id, ([], []))[0], + leader_names=staff_map.get(x.id, ([], []))[1], + ) + for x in rows + ], int(total) def get_collection_region(db: Session, region_id: str, tenant_no: Optional[str] = None) -> Optional[BizCollectionRegion]: @@ -183,6 +282,20 @@ def soft_delete_collection_region(db: Session, region_id: str, tenant_no: Option h.del_flag = 1 h.update_time = now db.add(h) + staff_subs = ( + db.query(BizCollectionRegionStaff) + .filter( + and_( + BizCollectionRegionStaff.region_id == region_id, + BizCollectionRegionStaff.del_flag == 0, + ) + ) + .all() + ) + for s in staff_subs: + s.del_flag = 1 + s.update_time = now + db.add(s) db.commit() @@ -343,6 +456,177 @@ def soft_delete_hospital(db: Session, hospital_id: str, tenant_no: Optional[str] db.commit() +def list_staff( + db: Session, + *, + region_id: str, + page: int = 1, + page_size: int = 10, + tenant_no: Optional[str] = None, +) -> Tuple[List[Dict[str, Any]], int]: + region = get_collection_region(db, region_id, tenant_no=tenant_no) + if not region: + raise ValueError("采集地区不存在") + + def _filtered(): + q0 = db.query(BizCollectionRegionStaff).filter( + and_(BizCollectionRegionStaff.region_id == region_id, BizCollectionRegionStaff.del_flag == 0) + ) + if tenant_no: + q0 = q0.filter(BizCollectionRegionStaff.tenant_no == tenant_no) + return q0 + + total = _filtered().order_by(None).with_entities(func.count(BizCollectionRegionStaff.id)).scalar() or 0 + page = max(1, page) + page_size = min(max(1, page_size), 100) + rows = ( + _filtered() + .order_by( + BizCollectionRegionStaff.is_leader.desc(), + BizCollectionRegionStaff.order_num.asc(), + BizCollectionRegionStaff.create_time.desc(), + ) + .offset((page - 1) * page_size) + .limit(page_size) + .all() + ) + user_ids = [r.user_id for r in rows] + user_map: Dict[str, SysUser] = {} + dept_map: Dict[str, str] = {} + if user_ids: + for u in db.query(SysUser).filter(SysUser.id.in_(user_ids)).all(): + user_map[u.id] = u + dept_ids = [u.dept_id for u in user_map.values() if u.dept_id] + if dept_ids: + for d in db.query(SysDept).filter(SysDept.id.in_(dept_ids), SysDept.del_flag == 0).all(): + dept_map[d.id] = d.dept_name or "" + items: List[Dict[str, Any]] = [] + for s in rows: + u = user_map.get(s.user_id) + items.append( + _staff_to_dict( + s, + nick_name=(u.nick_name if u else "") or "", + user_no=(u.user_no if u else "") or "", + dept_name=dept_map.get((u.dept_id if u else "") or "", ""), + ) + ) + return items, int(total) + + +def get_staff(db: Session, staff_id: str, tenant_no: Optional[str] = None) -> Optional[BizCollectionRegionStaff]: + q = db.query(BizCollectionRegionStaff).filter( + and_(BizCollectionRegionStaff.id == staff_id, BizCollectionRegionStaff.del_flag == 0) + ) + if tenant_no: + q = q.filter(BizCollectionRegionStaff.tenant_no == tenant_no) + return q.first() + + +def create_staff( + db: Session, + *, + region_id: str, + user_id: str, + is_leader: bool = False, + tenant_no: str = "zkxw", + order_num: Optional[int] = None, + remark: Optional[str] = None, + user_id_actor: Optional[str] = None, +) -> BizCollectionRegionStaff: + region = get_collection_region(db, region_id, tenant_no=tenant_no) + if not region: + raise ValueError("采集地区不存在") + uid = (user_id or "").strip() + if not uid: + raise ValueError("请选择系统人员") + user = ( + db.query(SysUser) + .filter( + and_( + SysUser.id == uid, + SysUser.del_flag == 0, + SysUser.tenant_no == tenant_no, + ) + ) + .first() + ) + if not user: + raise ValueError("用户不存在") + if (user.status or "") != "1": + raise ValueError("用户已停用,无法绑定") + exists = ( + db.query(BizCollectionRegionStaff) + .filter( + and_( + BizCollectionRegionStaff.region_id == region_id, + BizCollectionRegionStaff.user_id == uid, + BizCollectionRegionStaff.del_flag == 0, + BizCollectionRegionStaff.tenant_no == tenant_no, + ) + ) + .first() + ) + if exists: + raise ValueError("该人员已绑定到此地区") + obj = BizCollectionRegionStaff( + id=generate_uuid(), + region_id=region_id, + user_id=uid, + is_leader=1 if is_leader else 0, + tenant_no=tenant_no, + order_num=order_num if order_num is not None else 0, + remark=(remark or "").strip() or None, + create_by=user_id_actor, + create_dept_id="", + create_time=datetime.now(), + update_by=user_id_actor, + update_time=datetime.now(), + del_flag=0, + ) + db.add(obj) + db.commit() + db.refresh(obj) + return obj + + +def update_staff( + db: Session, + staff_id: str, + *, + is_leader: Optional[bool] = None, + order_num: Optional[int] = None, + remark: Optional[str] = None, + user_id: Optional[str] = None, + tenant_no: Optional[str] = None, +) -> BizCollectionRegionStaff: + obj = get_staff(db, staff_id, tenant_no=tenant_no) + if not obj: + raise ValueError("绑定记录不存在") + if is_leader is not None: + obj.is_leader = 1 if is_leader else 0 + if order_num is not None: + obj.order_num = order_num + if remark is not None: + obj.remark = remark.strip() or None + obj.update_by = user_id + obj.update_time = datetime.now() + db.add(obj) + db.commit() + db.refresh(obj) + return obj + + +def soft_delete_staff(db: Session, staff_id: str, tenant_no: Optional[str] = None) -> None: + obj = get_staff(db, staff_id, tenant_no=tenant_no) + if not obj: + raise ValueError("绑定记录不存在") + obj.del_flag = 1 + obj.update_time = datetime.now() + db.add(obj) + db.commit() + + def total_pages(total: int, page_size: int) -> int: if total <= 0: return 0 diff --git a/backend/tests/test_collection_region_staff_helpers.py b/backend/tests/test_collection_region_staff_helpers.py new file mode 100644 index 00000000..0bfce70f --- /dev/null +++ b/backend/tests/test_collection_region_staff_helpers.py @@ -0,0 +1,26 @@ +import unittest + +from app.crud.collection_region import staff_display_name, build_staff_leader_name_lists + + +class CollectionRegionStaffHelperTests(unittest.TestCase): + def test_staff_display_name_prefers_nick_name(self): + self.assertEqual(staff_display_name("张敏", "SQ001"), "张敏") + + def test_staff_display_name_falls_back_to_user_no(self): + self.assertEqual(staff_display_name("", "SQ001"), "SQ001") + self.assertEqual(staff_display_name(None, "SQ001"), "SQ001") + + def test_build_staff_leader_name_lists_leaders_first_in_staff(self): + rows = [ + {"userId": "u2", "nickName": "李强", "isLeader": False}, + {"userId": "u1", "nickName": "张敏", "isLeader": True}, + {"userId": "u3", "nickName": "王芳", "isLeader": True}, + ] + staff_names, leader_names = build_staff_leader_name_lists(rows) + self.assertEqual(leader_names, ["张敏", "王芳"]) + self.assertEqual(staff_names, ["张敏", "王芳", "李强"]) + + +if __name__ == "__main__": + unittest.main()