Files
server-core/app/repositories.py
T
Pine 26fa546f67 feat: 核心服务端基础框架(身份/平台 API)
- 七端口 RBAC、select-identity、JWT、审计
- FastAPI + SQLAlchemy + SQLite,/auth /opc /admin /agents 等路由
2026-08-23 22:35:59 +08:00

1805 lines
66 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""数据访问层(Repository):SQLAlchemy/SQLite 实现。
每个 Repository 对应一张表,只做行级操作,不掺入 HTTP/路由逻辑。
方法签名与原先 JSON 实现保持一致,路由与业务逻辑不变。
"""
from __future__ import annotations
import json
import secrets
from datetime import datetime, timezone
from sqlalchemy import delete, select
from sqlalchemy.orm import Session
from . import config
from .models import (
Agent,
AuditLog,
Bid,
ContentItem,
Contract,
Dispute,
Escrow,
FinanceRecord,
InvestmentIntent,
InvestorPreference,
Message,
Notification,
OpcProfile,
OpcTask,
Organization,
OrganizationMember,
Permission,
PortalDashboard,
PortalPage,
Rating,
Region,
Roadshow,
RoadshowRegistration,
Role,
RolePermission,
ServiceProvider,
ServiceReferral,
SessionToken,
SubsidyApplication,
SystemConfig,
Task,
TrainingEnrollment,
User,
UserIdentity,
)
from .security import hash_password, verify_password
USER_TABLE = "users"
TOKEN_TABLE = "tokens"
AGENTS_TABLE = "agents"
# 内置智能体身份(唯一来源在服务端;本地据此同步 workspace)
DEFAULT_AGENT_ID = "pine_agents_official_001"
QA_AGENT_ID = "pine_agents_official_002"
AGENT_SEED = (
{
"id": DEFAULT_AGENT_ID,
"name": "小园",
"description": "默认助手,处理和园区、创业、政策等相关工作",
"language": "zh",
"model_name": "",
"template_type": "default",
"deletable": False,
"use_fixed_soul": True,
},
{
"id": QA_AGENT_ID,
"name": "问答助手",
"description": (
"内置 PineAgents 设置问答助手,本地配置在 `PINEAGENTS_WORKING_DIR` 下,"
"并提供文档。建议在回答前阅读文件;在此工作区外使用绝对路径编写代码。"
),
"language": "zh",
"model_name": "",
"template_type": "qa",
"deletable": True,
"use_fixed_soul": False,
},
)
PROFILE_FIELDS = (
"username",
"nickname",
"account",
"company",
"room",
"avatar",
"company_avatar",
)
# ---------------------------------------------------------------------------
# 记录工具
# ---------------------------------------------------------------------------
def utcnow_iso() -> str:
"""当前 UTC 时间,ISO 8601 字符串(如 ``2026-08-02T12:00:00+00:00``)。"""
return datetime.now(timezone.utc).isoformat()
def new_id(prefix: str) -> str:
"""生成记录主键,如 ``u_<hex>`` / ``sess_<hex>``。"""
return f"{prefix}_{secrets.token_hex(12)}"
# 业务角色 → 端口映射(与 rbac_admin._port_for_role 保持一致)
_PORT_FOR_ROLE = {
"opc_member": "opc", "carrier": "carrier", "enterprise": "enterprise",
"provider": "provider", "government": "government", "operator": "operator",
"investor": "investor", "developer": "developer",
}
def is_expired(expires_at: str) -> bool:
"""判断 ``expires_at``ISO 字符串)是否已过期。"""
try:
return datetime.fromisoformat(expires_at) < datetime.now(timezone.utc)
except (ValueError, TypeError):
return True
def _iso_in(seconds: int) -> str:
"""返回当前时刻往后 ``seconds`` 秒的 ISO 时间戳。"""
return datetime.fromtimestamp(
datetime.now(timezone.utc).timestamp() + seconds,
tz=timezone.utc,
).isoformat()
def _user_to_dict(u: User) -> dict:
return {
"id": u.id,
"username": u.username,
"password_hash": u.password_hash,
"password_salt": u.password_salt,
"nickname": u.nickname,
"account": u.account,
"company": u.company,
"room": u.room,
"avatar": u.avatar,
"company_avatar": u.company_avatar,
"role": u.role,
"sub_role": u.sub_role,
"org_id": u.org_id,
"region_id": u.region_id,
"status": u.status,
"token_version": u.token_version,
"created_at": u.created_at,
"updated_at": u.updated_at,
}
# ---------------------------------------------------------------------------
# 用户表
# ---------------------------------------------------------------------------
class UserRepository:
def __init__(self, session: Session):
self.session = session
def has_users(self) -> bool:
return self.session.scalar(select(User.id).limit(1)) is not None
def get_by_username(self, username: str) -> dict | None:
u = self.session.scalar(
select(User).where(User.username.ilike(username.strip())),
)
return _user_to_dict(u) if u else None
def get_by_id(self, user_id: str) -> dict | None:
u = self.session.get(User, user_id)
return _user_to_dict(u) if u else None
def list(self) -> list[dict]:
return [_user_to_dict(u) for u in self.session.scalars(select(User).order_by(User.created_at))]
def create(
self,
username: str,
password: str,
*,
nickname: str = "",
account: str = "",
company: str = "",
room: str = "",
avatar: str = "",
company_avatar: str = "",
role: str = "opc_member",
sub_role: str | None = None,
org_id: str | None = None,
region_id: str | None = None,
) -> dict:
digest, salt = hash_password(password)
now = utcnow_iso()
u = User(
id=new_id("u"),
username=username.strip(),
password_hash=digest,
password_salt=salt,
nickname=nickname,
account=account,
company=company,
room=room,
avatar=avatar,
company_avatar=company_avatar,
role=role,
sub_role=sub_role,
org_id=org_id,
region_id=region_id,
status="active",
token_version=0,
created_at=now,
updated_at=now,
)
self.session.add(u)
self.session.commit()
return _user_to_dict(u)
def update_profile(self, user_id: str, fields: dict) -> dict | None:
u = self.session.get(User, user_id)
if u is None:
return None
allowed = {k: fields[k] for k in fields if k in PROFILE_FIELDS}
if not allowed:
return None
for k, v in allowed.items():
setattr(u, k, v)
u.updated_at = utcnow_iso()
self.session.commit()
return _user_to_dict(u)
def update_credentials(
self,
user_id: str,
new_username: str | None,
new_password: str | None,
) -> dict | None:
u = self.session.get(User, user_id)
if u is None:
return None
if new_username is not None:
u.username = new_username.strip()
if new_password is not None:
digest, salt = hash_password(new_password)
u.password_hash = digest
u.password_salt = salt
u.updated_at = utcnow_iso()
self.session.commit()
return _user_to_dict(u)
def verify_password(self, user: dict, password: str) -> bool:
return verify_password(
password,
user.get("password_hash", ""),
user.get("password_salt", ""),
)
def set_role(
self,
user_id: str,
role: str,
sub_role: str | None,
org_id: str | None,
region_id: str | None,
) -> dict | None:
u = self.session.get(User, user_id)
if u is None:
return None
# 同步该用户"主角色"身份(角色改变需对已绑定身份会话生效)
new_port = _PORT_FOR_ROLE.get(role, "opc")
for ident in self.session.scalars(
select(UserIdentity).where(UserIdentity.user_id == user_id)
):
if ident.role == u.role:
ident.role = role
ident.sub_role = sub_role
ident.port = new_port
ident.org_id = org_id
ident.region_id = region_id
ident.updated_at = utcnow_iso()
u.role = role
u.sub_role = sub_role
u.org_id = org_id
u.region_id = region_id
u.token_version += 1 # 使既有令牌失效,角色变更即时生效
u.updated_at = utcnow_iso()
self.session.commit()
return _user_to_dict(u)
def set_status(self, user_id: str, status: str) -> dict | None:
u = self.session.get(User, user_id)
if u is None:
return None
u.status = status
u.updated_at = utcnow_iso()
self.session.commit()
return _user_to_dict(u)
def bump_token_version(self, user_id: str) -> int:
u = self.session.get(User, user_id)
if u is None:
return 0
u.token_version += 1
u.updated_at = utcnow_iso()
self.session.commit()
return u.token_version
def to_profile(self, user: dict) -> dict:
"""把用户记录裁剪成对外暴露的资料结构(不含任何密码字段)。"""
profile = {field: user.get(field, "") for field in PROFILE_FIELDS}
profile["role"] = user.get("role", "opc_member")
profile["sub_role"] = user.get("sub_role")
profile["org_id"] = user.get("org_id")
profile["region_id"] = user.get("region_id")
profile["permissions"] = user.get("permissions", [])
profile["scope_region_ids"] = user.get("scope_region_ids", [])
profile["identity_id"] = user.get("identity_id")
profile["port"] = user.get("port")
profile["identity_name"] = user.get("identity_name", "")
return profile
# ---------------------------------------------------------------------------
# 令牌表(JWT + sessions 撤销台账)
# ---------------------------------------------------------------------------
class TokenRepository:
def __init__(self, session: Session):
self.session = session
def create(
self,
user: dict,
*,
permissions: list[str],
scope_region_ids: list[str],
scope_level: str | None,
expiry_seconds: int,
identity_id: str | None = None,
) -> dict:
"""签发 JWT 并登记 sessions 行,返回 {token, id, user_id, ...}。"""
from .jwt import create_access_token
token, jti, expires_at = create_access_token(
user,
permissions=permissions,
scope_region_ids=scope_region_ids,
scope_level=scope_level,
expiry_seconds=expiry_seconds,
identity_id=identity_id,
)
now = utcnow_iso()
rec = SessionToken(
id=jti,
jti=jti,
token=token,
user_id=user["id"],
username=user.get("username", ""),
created_at=now,
expires_at=expires_at,
revoked=False,
)
self.session.add(rec)
self.session.commit()
return {"id": jti, "token": token, "user_id": user["id"]}
def get(self, token: str) -> dict | None:
"""按 JWT 解码得到的 jti 查会话行。"""
from .jwt import decode_access_token
payload = decode_access_token(token)
if not payload:
return None
rec = self.session.scalar(
select(SessionToken).where(SessionToken.jti == payload.get("jti")),
)
if rec is None:
return None
return {
"id": rec.id,
"jti": rec.jti,
"token": rec.token,
"user_id": rec.user_id,
"username": rec.username,
"expires_at": rec.expires_at,
"revoked": rec.revoked,
}
def session_valid(self, jti: str, user_id: str, ver: int) -> bool:
"""JWT 声称的会话与用户版本是否仍有效。"""
rec = self.session.scalar(
select(SessionToken).where(SessionToken.jti == jti),
)
if rec is None or rec.revoked or rec.user_id != user_id:
return False
if is_expired(rec.expires_at):
return False
u = self.session.get(User, user_id)
return u is not None and u.token_version == ver and u.status == "active"
def get_valid_user_id(self, token: str) -> str | None:
"""JWT 有效(验签、会话未吊销、版本一致、用户启用)时返回 user_id。"""
from .jwt import decode_access_token
payload = decode_access_token(token)
if not payload:
return None
user_id = payload.get("sub")
if not self.session_valid(payload.get("jti", ""), user_id, payload.get("ver", 0)):
return None
return user_id
def revoke(self, token: str) -> bool:
rec = self.get(token)
if rec is None:
return False
row = self.session.get(SessionToken, rec["id"])
if row is None:
return False
row.revoked = True
self.session.commit()
return True
def revoke_all(self, user_id: str | None = None) -> int:
"""吊销会话(指定用户或全部),并递增其 token_version 使旧 JWT 失效。"""
if user_id is not None:
affected = self.session.execute(
delete(SessionToken).where(SessionToken.user_id == user_id)
).rowcount
u = self.session.get(User, user_id)
if u is not None:
u.token_version += 1
self.session.commit()
return affected
affected = self.session.execute(delete(SessionToken)).rowcount
for u in self.session.scalars(select(User)):
u.token_version += 1
self.session.commit()
return affected
# ---------------------------------------------------------------------------
# 角色 / 权限
# ---------------------------------------------------------------------------
class RoleRepository:
def __init__(self, session: Session):
self.session = session
def list_roles(self) -> list[dict]:
matrix = self.role_matrix()
return [
{
"id": r.id, "name": r.name, "scope_type": r.scope_type,
"description": r.description, "permissions": matrix.get(r.id, []),
}
for r in self.session.scalars(select(Role).order_by(Role.scope_type, Role.id))
]
def list_permissions(self) -> list[dict]:
return [
{"id": p.id, "name": p.name, "category": p.category, "module": p.module}
for p in self.session.scalars(select(Permission).order_by(Permission.module, Permission.id))
]
def permissions_for(self, role: str, sub_role: str | None) -> list[str]:
"""返回该用户(业务角色 + 可选子角色)的全部权限码。
合并 role 与 ``role|sub_role`` 两个角色的权限。
"""
ids = [role]
if sub_role:
ids.append(f"{role}|{sub_role}")
return sorted(
{
pid
for pid, in self.session.execute(
select(RolePermission.permission_id).where(
RolePermission.role_id.in_(ids)
)
)
}
)
def role_matrix(self) -> dict[str, list[str]]:
rows = self.session.execute(
select(RolePermission.role_id, RolePermission.permission_id)
).all()
matrix: dict[str, list[str]] = {}
for role_id, perm_id in rows:
matrix.setdefault(role_id, []).append(perm_id)
return matrix
def role_exists(self, role_id: str) -> bool:
return self.session.get(Role, role_id) is not None
def set_role_permissions(self, role_id: str, permissions: list[str]) -> list[str]:
"""重设某角色的权限集合(角色权限配置,仅超管)。"""
self.session.execute(
delete(RolePermission).where(RolePermission.role_id == role_id)
)
for pid in permissions:
if self.session.get(Permission, pid) is not None:
self.session.add(RolePermission(role_id=role_id, permission_id=pid))
self.session.commit()
return self.permissions_for_role(role_id)
def permissions_for_role(self, role_id: str) -> list[str]:
if "|" in role_id:
role, sub = role_id.split("|", 1)
return self.permissions_for(role, sub)
return self.permissions_for(role_id, None)
# ---------------------------------------------------------------------------
# 区域(数据范围层级)
# ---------------------------------------------------------------------------
class RegionRepository:
def __init__(self, session: Session):
self.session = session
def get(self, region_id: str) -> dict | None:
r = self.session.get(Region, region_id)
return {"id": r.id, "name": r.name, "level": r.level, "parent_id": r.parent_id} if r else None
def all(self) -> list[dict]:
return [
{"id": r.id, "name": r.name, "level": r.level, "parent_id": r.parent_id}
for r in self.session.scalars(select(Region))
]
def level(self, region_id: str) -> str | None:
r = self.session.get(Region, region_id)
return r.level if r else None
def _descendants(self, region_id: str) -> set[str]:
"""收集某区域的全部后代 id(含自身)。"""
result = {region_id}
rows = self.session.execute(
select(Region.id, Region.parent_id)
).all()
children: dict[str, list[str]] = {}
for rid, parent in rows:
if parent:
children.setdefault(parent, []).append(rid)
stack = list(children.get(region_id, []))
while stack:
cur = stack.pop()
if cur in result:
continue
result.add(cur)
stack.extend(children.get(cur, []))
return result
def visible_region_ids(self, region_id: str | None) -> list[str]:
"""数据范围:本域 + 全部后代(上级可看下级)。无区域则返回空。"""
if not region_id:
return []
return sorted(self._descendants(region_id))
# ---------------------------------------------------------------------------
# 组织(企业 / 载体 / 服务商)
# ---------------------------------------------------------------------------
class OrgRepository:
def __init__(self, session: Session):
self.session = session
def _to_dict(self, o: Organization) -> dict:
return {
"id": o.id,
"name": o.name,
"type": o.type,
"region_id": o.region_id,
"parent_id": o.parent_id,
"status": o.status,
"created_at": o.created_at,
}
def get(self, org_id: str) -> dict | None:
o = self.session.get(Organization, org_id)
return self._to_dict(o) if o else None
def all(self) -> list[dict]:
return [self._to_dict(o) for o in self.session.scalars(select(Organization))]
def list_by_region(self, region_ids: list[str]) -> list[dict]:
if not region_ids:
return []
return [
self._to_dict(o)
for o in self.session.scalars(
select(Organization).where(Organization.region_id.in_(region_ids))
)
]
def list_by_parent(self, parent_id: str) -> list[dict]:
return [
self._to_dict(o)
for o in self.session.scalars(
select(Organization).where(Organization.parent_id == parent_id)
)
]
# ---------------------------------------------------------------------------
# 审计日志
# ---------------------------------------------------------------------------
class AuditRepository:
def __init__(self, session: Session):
self.session = session
def add(self, *, action: str, resource: str, resource_id: str = "",
detail: str = "", ip: str = "", user_id: str | None = None) -> None:
self.session.add(
AuditLog(
id=new_id("aud"),
user_id=user_id,
action=action,
resource=resource,
resource_id=resource_id,
detail=detail,
ip=ip,
created_at=utcnow_iso(),
)
)
self.session.commit()
def list(self, limit: int = 100, offset: int = 0) -> list[dict]:
rows = self.session.scalars(
select(AuditLog).order_by(AuditLog.created_at.desc()).limit(limit).offset(offset)
)
return [
{
"id": a.id,
"user_id": a.user_id,
"action": a.action,
"resource": a.resource,
"resource_id": a.resource_id,
"detail": a.detail,
"ip": a.ip,
"created_at": a.created_at,
}
for a in rows
]
# ---------------------------------------------------------------------------
# 智能体表(身份记录;按用户隔离)
# ---------------------------------------------------------------------------
class AgentRepository:
def __init__(self, session: Session):
self.session = session
def _to_dict(self, a: Agent) -> dict:
return {
"id": a.id,
"user_id": a.user_id,
"port": a.port,
"name": a.name,
"description": a.description,
"language": a.language,
"model_name": a.model_name,
"deletable": a.deletable,
"use_fixed_soul": a.use_fixed_soul,
"created_at": a.created_at,
"updated_at": a.updated_at,
}
def get_by_user(self, user_id: str, port: str | None = None) -> list[dict]:
if not port:
return []
stmt = select(Agent).where(Agent.user_id == user_id, Agent.port == port)
rows = self.session.scalars(stmt.order_by(Agent.created_at))
return [self._to_dict(a) for a in rows]
def get(self, agent_id: str, user_id: str, port: str | None = None) -> dict | None:
if not port:
return None
stmt = select(Agent).where(Agent.id == agent_id, Agent.user_id == user_id, Agent.port == port)
a = self.session.scalar(stmt)
return self._to_dict(a) if a else None
def count_by_user(self, user_id: str, port: str | None = None) -> int:
if not port:
return 0
stmt = select(Agent.id).where(Agent.user_id == user_id, Agent.port == port)
return len(list(self.session.scalars(stmt)))
def create(
self,
user_id: str,
name: str,
*,
description: str = "",
language: str = "zh",
model_name: str = "",
agent_id: str | None = None,
port: str = "opc",
) -> dict:
now = utcnow_iso()
a = Agent(
id=agent_id or new_id("agent"),
user_id=user_id,
port=port,
name=name.strip(),
description=description,
language=language,
model_name=model_name,
deletable=True,
use_fixed_soul=False,
created_at=now,
updated_at=now,
)
self.session.add(a)
self.session.commit()
return self._to_dict(a)
def update(self, agent_id: str, user_id: str, fields: dict, port: str | None = None) -> dict | None:
if not port:
return None
stmt = select(Agent).where(Agent.id == agent_id, Agent.user_id == user_id, Agent.port == port)
a = self.session.scalar(stmt)
if a is None:
return None
allowed = {
k: fields[k]
for k in fields
if k in ("name", "description", "language", "model_name", "use_fixed_soul")
}
if not allowed:
return None
for k, v in allowed.items():
setattr(a, k, v)
a.updated_at = utcnow_iso()
self.session.commit()
return self._to_dict(a)
def delete(self, agent_id: str, user_id: str, port: str | None = None) -> bool:
if not port:
return False
stmt = select(Agent).where(Agent.id == agent_id, Agent.user_id == user_id, Agent.port == port)
a = self.session.scalar(stmt)
if a is None:
return False
self.session.delete(a)
self.session.commit()
return True
# ---------------------------------------------------------------------------
# 任务
# ---------------------------------------------------------------------------
class TaskRepository:
def __init__(self, session: Session):
self.session = session
def _to_dict(self, t: Task) -> dict:
return {
"id": t.id, "title": t.title, "category": t.category,
"sub_category": t.sub_category, "description": t.description,
"mode": t.mode, "budget_min": t.budget_min, "budget_max": t.budget_max,
"deadline": t.deadline, "status": t.status,
"publisher_org_id": t.publisher_org_id, "publisher_name": t.publisher_name,
"created_at": t.created_at, "updated_at": t.updated_at,
}
def list(self, status: str | None = None) -> list[dict]:
q = select(Task).order_by(Task.created_at.desc())
if status:
q = q.where(Task.status == status)
return [self._to_dict(t) for t in self.session.scalars(q)]
def get(self, task_id: str) -> dict | None:
t = self.session.get(Task, task_id)
return self._to_dict(t) if t else None
def create(self, fields: dict) -> dict:
now = utcnow_iso()
t = Task(id=new_id("task"), title=fields.get("title", ""),
category=fields.get("category", ""), sub_category=fields.get("sub_category", ""),
description=fields.get("description", ""), mode=fields.get("mode", "grab"),
budget_min=fields.get("budget_min", 0), budget_max=fields.get("budget_max", 0),
deadline=fields.get("deadline", ""), status=fields.get("status", "draft"),
publisher_org_id=fields.get("publisher_org_id"),
publisher_name=fields.get("publisher_name", ""),
created_at=now, updated_at=now)
self.session.add(t)
self.session.commit()
return self._to_dict(t)
def set_status(self, task_id: str, status: str) -> dict | None:
t = self.session.get(Task, task_id)
if t is None:
return None
t.status = status
t.updated_at = utcnow_iso()
self.session.commit()
return self._to_dict(t)
# ---------------------------------------------------------------------------
# 服务商
# ---------------------------------------------------------------------------
class ProviderRepository:
def __init__(self, session: Session):
self.session = session
def _to_dict(self, p: ServiceProvider) -> dict:
return {
"id": p.id, "name": p.name, "category": p.category,
"org_id": p.org_id, "level": p.level, "rating": p.rating,
"order_count": p.order_count, "status": p.status,
"contact": p.contact, "created_at": p.created_at, "updated_at": p.updated_at,
}
def list(self, status: str | None = None) -> list[dict]:
q = select(ServiceProvider).order_by(ServiceProvider.created_at.desc())
if status:
q = q.where(ServiceProvider.status == status)
return [self._to_dict(p) for p in self.session.scalars(q)]
def create(self, fields: dict) -> dict:
now = utcnow_iso()
p = ServiceProvider(id=new_id("prov"), name=fields.get("name", ""),
category=fields.get("category", ""), org_id=fields.get("org_id"),
level=fields.get("level", "certified"), rating=fields.get("rating", 5.0),
order_count=fields.get("order_count", 0),
status=fields.get("status", "pending"),
contact=fields.get("contact", ""), created_at=now, updated_at=now)
self.session.add(p)
self.session.commit()
return self._to_dict(p)
def update(self, provider_id: str, fields: dict) -> dict | None:
p = self.session.get(ServiceProvider, provider_id)
if p is None:
return None
for k in ("name", "category", "level", "status", "contact"):
if k in fields:
setattr(p, k, fields[k])
if "rating" in fields:
p.rating = fields["rating"]
p.updated_at = utcnow_iso()
self.session.commit()
return self._to_dict(p)
# ---------------------------------------------------------------------------
# 内容
# ---------------------------------------------------------------------------
class ContentRepository:
def __init__(self, session: Session):
self.session = session
def _to_dict(self, c: ContentItem) -> dict:
return {
"id": c.id, "type": c.type, "title": c.title, "summary": c.summary,
"body": c.body, "publisher_id": c.publisher_id, "status": c.status,
"created_at": c.created_at, "updated_at": c.updated_at,
}
def list(self, ctype: str | None = None, status: str | None = None) -> list[dict]:
q = select(ContentItem).order_by(ContentItem.created_at.desc())
if ctype:
q = q.where(ContentItem.type == ctype)
if status:
q = q.where(ContentItem.status == status)
return [self._to_dict(c) for c in self.session.scalars(q)]
def create(self, fields: dict) -> dict:
now = utcnow_iso()
c = ContentItem(id=new_id("cont"), type=fields.get("type", "news"),
title=fields.get("title", ""), summary=fields.get("summary", ""),
body=fields.get("body", ""), publisher_id=fields.get("publisher_id"),
status=fields.get("status", "draft"), created_at=now, updated_at=now)
self.session.add(c)
self.session.commit()
return self._to_dict(c)
def set_status(self, content_id: str, status: str) -> dict | None:
c = self.session.get(ContentItem, content_id)
if c is None:
return None
c.status = status
c.updated_at = utcnow_iso()
self.session.commit()
return self._to_dict(c)
# ---------------------------------------------------------------------------
# 系统配置
# ---------------------------------------------------------------------------
class ConfigRepository:
def __init__(self, session: Session):
self.session = session
def all(self) -> list[dict]:
return [
{"key": c.key, "value": c.value, "description": c.description,
"updated_at": c.updated_at}
for c in self.session.scalars(select(SystemConfig).order_by(SystemConfig.key))
]
def get(self, key: str) -> str | None:
c = self.session.get(SystemConfig, key)
return c.value if c else None
def set(self, key: str, value: str, description: str = "") -> dict:
c = self.session.get(SystemConfig, key)
if c is None:
c = SystemConfig(key=key, value=value, description=description, updated_at=utcnow_iso())
self.session.add(c)
else:
c.value = value
if description:
c.description = description
c.updated_at = utcnow_iso()
self.session.commit()
return {"key": c.key, "value": c.value, "description": c.description, "updated_at": c.updated_at}
# ---------------------------------------------------------------------------
# OPC 超级个体(工作台聚合数据)
# ---------------------------------------------------------------------------
class OpcProfileRepository:
"""OPC 档案(信用评分等)。"""
def __init__(self, session: Session):
self.session = session
def get(self, user_id: str) -> dict | None:
row = self.session.get(OpcProfile, user_id)
return self._to_dict(row) if row else None
def upsert(self, user_id: str, credit_score: int = 80) -> dict:
row = self.session.get(OpcProfile, user_id)
if row is None:
row = OpcProfile(
user_id=user_id, credit_score=credit_score,
created_at=utcnow_iso(), updated_at=utcnow_iso(),
)
self.session.add(row)
else:
row.credit_score = credit_score
row.updated_at = utcnow_iso()
self.session.commit()
return self._to_dict(row)
@staticmethod
def _to_dict(p: OpcProfile) -> dict:
return {
"user_id": p.user_id, "credit_score": p.credit_score,
"updated_at": p.updated_at,
}
class FinanceRepository:
"""OPC 财务流水(收入/支出)与月度聚合。"""
def __init__(self, session: Session):
self.session = session
def list_by_user(self, user_id: str, category: str | None = None) -> list[dict]:
stmt = select(FinanceRecord).where(FinanceRecord.user_id == user_id)
if category:
stmt = stmt.where(FinanceRecord.category == category)
rows = self.session.scalars(stmt.order_by(FinanceRecord.date.desc())).all()
return [self._to_dict(f) for f in rows]
def total_income(self, user_id: str) -> int:
return sum(
f.amount
for f in self.session.scalars(
select(FinanceRecord).where(
FinanceRecord.user_id == user_id,
FinanceRecord.category == "income",
)
)
)
def monthly_income(self, user_id: str, limit: int = 6) -> list[dict]:
"""按 YYYY-MM 汇总收入,取最近 N 个月(升序返回)。"""
agg: dict[str, int] = {}
for f in self.session.scalars(
select(FinanceRecord).where(
FinanceRecord.user_id == user_id,
FinanceRecord.category == "income",
)
):
month = (f.date or "")[:7]
if not month:
continue
agg[month] = agg.get(month, 0) + f.amount
months = sorted(agg.keys())[-limit:]
return [{"month": m, "amount": agg[m]} for m in months]
@staticmethod
def _to_dict(f: FinanceRecord) -> dict:
return {
"id": f.id, "user_id": f.user_id, "category": f.category,
"amount": f.amount, "date": f.date, "note": f.note,
"created_at": f.created_at,
}
class MessageRepository:
"""OPC 站内消息。"""
def __init__(self, session: Session):
self.session = session
def recent(self, user_id: str, limit: int = 4) -> list[dict]:
rows = self.session.scalars(
select(Message)
.where(Message.user_id == user_id)
.order_by(Message.created_at.desc())
.limit(limit)
).all()
return [self._to_dict(m) for m in rows]
def list_by_user(self, user_id: str) -> list[dict]:
rows = self.session.scalars(
select(Message)
.where(Message.user_id == user_id)
.order_by(Message.created_at.desc())
).all()
return [self._to_dict(m) for m in rows]
def unread_count(self, user_id: str) -> int:
return len(
self.session.scalars(
select(Message).where(Message.user_id == user_id, Message.read.is_(False))
).all()
)
@staticmethod
def _to_dict(m: Message) -> dict:
return {
"id": m.id, "user_id": m.user_id, "msg_type": m.msg_type,
"title": m.title, "content": m.content, "read": m.read,
"created_at": m.created_at,
}
class OpcTaskRepository:
"""OPC 个人任务看板。"""
def __init__(self, session: Session):
self.session = session
def list_by_user(self, user_id: str) -> list[dict]:
rows = self.session.scalars(
select(OpcTask)
.where(OpcTask.user_id == user_id)
.order_by(OpcTask.created_at.desc())
).all()
return [self._to_dict(t) for t in rows]
def counts(self, user_id: str) -> dict:
in_progress = 0
completed = 0
for row in self.session.scalars(
select(OpcTask).where(OpcTask.user_id == user_id)
):
if row.status == "completed":
completed += 1
elif row.status in ("in_progress", "urgent"):
in_progress += 1
return {"in_progress": in_progress, "completed": completed}
@staticmethod
def _to_dict(t: OpcTask) -> dict:
return {
"id": t.id, "user_id": t.user_id, "title": t.title, "status": t.status,
"budget": t.budget, "deadline": t.deadline, "progress": t.progress,
"created_at": t.created_at, "updated_at": t.updated_at,
}
# ---------------------------------------------------------------------------
# 账号↔端口身份(多身份绑定)
# ---------------------------------------------------------------------------
class IdentityRepository:
"""一个账号可绑定的多个端口身份(登录后选择其一进入)。"""
def __init__(self, session: Session):
self.session = session
def _to_dict(self, i: UserIdentity) -> dict:
org = self.session.get(Organization, i.org_id) if i.org_id else None
region = self.session.get(Region, i.region_id) if i.region_id else None
return {
"id": i.id,
"user_id": i.user_id,
"port": i.port,
"role": i.role,
"sub_role": i.sub_role,
"name": i.name,
"org_id": i.org_id,
"region_id": i.region_id,
"org_name": org.name if org else None,
"region_name": region.name if region else None,
"status": i.status,
"created_at": i.created_at,
"updated_at": i.updated_at,
}
def get(self, identity_id: str) -> dict | None:
row = self.session.get(UserIdentity, identity_id)
return self._to_dict(row) if row else None
def get_for_user(self, identity_id: str, user_id: str) -> dict | None:
row = self.session.scalar(
select(UserIdentity).where(
UserIdentity.id == identity_id,
UserIdentity.user_id == user_id,
)
)
return self._to_dict(row) if row else None
def list_for_user(self, user_id: str, active_only: bool = True) -> list[dict]:
stmt = select(UserIdentity).where(UserIdentity.user_id == user_id)
if active_only:
stmt = stmt.where(UserIdentity.status == "active")
rows = self.session.scalars(stmt.order_by(UserIdentity.port)).all()
return [self._to_dict(i) for i in rows]
def create(
self,
user_id: str,
*,
port: str,
role: str | None = None,
sub_role: str | None = None,
name: str = "",
org_id: str | None = None,
region_id: str | None = None,
) -> dict:
now = utcnow_iso()
row = UserIdentity(
id=new_id("ident"),
user_id=user_id,
port=port,
role=role or port,
sub_role=sub_role,
name=name,
org_id=org_id,
region_id=region_id,
status="active",
created_at=now,
updated_at=now,
)
self.session.add(row)
self.session.commit()
return self._to_dict(row)
def set_status(self, identity_id: str, status: str) -> dict | None:
row = self.session.get(UserIdentity, identity_id)
if row is None:
return None
row.status = status
row.updated_at = utcnow_iso()
self.session.commit()
return self._to_dict(row)
# ---------------------------------------------------------------------------
# 端口工作台(按端口 JSON 载荷)
# ---------------------------------------------------------------------------
class PortalDashboardRepository:
def __init__(self, session: Session):
self.session = session
def get(self, port: str) -> dict | None:
row = self.session.get(PortalDashboard, port)
if row is None:
return None
try:
payload = json.loads(row.payload)
except (ValueError, TypeError):
return {}
payload["port"] = row.port
payload["updated_at"] = row.updated_at
return payload
def set(self, port: str, payload: dict) -> dict:
row = self.session.get(PortalDashboard, port)
now = utcnow_iso()
data = json.dumps(payload, ensure_ascii=False)
if row is None:
row = PortalDashboard(port=port, payload=data, updated_at=now)
self.session.add(row)
else:
row.payload = data
row.updated_at = now
self.session.commit()
return self.get(port)
class PortalPageRepository:
"""端口子页面数据(按 端口+页面 存 JSON 载荷)。"""
def __init__(self, session: Session):
self.session = session
def get(self, port: str, page: str) -> dict | None:
row = self.session.get(PortalPage, (port, page))
if row is None:
return None
try:
return json.loads(row.payload)
except (ValueError, TypeError):
return {}
def set(self, port: str, page: str, payload: dict) -> dict:
row = self.session.get(PortalPage, (port, page))
now = utcnow_iso()
data = json.dumps(payload, ensure_ascii=False)
if row is None:
row = PortalPage(port=port, page=page, payload=data, updated_at=now)
self.session.add(row)
else:
row.payload = data
row.updated_at = now
self.session.commit()
return self.get(port, page)
# ---------------------------------------------------------------------------
# 投资人端(偏好 / 路演 / 报名 / 意向)
# ---------------------------------------------------------------------------
class InvestorPreferenceRepository:
def __init__(self, session: Session):
self.session = session
def get(self, user_id: str) -> dict | None:
row = self.session.get(InvestorPreference, user_id)
if row is None:
return None
return {
"user_id": row.user_id,
"industries": json.loads(row.industries or "[]"),
"stage": row.stage,
"amount_min": row.amount_min,
"amount_max": row.amount_max,
"region_id": row.region_id,
"updated_at": row.updated_at,
}
def upsert(self, user_id: str, fields: dict) -> dict:
row = self.session.get(InvestorPreference, user_id)
now = utcnow_iso()
if row is None:
row = InvestorPreference(user_id=user_id, updated_at=now)
self.session.add(row)
if "industries" in fields:
row.industries = json.dumps(fields["industries"], ensure_ascii=False)
if "stage" in fields:
row.stage = fields["stage"] or ""
if "amount_min" in fields:
row.amount_min = fields["amount_min"] or 0
if "amount_max" in fields:
row.amount_max = fields["amount_max"] or 0
if "region_id" in fields:
row.region_id = fields["region_id"]
row.updated_at = now
self.session.commit()
return self.get(user_id)
class RoadshowRepository:
def __init__(self, session: Session):
self.session = session
def _to_dict(self, r: Roadshow) -> dict:
region = self.session.get(Region, r.region_id) if r.region_id else None
return {
"id": r.id, "publisher_id": r.publisher_id, "publisher_role": r.publisher_role,
"title": r.title, "summary": r.summary, "activity_type": r.activity_type,
"scope_type": r.scope_type, "region_id": r.region_id,
"region_name": region.name if region else None,
"start_at": r.start_at, "end_at": r.end_at, "register_deadline": r.register_deadline,
"quota": r.quota, "venue": r.venue, "live_url": r.live_url,
"status": r.status, "need_review": r.need_review,
"review_comment": r.review_comment, "created_at": r.created_at, "updated_at": r.updated_at,
}
def list(self, status: str | None = None) -> list[dict]:
stmt = select(Roadshow).order_by(Roadshow.created_at.desc())
if status:
stmt = stmt.where(Roadshow.status == status)
return [self._to_dict(r) for r in self.session.scalars(stmt)]
def get(self, roadshow_id: str) -> dict | None:
row = self.session.get(Roadshow, roadshow_id)
return self._to_dict(row) if row else None
def create(self, fields: dict) -> dict:
now = utcnow_iso()
row = Roadshow(
id=new_id("rs"), publisher_id=fields.get("publisher_id", ""),
publisher_role=fields.get("publisher_role", "investor"),
title=fields.get("title", ""), summary=fields.get("summary", ""),
activity_type=fields.get("activity_type", "online"),
scope_type=fields.get("scope_type", "all"),
region_id=fields.get("region_id"),
start_at=fields.get("start_at", ""), end_at=fields.get("end_at", ""),
register_deadline=fields.get("register_deadline", ""),
quota=fields.get("quota", 0), venue=fields.get("venue", ""),
live_url=fields.get("live_url", ""),
status=fields.get("status", "draft"),
need_review=fields.get("need_review", True),
created_at=now, updated_at=now,
)
self.session.add(row)
self.session.commit()
return self._to_dict(row)
def set_status(self, roadshow_id: str, status: str, review_comment: str = "") -> dict | None:
row = self.session.get(Roadshow, roadshow_id)
if row is None:
return None
row.status = status
if review_comment:
row.review_comment = review_comment
row.updated_at = utcnow_iso()
self.session.commit()
return self._to_dict(row)
class RoadshowRegistrationRepository:
def __init__(self, session: Session):
self.session = session
def create(self, roadshow_id: str, user_id: str, role: str = "investor", note: str = "") -> dict:
row = RoadshowRegistration(
id=new_id("rsreg"), roadshow_id=roadshow_id, user_id=user_id,
role=role, status="applying", note=note, created_at=utcnow_iso(),
)
self.session.add(row)
self.session.commit()
return {"id": row.id, "roadshow_id": row.roadshow_id, "user_id": row.user_id,
"role": row.role, "status": row.status, "note": row.note}
class InvestmentIntentRepository:
def __init__(self, session: Session):
self.session = session
def create(self, investor_id: str, project_id: str, project_name: str, message: str = "") -> dict:
row = InvestmentIntent(
id=new_id("intent"), investor_id=investor_id, project_id=project_id,
project_name=project_name, status="interested", message=message,
created_at=utcnow_iso(), updated_at=utcnow_iso(),
)
self.session.add(row)
self.session.commit()
return {"id": row.id, "investor_id": row.investor_id, "project_id": row.project_id,
"project_name": row.project_name, "status": row.status, "message": row.message}
def list_for(self, investor_id: str) -> list[dict]:
rows = self.session.scalars(
select(InvestmentIntent).where(InvestmentIntent.investor_id == investor_id)
.order_by(InvestmentIntent.created_at.desc())
).all()
return [{"id": r.id, "project_id": r.project_id, "project_name": r.project_name,
"status": r.status, "message": r.message, "created_at": r.created_at} for r in rows]
class BidRepository:
"""任务竞标。"""
def __init__(self, session: Session):
self.session = session
def list_for_task(self, task_id: str) -> list[dict]:
rows = self.session.scalars(
select(Bid).where(Bid.task_id == task_id).order_by(Bid.created_at.desc())
).all()
return [self._to_dict(r) for r in rows]
def get(self, bid_id: str) -> dict | None:
row = self.session.get(Bid, bid_id)
return self._to_dict(row) if row else None
def create(self, task_id: str, opc_id: str, opc_name: str, quote: int, plan: str = "") -> dict:
row = Bid(
id=new_id("bid"), task_id=task_id, opc_id=opc_id, opc_name=opc_name,
quote=quote, plan=plan, status="submitted",
created_at=utcnow_iso(), updated_at=utcnow_iso(),
)
self.session.add(row)
self.session.commit()
return self._to_dict(row)
def set_status(self, bid_id: str, status: str) -> dict | None:
row = self.session.get(Bid, bid_id)
if row is None:
return None
row.status = status
row.updated_at = utcnow_iso()
self.session.commit()
return self._to_dict(row)
@staticmethod
def _to_dict(b: Bid) -> dict:
return {"id": b.id, "task_id": b.task_id, "opc_id": b.opc_id, "opc_name": b.opc_name,
"quote": b.quote, "plan": b.plan, "status": b.status,
"created_at": b.created_at, "updated_at": b.updated_at}
class SubsidyRepository:
"""补贴申报(政务三级审批)。"""
def __init__(self, session: Session):
self.session = session
def list(self, region_ids: list[str] | None = None) -> list[dict]:
rows = self.session.scalars(select(SubsidyApplication).order_by(SubsidyApplication.created_at.desc()))
out = []
for r in rows:
if region_ids and r.region_id and r.region_id not in region_ids:
continue
out.append(self._to_dict(r))
return out
def get(self, aid: str) -> dict | None:
row = self.session.get(SubsidyApplication, aid)
return self._to_dict(row) if row else None
def create(self, fields: dict) -> dict:
now = utcnow_iso()
row = SubsidyApplication(
id=new_id("sub"), opc_id=fields.get("opc_id", ""), opc_name=fields.get("opc_name", ""),
title=fields.get("title", ""), amount=fields.get("amount", 0),
region_id=fields.get("region_id"), status="applying",
created_at=now, updated_at=now,
)
self.session.add(row)
self.session.commit()
return self._to_dict(row)
def set_status(self, aid: str, status: str, comment: str = "") -> dict | None:
row = self.session.get(SubsidyApplication, aid)
if row is None:
return None
row.status = status
if comment:
row.comment = comment
row.updated_at = utcnow_iso()
self.session.commit()
return self._to_dict(row)
@staticmethod
def _to_dict(s: SubsidyApplication) -> dict:
return {"id": s.id, "opc_id": s.opc_id, "opc_name": s.opc_name, "title": s.title,
"amount": s.amount, "region_id": s.region_id, "status": s.status,
"comment": s.comment, "created_at": s.created_at}
class ServiceReferralRepository:
"""载体-服务商引荐。"""
def __init__(self, session: Session):
self.session = session
def list_for(self, carrier_id: str | None = None) -> list[dict]:
stmt = select(ServiceReferral).order_by(ServiceReferral.created_at.desc())
if carrier_id:
stmt = stmt.where(ServiceReferral.carrier_id == carrier_id)
return [self._to_dict(r) for r in self.session.scalars(stmt)]
def create(self, fields: dict) -> dict:
row = ServiceReferral(
id=new_id("ref"), carrier_id=fields.get("carrier_id", ""),
provider_id=fields.get("provider_id", ""), provider_name=fields.get("provider_name", ""),
opc_id=fields.get("opc_id", ""), opc_name=fields.get("opc_name", ""),
status="referred", created_at=utcnow_iso(), updated_at=utcnow_iso(),
)
self.session.add(row)
self.session.commit()
return self._to_dict(row)
@staticmethod
def _to_dict(r: ServiceReferral) -> dict:
return {"id": r.id, "carrier_id": r.carrier_id, "provider_id": r.provider_id,
"provider_name": r.provider_name, "opc_id": r.opc_id, "opc_name": r.opc_name,
"status": r.status, "created_at": r.created_at}
class TrainingEnrollmentRepository:
"""投融资培训报名。"""
def __init__(self, session: Session):
self.session = session
def list_for(self, user_id: str) -> list[dict]:
rows = self.session.scalars(
select(TrainingEnrollment).where(TrainingEnrollment.user_id == user_id)
).all()
return [{"id": r.id, "training_id": r.training_id, "training_name": r.training_name,
"status": r.status, "created_at": r.created_at} for r in rows]
def create(self, user_id: str, training_id: str, training_name: str) -> dict:
row = TrainingEnrollment(
id=new_id("treg"), user_id=user_id, training_id=training_id,
training_name=training_name, status="enrolled", created_at=utcnow_iso(),
)
self.session.add(row)
self.session.commit()
return {"id": row.id, "training_id": row.training_id, "training_name": row.training_name,
"status": row.status}
class EscrowRepository:
"""任务资金托管/结算。"""
def __init__(self, session: Session):
self.session = session
def list(self, status: str | None = None) -> list[dict]:
stmt = select(Escrow).order_by(Escrow.created_at.desc())
if status:
stmt = stmt.where(Escrow.status == status)
return [self._to_dict(r) for r in self.session.scalars(stmt)]
def get(self, escrow_id: str) -> dict | None:
row = self.session.get(Escrow, escrow_id)
return self._to_dict(row) if row else None
def create(self, task_id: str, task_title: str, amount: int, commission: int = 0) -> dict:
row = Escrow(id=new_id("esc"), task_id=task_id, task_title=task_title,
amount=amount, commission=commission, status="deposited",
created_at=utcnow_iso(), updated_at=utcnow_iso())
self.session.add(row)
self.session.commit()
return self._to_dict(row)
def set_status(self, escrow_id: str, status: str) -> dict | None:
row = self.session.get(Escrow, escrow_id)
if row is None:
return None
row.status = status
row.updated_at = utcnow_iso()
self.session.commit()
return self._to_dict(row)
@staticmethod
def _to_dict(e: Escrow) -> dict:
return {"id": e.id, "task_id": e.task_id, "task_title": e.task_title, "amount": e.amount,
"commission": e.commission, "status": e.status, "created_at": e.created_at}
class ContractRepository:
def __init__(self, session: Session):
self.session = session
def get_for_task(self, task_id: str) -> dict | None:
row = self.session.scalar(select(Contract).where(Contract.task_id == task_id))
return self._to_dict(row) if row else None
def create(self, task_id: str, task_title: str, enterprise_id: str, opc_id: str) -> dict:
row = Contract(id=new_id("ct"), task_id=task_id, task_title=task_title,
enterprise_id=enterprise_id, opc_id=opc_id, status="signed",
content=f"任务《{task_title}》电子合同,双方已签署,资金由平台托管。",
created_at=utcnow_iso())
self.session.add(row)
self.session.commit()
return self._to_dict(row)
@staticmethod
def _to_dict(c: Contract) -> dict:
return {"id": c.id, "task_id": c.task_id, "task_title": c.task_title,
"enterprise_id": c.enterprise_id, "opc_id": c.opc_id, "status": c.status,
"content": c.content, "created_at": c.created_at}
class DisputeRepository:
def __init__(self, session: Session):
self.session = session
def list(self, status: str | None = None) -> list[dict]:
stmt = select(Dispute).order_by(Dispute.created_at.desc())
if status:
stmt = stmt.where(Dispute.status == status)
return [self._to_dict(r) for r in self.session.scalars(stmt)]
def create(self, task_id: str, task_title: str, initiator: str, reason: str) -> dict:
row = Dispute(id=new_id("disp"), task_id=task_id, task_title=task_title,
initiator=initiator, reason=reason, status="opened", created_at=utcnow_iso())
self.session.add(row)
self.session.commit()
return self._to_dict(row)
def set_status(self, dispute_id: str, status: str, resolution: str = "") -> dict | None:
row = self.session.get(Dispute, dispute_id)
if row is None:
return None
row.status = status
if resolution:
row.resolution = resolution
self.session.commit()
return self._to_dict(row)
@staticmethod
def _to_dict(d: Dispute) -> dict:
return {"id": d.id, "task_id": d.task_id, "task_title": d.task_title,
"initiator": d.initiator, "reason": d.reason, "status": d.status,
"resolution": d.resolution, "created_at": d.created_at}
class RatingRepository:
def __init__(self, session: Session):
self.session = session
def avg_for(self, user_id: str) -> float:
rows = self.session.scalars(select(Rating).where(Rating.to_id == user_id)).all()
return round(sum(r.score for r in rows) / len(rows), 1) if rows else 5.0
def create(self, task_id: str, from_id: str, to_id: str, score: int, comment: str = "") -> dict:
row = Rating(id=new_id("rate"), task_id=task_id, from_id=from_id, to_id=to_id,
score=max(1, min(5, score)), comment=comment, created_at=utcnow_iso())
self.session.add(row)
self.session.commit()
return {"id": row.id, "score": row.score, "comment": row.comment}
class NotificationRepository:
def __init__(self, session: Session):
self.session = session
def list_for(self, user_id: str, limit: int = 50) -> list[dict]:
rows = self.session.scalars(
select(Notification).where(Notification.user_id == user_id)
.order_by(Notification.created_at.desc()).limit(limit)
).all()
return [{"id": r.id, "type": r.type, "title": r.title, "content": r.content,
"read": r.read, "created_at": r.created_at} for r in rows]
def unread(self, user_id: str) -> int:
return len(self.session.scalars(
select(Notification).where(Notification.user_id == user_id, Notification.read.is_(False))
).all())
def create(self, user_id: str, type: str, title: str, content: str) -> dict:
row = Notification(id=new_id("ntf"), user_id=user_id, type=type, title=title,
content=content, read=False, created_at=utcnow_iso())
self.session.add(row)
self.session.commit()
return {"id": row.id, "title": row.title}
def mark_read(self, user_id: str) -> int:
rows = self.session.scalars(select(Notification).where(Notification.user_id == user_id)).all()
for r in rows:
r.read = True
self.session.commit()
return len(rows)
# ---------------------------------------------------------------------------
# 机构成员(机构主账号 + 机构内子账号)
# ---------------------------------------------------------------------------
class OrganizationMemberRepository:
def __init__(self, session: Session):
self.session = session
def list_for_org(self, org_id: str) -> list[dict]:
rows = self.session.scalars(
select(OrganizationMember).where(OrganizationMember.org_id == org_id)
).all()
out = []
for r in rows:
u = self.session.get(User, r.user_id)
out.append({"org_id": r.org_id, "user_id": r.user_id,
"username": u.username if u else r.user_id,
"nickname": u.nickname if u else "",
"role": r.role, "is_admin": r.is_admin, "status": r.status})
return out
def is_member(self, org_id: str, user_id: str) -> bool:
return self.session.get(OrganizationMember, (org_id, user_id)) is not None
def is_admin(self, org_id: str, user_id: str) -> bool:
row = self.session.get(OrganizationMember, (org_id, user_id))
return bool(row and row.is_admin)
def add_member(self, org_id: str, user_id: str, role: str = "member", is_admin: bool = False) -> dict:
row = self.session.get(OrganizationMember, (org_id, user_id))
if row is None:
row = OrganizationMember(org_id=org_id, user_id=user_id, role=role,
is_admin=is_admin, status="active", joined_at=utcnow_iso())
self.session.add(row)
else:
row.role = role
row.is_admin = is_admin
self.session.commit()
return {"org_id": org_id, "user_id": user_id, "role": role, "is_admin": is_admin}
# ---------------------------------------------------------------------------
# 统计
# ---------------------------------------------------------------------------
class StatsRepository:
"""运营端/政务端数据总览(基于现有表聚合)。"""
def __init__(self, session: Session):
self.session = session
def overview(self, region_ids: list[str] | None = None) -> dict:
users = self.session.scalars(select(User)).all()
if region_ids is not None:
scope = set(region_ids)
users = [u for u in users if not u.region_id or u.region_id in scope]
tasks = self.session.scalars(select(Task)).all()
providers = self.session.scalars(select(ServiceProvider)).all()
content = self.session.scalars(select(ContentItem)).all()
active_users = [u for u in users if u.status == "active"]
return {
"user_count": len(users),
"active_user_count": len(active_users),
"task_count": len(tasks),
"published_task_count": len([t for t in tasks if t.status == "published"]),
"provider_count": len(providers),
"active_provider_count": len([p for p in providers if p.status == "active"]),
"content_count": len(content),
"published_content_count": len([c for c in content if c.status == "published"]),
}
# ---------------------------------------------------------------------------
# 数据库门面
# ---------------------------------------------------------------------------
class Database:
"""持有全部 Repository,统一访问入口。"""
def __init__(self, db_url: str | None = None, session: Session | None = None):
if session is None:
from sqlalchemy.orm import sessionmaker
from .db import make_engine
url = db_url or config.DATABASE_URL
self._engine = make_engine(url)
self._session_factory = sessionmaker(
bind=self._engine, autoflush=False, expire_on_commit=False,
)
self._owns_session = True
else:
self._engine = None
self._session_factory = lambda: session # type: ignore[assignment]
self._owns_session = False
self.session = self._session_factory()
self.users = UserRepository(self.session)
self.tokens = TokenRepository(self.session)
self.agents = AgentRepository(self.session)
self.roles = RoleRepository(self.session)
self.orgs = OrgRepository(self.session)
self.regions = RegionRepository(self.session)
self.audit = AuditRepository(self.session)
self.tasks = TaskRepository(self.session)
self.providers = ProviderRepository(self.session)
self.content = ContentRepository(self.session)
self.config = ConfigRepository(self.session)
self.identities = IdentityRepository(self.session)
self.portal_dashboards = PortalDashboardRepository(self.session)
self.portal_pages = PortalPageRepository(self.session)
self.opc_profiles = OpcProfileRepository(self.session)
self.finance = FinanceRepository(self.session)
self.messages = MessageRepository(self.session)
self.opc_tasks = OpcTaskRepository(self.session)
self.investor_prefs = InvestorPreferenceRepository(self.session)
self.roadshows = RoadshowRepository(self.session)
self.roadshow_regs = RoadshowRegistrationRepository(self.session)
self.intents = InvestmentIntentRepository(self.session)
self.bids = BidRepository(self.session)
self.subsidies = SubsidyRepository(self.session)
self.referrals = ServiceReferralRepository(self.session)
self.training_enrolls = TrainingEnrollmentRepository(self.session)
self.escrows = EscrowRepository(self.session)
self.contracts = ContractRepository(self.session)
self.disputes = DisputeRepository(self.session)
self.ratings = RatingRepository(self.session)
self.notifications = NotificationRepository(self.session)
self.org_members = OrganizationMemberRepository(self.session)
self.stats = StatsRepository(self.session)
def initialize(self) -> None:
"""建表(本实例的 engine)+ 幂等种子(roles 空时才写)。"""
from .seed import seed_data
if self._engine is not None:
from .db import create_all
create_all(self._engine)
seed_data(self.session)
def close(self) -> None:
if self._owns_session:
self.session.close()