"""开放资金接口的实时插件授权与钱包写锁;事务由调用方提交/回滚。""" import hmac import hashlib import json import re from decimal import Decimal from fastapi import HTTPException from models_sql import RegisteredPlugin, OpenApiOperation, Transaction from plugin_permissions import parse_permissions from tip_transaction import lock_wallet_accounts def validate_live_plugin(row, key, permission): if row is None or not row.enabled: raise HTTPException(status_code=403, detail="插件已停用或不存在") if not isinstance(row.plugin_key, str) or not isinstance(key, str) or not hmac.compare_digest(row.plugin_key.encode('utf-8'), key.encode('utf-8')): raise HTTPException(status_code=403, detail="Plugin Key 已失效") try: permissions = parse_permissions(row.permissions) except Exception: raise HTTPException(status_code=503, detail="插件权限配置暂不可用") if permission not in permissions: raise HTTPException(status_code=403, detail="插件无权限调用该接口") return row def lock_open_wallet(session, account, plugin_id, key, permission): # 首项资金SQL取得SQLite写事务;其它方言先锁钱包再锁插件,所有开放资金路径同序。 wallet = lock_wallet_accounts(session, (account,))[account] row = session.query(RegisteredPlugin).filter(RegisteredPlugin.plugin_id == plugin_id).populate_existing().with_for_update().first() validate_live_plugin(row, key, permission) return wallet, row def positive_integer(value): if isinstance(value, bool) or not isinstance(value, int) or value <= 0: raise HTTPException(status_code=503, detail="插件额度配置暂不可用") return value def quota_total(value): """SQL聚合可返回整数或精确Decimal;拒绝浮点/字符串/非整数与坏统计。""" if isinstance(value, Decimal) and value.is_finite() and value >= 0 and value == value.to_integral_value(): return int(value) if isinstance(value, bool) or not isinstance(value, int) or value < 0: raise ValueError("invalid quota history") return value def open_operation_identity(request_id, account, plugin_id, kind, payload): if request_id is None: return None if not isinstance(request_id, str) or re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:-]{7,127}", request_id) is None: raise HTTPException(status_code=422, detail="OPEN_REQUEST_ID_INVALID") encode = lambda value: json.dumps(value, ensure_ascii=False, separators=(',', ':'), allow_nan=False).encode('utf-8') key = hashlib.sha256(encode([plugin_id, account, kind, request_id])).hexdigest() fingerprint = hashlib.sha256(encode(payload)).hexdigest() return key, fingerprint def read_open_operation(db, identity, account, plugin_id, kind, amount, reference_id): if identity is None: return None key, fingerprint = identity row = db.get(OpenApiOperation, key) if row is None: if db.get(Transaction, f'{kind}_{key}') is not None: raise HTTPException(status_code=503, detail="OPEN_OPERATION_UNAVAILABLE") return None if row.request_hash != fingerprint: raise HTTPException(status_code=409, detail="OPEN_REQUEST_CONFLICT") try: response = json.loads(row.response_payload) transaction = db.get(Transaction, row.transaction_id) if (not isinstance(response, dict) or set(response) != {'success', 'remaining_balance', 'transaction_id'} or response['success'] is not True or type(response['remaining_balance']) is not int or response['transaction_id'] != row.transaction_id or row.transaction_id != f'{kind}_{key}' or transaction is None or transaction.account != account or transaction.tx_type != kind or transaction.source_plugin != plugin_id or transaction.related_account != plugin_id or type(transaction.amount) is not int or transaction.amount != amount or transaction.item_id != reference_id or not isinstance(transaction.prev_hash, str) or transaction.tx_hash != hashlib.sha256( f"{transaction.tx_id}{account}{kind}{amount}{transaction.prev_hash}".encode()).hexdigest()): raise ValueError('invalid open operation') except Exception: raise HTTPException(status_code=503, detail="OPEN_OPERATION_UNAVAILABLE") return response def save_open_operation(db, identity, response): if identity is not None: key, fingerprint = identity db.add(OpenApiOperation(request_key=key, request_hash=fingerprint, transaction_id=response['transaction_id'], response_payload=json.dumps(response, ensure_ascii=False, separators=(',', ':'), allow_nan=False)))