Files
server-core/app/infrastructure/repositories.py
T
Pine 75caab4d39 feat(db): P2 摘除运行时种子 + 脚本化迁移/种子(migrate.py/seed.py)
- Database.initialize() 不再 create_all/seed_data:运行时仅做库连通检查(不建表不灌种子)。
- 新增 scripts/db/migrate.py(封装 alembic upgrade/downgrade)、scripts/db/seed.py(非运行态灌平台种子)。
- 新增 serverdata/seed/(README) 存放种子 JSON。
- 迁移/种子流程:uv run python scripts/db/migrate.py + scripts/db/seed.py,再启动应用。
- 已知待修:seed 内 AGENT_SEED 全局固定 id 唯一冲突(既有缺陷, 待 P2 后续修);pine/角色已能灌入。
2026-08-24 19:32:41 +08:00

1871 lines
70 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.ext.asyncio import AsyncSession
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,
"wx_openid": u.wx_openid,
"phone": u.phone,
"created_at": u.created_at,
"updated_at": u.updated_at,
}
# ---------------------------------------------------------------------------
# 用户表
# ---------------------------------------------------------------------------
class UserRepository:
def __init__(self, session: AsyncSession):
self.session = session
async def has_users(self) -> bool:
return await self.session.scalar(select(User.id).limit(1)) is not None
async def get_by_username(self, username: str) -> dict | None:
u = await self.session.scalar(
select(User).where(User.username.ilike(username.strip())),
)
return _user_to_dict(u) if u else None
async def get_by_id(self, user_id: str) -> dict | None:
u = await self.session.get(User, user_id)
return _user_to_dict(u) if u else None
async def list(self) -> list[dict]:
return [_user_to_dict(u) for u in await self.session.scalars(select(User).order_by(User.created_at))]
async def delete(self, user_id: str) -> bool:
"""硬删除用户(含其身份/会话在级联范围内由 DB 层处理)。"""
u = await self.session.get(User, user_id)
if u is None:
return False
await self.session.delete(u)
await self.session.commit()
return True
async 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,
wx_openid: str = "",
phone: str = "",
) -> 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,
wx_openid=wx_openid,
phone=phone,
created_at=now,
updated_at=now,
)
self.session.add(u)
await self.session.commit()
return _user_to_dict(u)
async def get_by_wx_openid(self, wx_openid: str) -> dict | None:
u = await self.session.scalar(
select(User).where(User.wx_openid == wx_openid.strip()),
)
return _user_to_dict(u) if u else None
async def set_wx_openid(self, user_id: str, wx_openid: str) -> dict | None:
u = await self.session.get(User, user_id)
if u is None:
return None
u.wx_openid = wx_openid.strip()
u.updated_at = utcnow_iso()
await self.session.commit()
return _user_to_dict(u)
async def set_phone(self, user_id: str, phone: str) -> dict | None:
u = await self.session.get(User, user_id)
if u is None:
return None
u.phone = phone.strip()
u.updated_at = utcnow_iso()
await self.session.commit()
return _user_to_dict(u)
async def update_profile(self, user_id: str, fields: dict) -> dict | None:
u = await 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()
await self.session.commit()
return _user_to_dict(u)
async def update_credentials(
self,
user_id: str,
new_username: str | None,
new_password: str | None,
) -> dict | None:
u = await 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()
await self.session.commit()
return _user_to_dict(u)
async def verify_password(self, user: dict, password: str) -> bool:
return verify_password(
password,
user.get("password_hash", ""),
user.get("password_salt", ""),
)
async def set_role(
self,
user_id: str,
role: str,
sub_role: str | None,
org_id: str | None,
region_id: str | None,
) -> dict | None:
u = await self.session.get(User, user_id)
if u is None:
return None
# 同步该用户"主角色"身份(角色改变需对已绑定身份会话生效)
new_port = _PORT_FOR_ROLE.get(role, "opc")
for ident in await 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()
await self.session.commit()
return _user_to_dict(u)
async def set_status(self, user_id: str, status: str) -> dict | None:
u = await self.session.get(User, user_id)
if u is None:
return None
u.status = status
u.updated_at = utcnow_iso()
await self.session.commit()
return _user_to_dict(u)
async def bump_token_version(self, user_id: str) -> int:
u = await self.session.get(User, user_id)
if u is None:
return 0
u.token_version += 1
u.updated_at = utcnow_iso()
await self.session.commit()
return u.token_version
async 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: AsyncSession):
self.session = session
async 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)
await self.session.commit()
return {"id": jti, "token": token, "user_id": user["id"]}
async 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 = await 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,
}
async def session_valid(self, jti: str, user_id: str, ver: int) -> bool:
"""JWT 声称的会话与用户版本是否仍有效。"""
rec = await 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 = await self.session.get(User, user_id)
return u is not None and u.token_version == ver and u.status == "active"
async 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
async def revoke(self, token: str) -> bool:
rec = await self.get(token)
if rec is None:
return False
row = await self.session.get(SessionToken, rec["id"])
if row is None:
return False
row.revoked = True
await self.session.commit()
return True
async def revoke_all(self, user_id: str | None = None) -> int:
"""吊销会话(指定用户或全部),并递增其 token_version 使旧 JWT 失效。"""
if user_id is not None:
affected = (await self.session.execute(
delete(SessionToken).where(SessionToken.user_id == user_id)
)).rowcount
u = await self.session.get(User, user_id)
if u is not None:
u.token_version += 1
await self.session.commit()
return affected
affected = (await self.session.execute(delete(SessionToken))).rowcount
for u in await self.session.scalars(select(User)):
u.token_version += 1
await self.session.commit()
return affected
# ---------------------------------------------------------------------------
# 角色 / 权限
# ---------------------------------------------------------------------------
class RoleRepository:
def __init__(self, session: AsyncSession):
self.session = session
async def list_roles(self) -> list[dict]:
matrix = await 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 await self.session.scalars(select(Role).order_by(Role.scope_type, Role.id))
]
async def list_permissions(self) -> list[dict]:
return [
{"id": p.id, "name": p.name, "category": p.category, "module": p.module}
for p in await self.session.scalars(select(Permission).order_by(Permission.module, Permission.id))
]
async 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 (await self.session.execute(
select(RolePermission.permission_id).where(
RolePermission.role_id.in_(ids)
)
))
}
)
async def role_matrix(self) -> dict[str, list[str]]:
rows = (await 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
async def role_exists(self, role_id: str) -> bool:
return await self.session.get(Role, role_id) is not None
async def set_role_permissions(self, role_id: str, permissions: list[str]) -> list[str]:
"""重设某角色的权限集合(角色权限配置,仅超管)。"""
(await self.session.execute(
delete(RolePermission).where(RolePermission.role_id == role_id)
))
for pid in permissions:
if await self.session.get(Permission, pid) is not None:
self.session.add(RolePermission(role_id=role_id, permission_id=pid))
await self.session.commit()
return await self.permissions_for_role(role_id)
async def permissions_for_role(self, role_id: str) -> list[str]:
if "|" in role_id:
role, sub = role_id.split("|", 1)
return await self.permissions_for(role, sub)
return await self.permissions_for(role_id, None)
# ---------------------------------------------------------------------------
# 区域(数据范围层级)
# ---------------------------------------------------------------------------
class RegionRepository:
def __init__(self, session: AsyncSession):
self.session = session
async def get(self, region_id: str) -> dict | None:
r = await 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
async def all(self) -> list[dict]:
return [
{"id": r.id, "name": r.name, "level": r.level, "parent_id": r.parent_id}
for r in await self.session.scalars(select(Region))
]
async def level(self, region_id: str) -> str | None:
r = await self.session.get(Region, region_id)
return r.level if r else None
async def _descendants(self, region_id: str) -> set[str]:
"""收集某区域的全部后代 id(含自身)。"""
result = {region_id}
rows = (await 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
async def visible_region_ids(self, region_id: str | None) -> list[str]:
"""数据范围:本域 + 全部后代(上级可看下级)。无区域则返回空。"""
if not region_id:
return []
return sorted(await self._descendants(region_id))
# ---------------------------------------------------------------------------
# 组织(企业 / 载体 / 服务商)
# ---------------------------------------------------------------------------
class OrgRepository:
def __init__(self, session: AsyncSession):
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,
}
async def all(self) -> list[dict]:
rows = await self.session.scalars(select(Organization).order_by(Organization.created_at))
return [self._to_dict(o) for o in rows]
async def list_by_ids(self, org_ids: list[str]) -> list[dict]:
if not org_ids:
return []
rows = await self.session.scalars(select(Organization).where(Organization.id.in_(org_ids)))
return [self._to_dict(o) for o in rows]
async def get(self, org_id: str) -> dict | None:
o = await self.session.get(Organization, org_id)
return self._to_dict(o) if o else None
async def all(self) -> list[dict]:
return [self._to_dict(o) for o in await self.session.scalars(select(Organization))]
async def list_by_region(self, region_ids: list[str]) -> list[dict]:
if not region_ids:
return []
return [
self._to_dict(o)
for o in await self.session.scalars(
select(Organization).where(Organization.region_id.in_(region_ids))
)
]
async def list_by_parent(self, parent_id: str) -> list[dict]:
return [
self._to_dict(o)
for o in await self.session.scalars(
select(Organization).where(Organization.parent_id == parent_id)
)
]
# ---------------------------------------------------------------------------
# 审计日志
# ---------------------------------------------------------------------------
class AuditRepository:
def __init__(self, session: AsyncSession):
self.session = session
async 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(),
)
)
await self.session.commit()
async def list(self, limit: int = 100, offset: int = 0) -> list[dict]:
rows = await 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: AsyncSession):
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,
}
async 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 = await self.session.scalars(stmt.order_by(Agent.created_at))
return [self._to_dict(a) for a in rows]
async 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 = await self.session.scalar(stmt)
return self._to_dict(a) if a else None
async 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(await self.session.scalars(stmt)))
async 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)
await self.session.commit()
return self._to_dict(a)
async 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 = await 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()
await self.session.commit()
return self._to_dict(a)
async 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 = await self.session.scalar(stmt)
if a is None:
return False
self.session.delete(a)
await self.session.commit()
return True
# ---------------------------------------------------------------------------
# 任务
# ---------------------------------------------------------------------------
class TaskRepository:
def __init__(self, session: AsyncSession):
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,
}
async 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 await self.session.scalars(q)]
async def get(self, task_id: str) -> dict | None:
t = await self.session.get(Task, task_id)
return self._to_dict(t) if t else None
async 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)
await self.session.commit()
return self._to_dict(t)
async def set_status(self, task_id: str, status: str) -> dict | None:
t = await self.session.get(Task, task_id)
if t is None:
return None
t.status = status
t.updated_at = utcnow_iso()
await self.session.commit()
return self._to_dict(t)
# ---------------------------------------------------------------------------
# 服务商
# ---------------------------------------------------------------------------
class ProviderRepository:
def __init__(self, session: AsyncSession):
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,
}
async 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 await self.session.scalars(q)]
async 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)
await self.session.commit()
return self._to_dict(p)
async def update(self, provider_id: str, fields: dict) -> dict | None:
p = await 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()
await self.session.commit()
return self._to_dict(p)
# ---------------------------------------------------------------------------
# 内容
# ---------------------------------------------------------------------------
class ContentRepository:
def __init__(self, session: AsyncSession):
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,
}
async 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 await self.session.scalars(q)]
async 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)
await self.session.commit()
return self._to_dict(c)
async def set_status(self, content_id: str, status: str) -> dict | None:
c = await self.session.get(ContentItem, content_id)
if c is None:
return None
c.status = status
c.updated_at = utcnow_iso()
await self.session.commit()
return self._to_dict(c)
# ---------------------------------------------------------------------------
# 系统配置
# ---------------------------------------------------------------------------
class ConfigRepository:
def __init__(self, session: AsyncSession):
self.session = session
async def all(self) -> list[dict]:
return [
{"key": c.key, "value": c.value, "description": c.description,
"updated_at": c.updated_at}
for c in await self.session.scalars(select(SystemConfig).order_by(SystemConfig.key))
]
async def get(self, key: str) -> str | None:
c = await self.session.get(SystemConfig, key)
return c.value if c else None
async def set(self, key: str, value: str, description: str = "") -> dict:
c = await 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()
await 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: AsyncSession):
self.session = session
async def get(self, user_id: str) -> dict | None:
row = await self.session.get(OpcProfile, user_id)
return self._to_dict(row) if row else None
async def upsert(self, user_id: str, credit_score: int = 80) -> dict:
row = await 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()
await 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: AsyncSession):
self.session = session
async def create(self, user_id: str, category: str, amount: int, date: str, note: str = "") -> dict:
"""新增一条财务流水。"""
rec = FinanceRecord(
id=new_id("fin"), user_id=user_id, category=category, amount=amount,
date=date, note=note, created_at=utcnow_iso(),
)
self.session.add(rec)
await self.session.commit()
return self._to_dict(rec)
async 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 = (await self.session.scalars(stmt.order_by(FinanceRecord.date.desc()))).all()
return [self._to_dict(f) for f in rows]
async def total_income(self, user_id: str) -> int:
return sum(
f.amount
for f in await self.session.scalars(
select(FinanceRecord).where(
FinanceRecord.user_id == user_id,
FinanceRecord.category == "income",
)
)
)
async def monthly_income(self, user_id: str, limit: int = 6) -> list[dict]:
"""按 YYYY-MM 汇总收入,取最近 N 个月(升序返回)。"""
agg: dict[str, int] = {}
for f in await 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: AsyncSession):
self.session = session
async def recent(self, user_id: str, limit: int = 4) -> list[dict]:
rows = (await 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]
async def list_by_user(self, user_id: str) -> list[dict]:
rows = (await 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]
async def unread_count(self, user_id: str) -> int:
return len(
(await 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: AsyncSession):
self.session = session
async def list_by_user(self, user_id: str) -> list[dict]:
rows = (await 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]
async def counts(self, user_id: str) -> dict:
in_progress = 0
completed = 0
for row in await 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: AsyncSession):
self.session = session
async def _to_dict(self, i: UserIdentity) -> dict:
org = await self.session.get(Organization, i.org_id) if i.org_id else None
region = await 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,
}
async def get(self, identity_id: str) -> dict | None:
row = await self.session.get(UserIdentity, identity_id)
return await self._to_dict(row) if row else None
async def get_for_user(self, identity_id: str, user_id: str) -> dict | None:
row = await self.session.scalar(
select(UserIdentity).where(
UserIdentity.id == identity_id,
UserIdentity.user_id == user_id,
)
)
return await self._to_dict(row) if row else None
async 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 = (await self.session.scalars(stmt.order_by(UserIdentity.port))).all()
return [await self._to_dict(i) for i in rows]
async 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)
await self.session.commit()
return await self._to_dict(row)
async def set_status(self, identity_id: str, status: str) -> dict | None:
row = await self.session.get(UserIdentity, identity_id)
if row is None:
return None
row.status = status
row.updated_at = utcnow_iso()
await self.session.commit()
return await self._to_dict(row)
# ---------------------------------------------------------------------------
# 端口工作台(按端口 JSON 载荷)
# ---------------------------------------------------------------------------
class PortalDashboardRepository:
def __init__(self, session: AsyncSession):
self.session = session
async def get(self, port: str) -> dict | None:
row = await 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
async def set(self, port: str, payload: dict) -> dict:
row = await 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
await self.session.commit()
return await self.get(port)
class PortalPageRepository:
"""端口子页面数据(按 端口+页面 存 JSON 载荷)。"""
def __init__(self, session: AsyncSession):
self.session = session
async def get(self, port: str, page: str) -> dict | None:
row = await self.session.get(PortalPage, (port, page))
if row is None:
return None
try:
return json.loads(row.payload)
except (ValueError, TypeError):
return {}
async def set(self, port: str, page: str, payload: dict) -> dict:
row = await 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
await self.session.commit()
return await self.get(port, page)
# ---------------------------------------------------------------------------
# 投资人端(偏好 / 路演 / 报名 / 意向)
# ---------------------------------------------------------------------------
class InvestorPreferenceRepository:
def __init__(self, session: AsyncSession):
self.session = session
async def get(self, user_id: str) -> dict | None:
row = await 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,
}
async def upsert(self, user_id: str, fields: dict) -> dict:
row = await 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
await self.session.commit()
return await self.get(user_id)
class RoadshowRepository:
def __init__(self, session: AsyncSession):
self.session = session
async def _to_dict(self, r: Roadshow) -> dict:
region = await 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,
}
async 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 [await self._to_dict(r) for r in await self.session.scalars(stmt)]
async def get(self, roadshow_id: str) -> dict | None:
row = await self.session.get(Roadshow, roadshow_id)
return await self._to_dict(row) if row else None
async 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)
await self.session.commit()
return await self._to_dict(row)
async def set_status(self, roadshow_id: str, status: str, review_comment: str = "") -> dict | None:
row = await 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()
await self.session.commit()
return await self._to_dict(row)
class RoadshowRegistrationRepository:
def __init__(self, session: AsyncSession):
self.session = session
async 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)
await 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: AsyncSession):
self.session = session
async 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)
await 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}
async def list_for(self, investor_id: str) -> list[dict]:
rows = (await 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: AsyncSession):
self.session = session
async def list_for_task(self, task_id: str) -> list[dict]:
rows = (await 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]
async def get(self, bid_id: str) -> dict | None:
row = await self.session.get(Bid, bid_id)
return self._to_dict(row) if row else None
async 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)
await self.session.commit()
return self._to_dict(row)
async def set_status(self, bid_id: str, status: str) -> dict | None:
row = await self.session.get(Bid, bid_id)
if row is None:
return None
row.status = status
row.updated_at = utcnow_iso()
await 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: AsyncSession):
self.session = session
async def list(self, region_ids: list[str] | None = None) -> list[dict]:
rows = await 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
async def get(self, aid: str) -> dict | None:
row = await self.session.get(SubsidyApplication, aid)
return self._to_dict(row) if row else None
async 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)
await self.session.commit()
return self._to_dict(row)
async def set_status(self, aid: str, status: str, comment: str = "") -> dict | None:
row = await self.session.get(SubsidyApplication, aid)
if row is None:
return None
row.status = status
if comment:
row.comment = comment
row.updated_at = utcnow_iso()
await 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: AsyncSession):
self.session = session
async 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 [await self._to_dict(r) for r in await self.session.scalars(stmt)]
async 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)
await 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: AsyncSession):
self.session = session
async def list_for(self, user_id: str) -> list[dict]:
rows = (await 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]
async 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)
await 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: AsyncSession):
self.session = session
async 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 [await self._to_dict(r) for r in await self.session.scalars(stmt)]
async def get(self, escrow_id: str) -> dict | None:
row = await self.session.get(Escrow, escrow_id)
return self._to_dict(row) if row else None
async 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)
await self.session.commit()
return self._to_dict(row)
async def set_status(self, escrow_id: str, status: str) -> dict | None:
row = await self.session.get(Escrow, escrow_id)
if row is None:
return None
row.status = status
row.updated_at = utcnow_iso()
await 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: AsyncSession):
self.session = session
async def get_for_task(self, task_id: str) -> dict | None:
row = await self.session.scalar(select(Contract).where(Contract.task_id == task_id))
return self._to_dict(row) if row else None
async 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)
await 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: AsyncSession):
self.session = session
async 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 [await self._to_dict(r) for r in await self.session.scalars(stmt)]
async 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)
await self.session.commit()
return self._to_dict(row)
async def set_status(self, dispute_id: str, status: str, resolution: str = "") -> dict | None:
row = await self.session.get(Dispute, dispute_id)
if row is None:
return None
row.status = status
if resolution:
row.resolution = resolution
await 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: AsyncSession):
self.session = session
async def avg_for(self, user_id: str) -> float:
rows = (await 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
async 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)
await self.session.commit()
return {"id": row.id, "score": row.score, "comment": row.comment}
class NotificationRepository:
def __init__(self, session: AsyncSession):
self.session = session
async def list_for(self, user_id: str, limit: int = 50) -> list[dict]:
rows = (await 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]
async def unread(self, user_id: str) -> int:
return len((await self.session.scalars(
select(Notification).where(Notification.user_id == user_id, Notification.read.is_(False))
)).all())
async 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)
await self.session.commit()
return {"id": row.id, "title": row.title}
async def mark_read(self, user_id: str) -> int:
rows = (await self.session.scalars(select(Notification).where(Notification.user_id == user_id))).all()
for r in rows:
r.read = True
await self.session.commit()
return len(rows)
# ---------------------------------------------------------------------------
# 机构成员(机构主账号 + 机构内子账号)
# ---------------------------------------------------------------------------
class OrganizationMemberRepository:
def __init__(self, session: AsyncSession):
self.session = session
async def list_for_user(self, user_id: str) -> list[dict]:
rows = await self.session.scalars(select(OrganizationMember).where(OrganizationMember.user_id == user_id))
return [self._to_dict(m) for m in rows]
async def get(self, org_id: str, user_id: str) -> dict | None:
m = await self.session.get(OrganizationMember, (org_id, user_id))
return self._to_dict(m) if m else None
async def list_for_org(self, org_id: str) -> list[dict]:
rows = (await self.session.scalars(
select(OrganizationMember).where(OrganizationMember.org_id == org_id)
)).all()
out = []
for r in rows:
u = await 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
async def is_member(self, org_id: str, user_id: str) -> bool:
return await self.session.get(OrganizationMember, (org_id, user_id)) is not None
async def is_admin(self, org_id: str, user_id: str) -> bool:
row = await self.session.get(OrganizationMember, (org_id, user_id))
return bool(row and row.is_admin)
async def add_member(self, org_id: str, user_id: str, role: str = "member", is_admin: bool = False) -> dict:
row = await 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
await self.session.commit()
return {"org_id": org_id, "user_id": user_id, "role": role, "is_admin": is_admin}
# ---------------------------------------------------------------------------
# 统计
# ---------------------------------------------------------------------------
class StatsRepository:
"""运营端/政务端数据总览(基于现有表聚合)。"""
def __init__(self, session: AsyncSession):
self.session = session
async def overview(self, region_ids: list[str] | None = None) -> dict:
users = (await 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 = (await self.session.scalars(select(Task))).all()
providers = (await self.session.scalars(select(ServiceProvider))).all()
content = (await 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: AsyncSession | None = None):
if session is None:
from sqlalchemy.ext.asyncio import async_sessionmaker
from .db import make_async_engine
url = db_url or config.DATABASE_URL
self._engine = make_async_engine(url)
self._session_factory = async_sessionmaker(
bind=self._engine, class_=AsyncSession, 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)
async def initialize(self) -> None:
"""运行时仅保活:不建表、不灌种子(迁移/种子一律走 alembic + scripts/db/seed.py)。
仅做一次连通性检查,确保库可达。"""
if self._engine is not None:
from sqlalchemy import text
async with self._engine.connect() as conn:
await conn.execute(text("SELECT 1"))
# 建表/种子:由 alembic upgrade + scripts/db/seed.py 在非运行态执行,禁止运行时注入。
async def close(self) -> None:
if self._owns_session:
await self.session.close()