chore: 汇总提交(园区资料/内容审核/任务相关)+迁移0019/0021及部署编排调整

This commit is contained in:
Pine
2026-08-27 21:02:47 +08:00
parent 4ef0f81629
commit 89396eb6b0
9 changed files with 149 additions and 14 deletions
+25
View File
@@ -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")
+38
View File
@@ -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)
+4 -1
View File
@@ -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")
+20 -1
View File
@@ -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="")
+7 -3
View File
@@ -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",
+3 -1
View File
@@ -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}
+48 -4
View File
@@ -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()
+3 -3
View File
@@ -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:
+1 -1
View File
@@ -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: