diff --git a/alembic/versions/0019_content_review.py b/alembic/versions/0019_content_review.py new file mode 100644 index 0000000..e8e0662 --- /dev/null +++ b/alembic/versions/0019_content_review.py @@ -0,0 +1,25 @@ +"""资讯审核体系:cover_small 小封面 / priority 优先级 + +Revision ID: 0019_content_review +Revises: 0018_content_publish +Create Date: 2026-08-27 +""" +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op + +revision = "0019_content_review" +down_revision = "0018_content_publish" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column("content_items", sa.Column("cover_small", sa.String(), nullable=False, server_default="")) + op.add_column("content_items", sa.Column("priority", sa.Integer(), nullable=False, server_default="0")) + + +def downgrade() -> None: + op.drop_column("content_items", "priority") + op.drop_column("content_items", "cover_small") diff --git a/alembic/versions/0021_park_profile.py b/alembic/versions/0021_park_profile.py new file mode 100644 index 0000000..4f165f7 --- /dev/null +++ b/alembic/versions/0021_park_profile.py @@ -0,0 +1,38 @@ +"""园区企业/用户富资料:park_companies + users 加列 + +Revision ID: 0021_park_profile +Revises: 0020_task_multi_mode +Create Date: 2026-08-27 +""" +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op + +revision = "0021_park_profile" +down_revision = "0020_task_multi_mode" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # 企业丰富资料 + for col in ["legal_person", "legal_phone", "registered_capital", "company_type", + "address", "contact_phone", "founded_at"]: + op.add_column("park_companies", sa.Column(col, sa.String(), nullable=False, server_default="")) + op.add_column("park_companies", sa.Column("honors", sa.Text(), nullable=False, server_default="")) + for col in ["emp_total", "emp_grad", "emp_layoff", "emp_veteran", "emp_migrant"]: + op.add_column("park_companies", sa.Column(col, sa.Integer(), nullable=True)) + # 用户详细资料 + for col in ["id_card", "ethnicity", "grad_school_major", "grad_time"]: + op.add_column("users", sa.Column(col, sa.String(), nullable=False, server_default="")) + + +def downgrade() -> None: + for col in ["grad_time", "grad_school_major", "ethnicity", "id_card"]: + op.drop_column("users", col) + for col in ["emp_migrant", "emp_veteran", "emp_layoff", "emp_grad", "emp_total"]: + op.drop_column("park_companies", col) + for col in ["founded_at", "contact_phone", "address", "honors", "company_type", + "registered_capital", "legal_phone", "legal_person"]: + op.drop_column("park_companies", col) diff --git a/app/api/routers/rbac_admin.py b/app/api/routers/rbac_admin.py index 1a563cc..98789a9 100644 --- a/app/api/routers/rbac_admin.py +++ b/app/api/routers/rbac_admin.py @@ -91,7 +91,10 @@ async def set_user_classification( db: Database = Depends(get_db), actor: dict = Depends(require_permission("action:user.manage")), ): - fields = req.model_dump(exclude_none=True) + # 铁律:任何端口不得手动修改用户「所属园区」。affiliation/park_id/park_name 仅经 + # 入驻申请 / 转园申请(审核通过)回填,此处剔除,只允许改 认证状态/账号类型。 + fields = {k: v for k, v in req.model_dump(exclude_none=True).items() + if k in ("certification_status", "certification_time", "account_type")} updated = await db.users.set_classification(user_id, **fields) if updated is None: raise HTTPException(status_code=404, detail="User not found") diff --git a/app/infrastructure/models.py b/app/infrastructure/models.py index 5ffa72e..7735b7e 100644 --- a/app/infrastructure/models.py +++ b/app/infrastructure/models.py @@ -49,6 +49,11 @@ class User(Base): company_avatar: Mapped[str] = mapped_column(String, default="") gender: Mapped[str] = mapped_column(String, default="") # male|female|unknown birthday: Mapped[str] = mapped_column(String, default="") # YYYY-MM-DD + # 用户详细资料(参考入园申请表 §一 申请人信息) + id_card: Mapped[str] = mapped_column(String, default="") # 身份证号 + ethnicity: Mapped[str] = mapped_column(String, default="") # 民族 + grad_school_major: Mapped[str] = mapped_column(String, default="") # 毕业院校及专业 + grad_time: Mapped[str] = mapped_column(String, default="") # 毕业时间 # RBAC role: Mapped[str] = mapped_column(String, default="opc_member") # 业务角色 sub_role: Mapped[str] = mapped_column(String, nullable=True) # 政务级/运营方内部 @@ -836,12 +841,26 @@ class ParkCompany(Base): founder: Mapped[str] = mapped_column(String, default="") status: Mapped[str] = mapped_column(String, default="applying") employees: Mapped[int | None] = mapped_column(Integer, nullable=True) + # 企业丰富资料(参考入园申请表 §二 企业信息) + legal_person: Mapped[str] = mapped_column(String, default="") # 法定代表人 + legal_phone: Mapped[str] = mapped_column(String, default="") # 法人联系电话 + registered_capital: Mapped[str] = mapped_column(String, default="") # 注册资本(万元) + company_type: Mapped[str] = mapped_column(String, default="") # 企业类型/性质 + honors: Mapped[str] = mapped_column(Text, default="") # 所获政策/荣誉 + address: Mapped[str] = mapped_column(String, default="") # 企业地址 + contact_phone: Mapped[str] = mapped_column(String, default="") # 企业联系人电话 + founded_at: Mapped[str] = mapped_column(String, default="") # 成立时间 + emp_total: Mapped[int | None] = mapped_column(Integer, nullable=True) # 职工总数 + emp_grad: Mapped[int | None] = mapped_column(Integer, nullable=True) # 毕业大学生 + emp_layoff: Mapped[int | None] = mapped_column(Integer, nullable=True) # 失业人员 + emp_veteran: Mapped[int | None] = mapped_column(Integer, nullable=True) # 退役军人 + emp_migrant: Mapped[int | None] = mapped_column(Integer, nullable=True) # 返乡农民工 # 企业算力(只增不减):折扣% off 模型价;配额为发放给成员的算力额度 compute_discount: Mapped[int] = mapped_column(Integer, default=0) # 折扣%(0-100,单调非降) compute_quota: Mapped[int] = mapped_column(Integer, default=0) # 企业算力配额(单调非降/增量) compute_quota_used: Mapped[int] = mapped_column(Integer, default=0) # 已分配/已用配额(统计) engine_group: Mapped[str] = mapped_column(String, default="") # 引擎用户组名(折扣倍率载体) - owner_user_id: Mapped[str] = mapped_column(String, default="") # 企业联系人/管理员账号id(可选) + owner_user_id: Mapped[str] = mapped_column(String, default="") # 负责人/管理员账号id(必填) created_at: Mapped[str] = mapped_column(String, default="") diff --git a/app/infrastructure/seed.py b/app/infrastructure/seed.py index a0c75cd..9e6476d 100644 --- a/app/infrastructure/seed.py +++ b/app/infrastructure/seed.py @@ -137,6 +137,7 @@ PERMISSIONS = [ ("menu:admin_plans", "流程启动", "menu", "admin"), ("menu:admin_compute", "算力管理", "menu", "admin"), ("menu:admin_content", "资讯管理", "menu", "admin"), + ("menu:admin_content_review", "资讯审核", "menu", "admin"), ("menu:admin_tasks", "任务管理", "menu", "admin"), ("menu:admin_providers", "服务商管理", "menu", "admin"), ("menu:admin_config", "系统配置", "menu", "admin"), @@ -148,7 +149,9 @@ PERMISSIONS = [ # 园区(carrier)工作台 ("menu:park_screens", "屏幕管理", "menu", "carrier"), ("menu:park_screen", "大屏控制台", "menu", "carrier"), - ("menu:park_companies", "入驻企业", "menu", "carrier"), + ("menu:park_companies", "企业管理", "menu", "carrier"), + ("menu:park_users", "用户管理", "menu", "carrier"), + ("menu:park_content", "资讯管理", "menu", "carrier"), ("menu:park_kb", "园区知识库", "menu", "carrier"), ("menu:park_agent", "园区智能体", "menu", "carrier"), ("menu:park_data", "大屏数据", "menu", "carrier"), @@ -162,7 +165,8 @@ ROLE_PERMISSIONS: dict[str, list[str]] = { "opc_member": ["menu:opc_dashboard"], "carrier": [ "menu:carrier_dashboard", "action:org.manage", - "menu:park_screens", "menu:park_screen", "menu:park_companies", "menu:park_kb", + "menu:park_screens", "menu:park_screen", "menu:park_companies", "menu:park_users", "menu:park_content", + "menu:park_kb", "menu:park_agent", "menu:park_data", "menu:park_vision", "menu:park_admissions", "menu:park_transfers", ], @@ -181,7 +185,7 @@ ROLE_PERMISSIONS: dict[str, list[str]] = { "menu:admin_user_mgmt", "menu:admin_role_mgmt", "menu:audit", "menu:settlements", "menu:admin_courses", "menu:admin_activities", "menu:admin_bookings", "menu:admin_tests", "menu:admin_surveys", "menu:admin_policies", "menu:admin_plans", "menu:admin_compute", - "menu:admin_content", "menu:admin_tasks", "menu:admin_providers", "menu:admin_config", + "menu:admin_content", "menu:admin_content_review", "menu:admin_tasks", "menu:admin_providers", "menu:admin_config", "menu:admin_park", "menu:admin_park_screens", "menu:opc_cert", "menu:admission", "menu:park_transfer", "action:user.assign_role", "action:user.disable", "action:role.grant_perm", "action:org.manage", "action:data.export", "action:audit.view", diff --git a/app/park/routers.py b/app/park/routers.py index c0f170a..76b42ed 100644 --- a/app/park/routers.py +++ b/app/park/routers.py @@ -1322,7 +1322,9 @@ async def park_user_update(uid: str, body: ParkUserUpdateBody, db=Depends(_carri existing = await db.users.get_by_id(uid) if existing is None or existing.get("park_id") != t["id"]: raise HTTPException(status_code=403, detail="仅可管理本园区用户") - u = await db.users.update_profile(uid, body.model_dump(exclude_none=True)) + # 铁律:账号(username)一旦创建不可再次编辑;此接口仅更新个人资料。 + fields = {k: v for k, v in body.model_dump(exclude_none=True).items() if k != "username"} + u = await db.users.update_profile(uid, fields) return {"ok": True, "user": u} diff --git a/app/park/tenants.py b/app/park/tenants.py index 4059531..d595418 100644 --- a/app/park/tenants.py +++ b/app/park/tenants.py @@ -143,7 +143,14 @@ def _co(r) -> dict: "founder": r.founder, "status": r.status, "employees": r.employees, "created_at": r.created_at, "compute_discount": r.compute_discount, "compute_quota": r.compute_quota, "compute_quota_used": r.compute_quota_used, "engine_group": r.engine_group, - "owner_user_id": r.owner_user_id} + "owner_user_id": r.owner_user_id, + "legal_person": getattr(r, "legal_person", ""), "legal_phone": getattr(r, "legal_phone", ""), + "registered_capital": getattr(r, "registered_capital", ""), "company_type": getattr(r, "company_type", ""), + "honors": getattr(r, "honors", ""), "address": getattr(r, "address", ""), + "contact_phone": getattr(r, "contact_phone", ""), "founded_at": getattr(r, "founded_at", ""), + "emp_total": getattr(r, "emp_total", None), "emp_grad": getattr(r, "emp_grad", None), + "emp_layoff": getattr(r, "emp_layoff", None), "emp_veteran": getattr(r, "emp_veteran", None), + "emp_migrant": getattr(r, "emp_migrant", None)} def _kb(r) -> dict: @@ -303,11 +310,25 @@ async def list_companies(tenant_id: str) -> list[dict]: return out +_COMPANY_FIELDS = ("name", "zone", "room", "industry", "bio", "founder", "status", "employees", + "owner_user_id", "legal_person", "legal_phone", "registered_capital", "company_type", + "honors", "address", "contact_phone", "founded_at", + "emp_total", "emp_grad", "emp_layoff", "emp_veteran", "emp_migrant") + + async def create_company(tenant_id: str, payload: dict) -> dict: row = ParkCompany(id=_new_id("PC"), tenant_id=tenant_id, name=payload.get("name", ""), zone=payload.get("zone", ""), room=payload.get("room", ""), industry=payload.get("industry", ""), bio=payload.get("bio", ""), founder=payload.get("founder", ""), - status=payload.get("status", "applying"), employees=payload.get("employees"), created_at=_now()) + status=payload.get("status", "applying"), employees=payload.get("employees"), + legal_person=payload.get("legal_person", ""), legal_phone=payload.get("legal_phone", ""), + registered_capital=payload.get("registered_capital", ""), company_type=payload.get("company_type", ""), + honors=payload.get("honors", ""), address=payload.get("address", ""), + contact_phone=payload.get("contact_phone", ""), founded_at=payload.get("founded_at", ""), + emp_total=payload.get("emp_total"), emp_grad=payload.get("emp_grad"), + emp_layoff=payload.get("emp_layoff"), emp_veteran=payload.get("emp_veteran"), + emp_migrant=payload.get("emp_migrant"), owner_user_id=payload.get("owner_user_id", ""), + created_at=_now()) async with _get_session() as s: s.add(row) await s.commit() @@ -316,7 +337,7 @@ async def create_company(tenant_id: str, payload: dict) -> dict: async def update_company(tenant_id: str, cid: str, payload: dict) -> dict | None: - fields = {k: v for k, v in payload.items() if v is not None and k in ("name", "zone", "room", "industry", "bio", "founder", "status", "employees", "owner_user_id")} + fields = {k: v for k, v in payload.items() if v is not None and k in _COMPANY_FIELDS} if not fields: return None async with _get_session() as s: @@ -331,6 +352,20 @@ async def update_company(tenant_id: str, cid: str, payload: dict) -> dict | None return _co(row) +async def set_company_owner(company_id: str, user_id: str) -> dict | None: + """设置企业负责人:该用户须为企业成员(park_company_id 命中)。""" + async with _get_session() as s: + u = await s.get(User, user_id) + c = await s.get(ParkCompany, company_id) + if u is None or c is None: + return None + if u.park_company_id != company_id: + return {"ok": False, "reason": "该用户不隶属此企业,无法设为负责人"} + c.owner_user_id = user_id + await s.commit() + return {"ok": True, "company_id": company_id, "owner_user_id": user_id} + + async def delete_company(tenant_id: str, cid: str) -> bool: async with _get_session() as s: r = await s.execute(delete(ParkCompany).where(ParkCompany.id == cid, ParkCompany.tenant_id == tenant_id)) @@ -361,18 +396,27 @@ async def company_members(company_id: str) -> list[dict]: "id": r.id, "username": r.username, "nickname": r.nickname or "", "phone": r.phone or "", "role": r.role or "", "certification_status": r.certification_status or "uncertified", + "company": r.company or "", "gender": r.gender or "", "birthday": r.birthday or "", + "id_card": r.id_card or "", "ethnicity": r.ethnicity or "", + "grad_school_major": r.grad_school_major or "", "grad_time": r.grad_time or "", "compute_quota": r.compute_quota or 0, "compute_used_quota": r.compute_used_quota or 0, "created_at": r.created_at or "", } for r in rows] async def add_company_member(company_id: str, user_id: str) -> dict | None: - """把用户归属到该企业(成员)。""" + """把用户归属到该企业(成员)。 + + 铁律:仅绑定企业成员关系(park_company_id),绝不改动用户所属园区(park_id)。 + 且仅允许把「已属于本园区」的用户加入本园区企业——跨园区用户须经转园/入驻申请。 + """ async with _get_session() as s: u = await s.get(User, user_id) c = await s.get(ParkCompany, company_id) if u is None or c is None: return None + if u.park_id != c.tenant_id: + return {"ok": False, "reason": "该用户不属于本园区,变动园区须经转园/入驻申请"} u.park_company_id = company_id u.updated_at = _now() await s.commit() diff --git a/serverrun/mysql/docker-compose.yml b/serverrun/mysql/docker-compose.yml index 9949713..32bfb64 100644 --- a/serverrun/mysql/docker-compose.yml +++ b/serverrun/mysql/docker-compose.yml @@ -5,13 +5,13 @@ services: container_name: opc-mysql restart: always environment: - MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root_pass} + MYSQL_ROOT_PASSWORD: sjnxhyjashaywiuwhaja MYSQL_DATABASE: opc MYSQL_USER: opc - MYSQL_PASSWORD: ${MYSQL_PASSWORD:-opc_pass} + MYSQL_PASSWORD: jjjsgysyujkwjwgb command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci ports: - - "127.0.0.1:3306:3306" + - "8091:3306" volumes: - ../../serverdata/mysql-data:/var/lib/mysql networks: diff --git a/serverrun/redis/docker-compose.yml b/serverrun/redis/docker-compose.yml index 0b31cd6..c040831 100644 --- a/serverrun/redis/docker-compose.yml +++ b/serverrun/redis/docker-compose.yml @@ -6,7 +6,7 @@ services: restart: always command: ["redis-server", "--requirepass", "redis_pass"] ports: - - "127.0.0.1:6379:6379" + - "8092:6379" volumes: - ../../serverdata/redis-data:/data networks: