26fa546f67
- 七端口 RBAC、select-identity、JWT、审计 - FastAPI + SQLAlchemy + SQLite,/auth /opc /admin /agents 等路由
1805 lines
66 KiB
Python
1805 lines
66 KiB
Python
# -*- 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()
|