chore(repo): 治理仓库卫生——忽略子服务器/上传目录/运行时文件并清理已入库敏感数据
- .gitignore: 新增 /ATestCServer/ /media/ /GVSite/ /frp/ /.idea/ 及 service.log/ips.txt 等运行时文件 - 取消跟踪: ATestCServer(含 HTTPS 私钥与 db.sqlite3)、media 用户上传、GVSite gitlink、根目录日志与状态文件 - 新增 docs/architecture_debt_governance.md 架构债治理清单
This commit is contained in:
1 parent
de98ed1e9a
commit
d2de6f7f3d
38 files changed
+108
-5192
No files matched your search
+18
-6
@@ -1,11 +1,10 @@
|
||||
/.ENV/
|
||||
/.git/
|
||||
/secrets.ini
|
||||
/secrets.ini.example
|
||||
/app.ini
|
||||
/.vscode/
|
||||
/FRP/
|
||||
/FRP - Copy/
|
||||
/.idea/
|
||||
/frp/
|
||||
/logs/
|
||||
/nginx/
|
||||
/resource/
|
||||
@@ -13,6 +12,20 @@
|
||||
/static/
|
||||
/staticfiles/
|
||||
|
||||
# 子服务器示例(含 HTTPS 私钥与 SQLite 数据,防入库)
|
||||
/ATestCServer/
|
||||
|
||||
# 用户上传目录
|
||||
/media/
|
||||
|
||||
# 运行时日志与状态文件
|
||||
/service.log
|
||||
/service_err.log
|
||||
/ips.txt
|
||||
/iptimes.txt
|
||||
/times.txt
|
||||
/timer.txt
|
||||
|
||||
# GERP 新前端工程(独立 git 仓库)
|
||||
/GERP/
|
||||
# GERP 的编译文件
|
||||
@@ -24,9 +37,8 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# GVSite 前端工程构建产物
|
||||
/GVSite/dist/
|
||||
/GVSite/node_modules/
|
||||
# GVSite 前端工程(独立 git 仓库,防 gitlink 误提交)
|
||||
/GVSite/
|
||||
|
||||
# GVSite 生产部署产物(TempLate/GVSite,由 deploy.ps1 同步)
|
||||
/TempLate/GVSite/
|
||||
|
||||
Whitespace-only changes.
@@ -1,5 +0,0 @@
|
||||
import os
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ATestCServer.settings')
|
||||
application = get_asgi_application()
|
||||
@@ -1,153 +0,0 @@
|
||||
"""ATestCServer — SMANAGE 子服务器
|
||||
通过 MSYNC 协议与 SMANAGE 主服务器(www.gvsds.com)桥接。
|
||||
|
||||
通信拓扑:
|
||||
子 → 父: https://www.gvsds.com/api/msync/* (MSYNC 协议)
|
||||
父 → 子: http://127.0.0.1:25080/api/sub/* (本地回环)"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
|
||||
if not SECRET_KEY:
|
||||
raise ImproperlyConfigured('DJANGO_SECRET_KEY 环境变量未设置,禁止使用默认值启动')
|
||||
|
||||
DEBUG = os.environ.get('DJANGO_DEBUG', 'False').lower() in ('true', '1', 'yes')
|
||||
|
||||
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',')
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
'gvsdsdk',
|
||||
'apps.sub_server',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'ATestCServer.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'ATestCServer.wsgi.application'
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'NAME': os.environ.get('DB_NAME', 'atestc'),
|
||||
'USER': os.environ.get('DB_USER', 'root'),
|
||||
'PASSWORD': os.environ.get('DB_PASSWORD', ''),
|
||||
'HOST': os.environ.get('DB_HOST', '127.0.0.1'),
|
||||
'PORT': os.environ.get('DB_PORT', '3306'),
|
||||
'OPTIONS': {
|
||||
'charset': 'utf8mb4',
|
||||
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
DATABASE_ROUTERS = []
|
||||
|
||||
# gvsdsdk 表由 ATestCServer 自己管理(同源数据库)
|
||||
GVSDSDK_MANAGE_TABLES = True
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
|
||||
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
|
||||
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
|
||||
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
|
||||
]
|
||||
|
||||
LANGUAGE_CODE = 'zh-hans'
|
||||
TIME_ZONE = 'Asia/Shanghai'
|
||||
USE_I18N = True
|
||||
USE_TZ = False
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_RENDERER_CLASSES': [
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
],
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
],
|
||||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
'rest_framework.permissions.IsAuthenticated',
|
||||
],
|
||||
'UNAUTHENTICATED_USER': None,
|
||||
}
|
||||
|
||||
MSYNC_MASTER_URL = os.environ.get('MSYNC_MASTER_URL', 'https://www.gvsds.com')
|
||||
|
||||
MSYNC_SERVICE_NAME = os.environ.get('MSYNC_SERVICE_NAME', 'atest-cserver')
|
||||
MSYNC_SERVICE_DOMAIN = os.environ.get('MSYNC_SERVICE_DOMAIN', 'http://127.0.0.1:25080')
|
||||
MSYNC_SERVICE_SECRET = os.environ.get('MSYNC_SERVICE_SECRET')
|
||||
if not MSYNC_SERVICE_SECRET:
|
||||
raise ImproperlyConfigured('MSYNC_SERVICE_SECRET 环境变量未设置,禁止使用默认值启动')
|
||||
|
||||
MSYNC_SERVICE_SCOPE = [
|
||||
'ogm:read', 'ogm:write',
|
||||
'registry:read',
|
||||
'tx:participate',
|
||||
]
|
||||
|
||||
MSYNC_HEARTBEAT_INTERVAL = int(os.environ.get('MSYNC_HEARTBEAT_INTERVAL', '30'))
|
||||
|
||||
HTTP_PORT = int(os.environ.get('HTTP_PORT', '25080'))
|
||||
HTTPS_PORT = int(os.environ.get('HTTPS_PORT', '25443'))
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'formatters': {
|
||||
'verbose': {
|
||||
'format': '[{levelname}] {asctime} {module} {message}',
|
||||
'style': '{',
|
||||
},
|
||||
},
|
||||
'handlers': {
|
||||
'console': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'verbose',
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'': {
|
||||
'handlers': ['console'],
|
||||
'level': 'INFO',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
"""根 URL 配置"""
|
||||
|
||||
from django.urls import path, include
|
||||
from django.shortcuts import redirect
|
||||
from apps.sub_server.views import HealthView
|
||||
from apps.sub_server import views
|
||||
|
||||
|
||||
def root_redirect(request):
|
||||
return redirect('/api/sub/status/')
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('', root_redirect),
|
||||
path('health/', HealthView.as_view(), name='health'),
|
||||
path('api/sub/', include('apps.sub_server.urls')),
|
||||
# SDK 契约路径:主服务器 Saga 编排引擎按 /api/msync/local/* 回调
|
||||
path('api/msync/local/execute/', views.LocalExecuteView.as_view(), name='msync_local_execute'),
|
||||
path('api/msync/local/compensate/', views.LocalCompensateView.as_view(), name='msync_local_compensate'),
|
||||
path('api/msync/local/reset/', views.LocalResetView.as_view(), name='msync_local_reset'),
|
||||
]
|
||||
@@ -1,5 +0,0 @@
|
||||
import os
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ATestCServer.settings')
|
||||
application = get_wsgi_application()
|
||||
Whitespace-only changes.
Whitespace-only changes.
@@ -1,72 +0,0 @@
|
||||
"""模块:子服务器 AppConfig
|
||||
Django 启动时自动完成 MSYNC Socket.IO 桥接——连接、注册、认证、心跳。"""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 与主服务器约定的 SDK 最低版本——版本不一致时启动警告
|
||||
REQUIRED_GVSDSDK_VERSION = '1.1.0'
|
||||
|
||||
|
||||
class SubServerConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.sub_server'
|
||||
verbose_name = 'SMANAGE 子服务器'
|
||||
verbose_name_plural = 'SMANAGE 子服务器'
|
||||
|
||||
def ready(self):
|
||||
# 1) 校验 gvsdsdk 版本与主服务器一致
|
||||
self._check_sdk_version()
|
||||
# 2) 注册实验本地事务处理器(供主服务器 Saga 编排回调)
|
||||
self._register_local_handlers()
|
||||
# 3) 启动 MSYNC 桥接
|
||||
self._connect_msync()
|
||||
|
||||
def _register_local_handlers(self):
|
||||
try:
|
||||
from .local_handlers import RegisterExperimentHandlers
|
||||
RegisterExperimentHandlers()
|
||||
logger.info('实验本地事务处理器已注册: demo_a/demo_b/demo_fail/demo_flaky')
|
||||
except Exception as e:
|
||||
logger.error(f'实验本地事务处理器注册失败: {e}')
|
||||
|
||||
def _check_sdk_version(self):
|
||||
try:
|
||||
import gvsdsdk
|
||||
info = gvsdsdk.get_version_info()
|
||||
logger.info(f'[SDK] 加载: v{info["Version"]} @ {info["Location"]}')
|
||||
ok, msg = gvsdsdk.check_min_version(REQUIRED_GVSDSDK_VERSION)
|
||||
if ok:
|
||||
logger.info(f'[SDK] {msg}')
|
||||
else:
|
||||
logger.warning(f'[SDK] 版本不符!{msg}。双端 gvsdsdk 版本必须一致!')
|
||||
except ImportError as e:
|
||||
logger.error(f'[SDK] gvsdsdk 未安装: {e}')
|
||||
except Exception as e:
|
||||
logger.error(f'[SDK] 版本检查失败: {e}')
|
||||
|
||||
def _connect_msync(self):
|
||||
try:
|
||||
from msync_client.sio_client import MSyncSocketClient
|
||||
|
||||
msync_client = MSyncSocketClient(
|
||||
master_url=settings.MSYNC_MASTER_URL,
|
||||
service_name=settings.MSYNC_SERVICE_NAME,
|
||||
service_domain=settings.MSYNC_SERVICE_DOMAIN,
|
||||
service_secret=settings.MSYNC_SERVICE_SECRET,
|
||||
scope=settings.MSYNC_SERVICE_SCOPE,
|
||||
heartbeat_interval=settings.MSYNC_HEARTBEAT_INTERVAL,
|
||||
metadata={'DBName': settings.DATABASES['default']['NAME']},
|
||||
)
|
||||
|
||||
setattr(settings, 'MSYNC_CLIENT', msync_client)
|
||||
|
||||
t = threading.Thread(target=msync_client.Connect, daemon=True)
|
||||
t.start()
|
||||
logger.info('MSYNC Socket.IO 桥接线程已启动')
|
||||
except Exception as e:
|
||||
logger.error(f'MSYNC 桥接启动失败: {e}')
|
||||
@@ -1,22 +0,0 @@
|
||||
"""模块:ATestCServer FormService
|
||||
使用 gvsdsdk 通用 FormService 引擎暴露子服务器 ORM 表。"""
|
||||
|
||||
from gvsdsdk.form_service import FormServiceBase
|
||||
from .models import BusinessUser, BusinessOrder, SyncLog
|
||||
|
||||
|
||||
class ATestCFormService(FormServiceBase):
|
||||
TABLE_MAP = {
|
||||
'BusinessUser': BusinessUser,
|
||||
'BusinessOrder': BusinessOrder,
|
||||
'SyncLog': SyncLog,
|
||||
}
|
||||
|
||||
ENTITY_PICKER_MAP = {
|
||||
'BusinessUser': {
|
||||
'api': '/api/sub/form/action',
|
||||
'labelField': 'Username',
|
||||
'valueField': 'UserID',
|
||||
'placeholder': '输入用户名搜索...',
|
||||
},
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
"""实验用本地事务处理器
|
||||
|
||||
随子服务器进程(runserver)启动即注册,供主服务器 Saga 编排回调。
|
||||
msync_experiment 管理命令与 HTTP 服务共用同一进程内处理器。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
from gvsdsdk.msync.local_tx import local_tx
|
||||
|
||||
# demo_flaky 重试实验状态(进程内计数器,可通过 local/reset 端点清零)
|
||||
_flaky_state = {'attempts': 0}
|
||||
|
||||
|
||||
def _ActionA(payload):
|
||||
return {'demo_a_uuid': str(uuid.uuid4()), 'received': payload}
|
||||
|
||||
|
||||
def _ActionB(payload):
|
||||
# 返回收到的完整 payload,供主服务器验证步骤间数据流
|
||||
return {'received_payload': payload}
|
||||
|
||||
|
||||
def _CompensateA(payload, action_result):
|
||||
return {'compensated': True, 'action_result': action_result}
|
||||
|
||||
|
||||
def _ActionFail(payload):
|
||||
raise RuntimeError('确定性失败(实验)')
|
||||
|
||||
|
||||
def _ActionFlaky(payload):
|
||||
_flaky_state['attempts'] += 1
|
||||
if _flaky_state['attempts'] == 1:
|
||||
raise RuntimeError('首次失败(实验:验证自动重试)')
|
||||
return {'retried': True, 'attempts': _flaky_state['attempts']}
|
||||
|
||||
|
||||
def RegisterExperimentHandlers():
|
||||
"""注册全部实验处理器(幂等:已存在则跳过)"""
|
||||
for name, action, compensate in (
|
||||
('demo_a', _ActionA, _CompensateA),
|
||||
('demo_b', _ActionB, None),
|
||||
('demo_fail', _ActionFail, None),
|
||||
('demo_flaky', _ActionFlaky, None),
|
||||
):
|
||||
if not local_tx.HasHandler(name):
|
||||
local_tx.register(name, action=action, compensate=compensate)
|
||||
|
||||
|
||||
def ResetExperimentState():
|
||||
"""清零重试实验状态(供 local/reset 端点调用)"""
|
||||
_flaky_state['attempts'] = 0
|
||||
Whitespace-only changes.
Whitespace-only changes.
@@ -1,202 +0,0 @@
|
||||
"""msync_experiment — MSYNC 主从链路自动化实验与验证
|
||||
|
||||
用法:
|
||||
python manage.py msync_experiment
|
||||
|
||||
前置条件:
|
||||
- 主服务器在线(SIO + HTTP),且本服务已通过 MSYNC 桥接注册成功
|
||||
- 本服务 local 端点可被主服务器访问(MSYNC_SERVICE_DOMAIN)
|
||||
|
||||
自动执行并输出 PASS/FAIL 汇报表:
|
||||
1. 环境状态: SDK 版本 / SIO 连接 / 认证 / 注册
|
||||
2. 正向 Saga: 多步骤成功 → Committed,且验证步骤间数据流
|
||||
3. 失败补偿: 步骤失败 → 逆序补偿 → Compensated
|
||||
4. 自动重试: 步骤首次失败 → 主服务器按 CanRetry 自动重试成功
|
||||
5. 幂等去重: local_tx 相同 Idempotency-Key 命中缓存
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_msync():
|
||||
return getattr(settings, 'MSYNC_CLIENT', None)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'MSYNC 主从链路自动化实验与验证'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--wait-seconds', type=int, default=10,
|
||||
help='等待桥接就绪的秒数(默认 10)',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.results = []
|
||||
wait_seconds = options['wait_seconds']
|
||||
|
||||
self.out('=' * 60)
|
||||
self.out('MSYNC 主从链路自动化实验')
|
||||
self.out('=' * 60)
|
||||
|
||||
msync = _get_msync()
|
||||
if msync is None:
|
||||
self.fail('MSYNC 未初始化', 'settings.MSYNC_CLIENT 不存在,桥接线程未启动')
|
||||
return
|
||||
|
||||
# 1. 等待桥接就绪(连接/注册/认证)
|
||||
self.record('等待桥接就绪', self._wait_ready(msync, wait_seconds))
|
||||
self._report_status(msync)
|
||||
|
||||
# 2. 注册本地实验步骤处理器
|
||||
self.record('注册本地步骤处理器', self._register_handlers(), 'demo_a/demo_b/demo_fail')
|
||||
|
||||
# 3. 正向 Saga + 步骤数据流
|
||||
self.record('正向 Saga(数据流)', self._experiment_forward(msync))
|
||||
|
||||
# 4. 失败补偿
|
||||
self.record('失败补偿', self._experiment_compensate(msync))
|
||||
|
||||
# 5. 自动重试
|
||||
self.record('自动重试', self._experiment_retry(msync))
|
||||
|
||||
# 6. 幂等去重
|
||||
self.record('幂等去重', self._experiment_idempotency())
|
||||
|
||||
self._summary()
|
||||
|
||||
# ─── 工具 ──────────────────────────────────────────────
|
||||
|
||||
def out(self, msg):
|
||||
self.stdout.write(msg)
|
||||
|
||||
def record(self, name, ok, detail=''):
|
||||
self.results.append((name, bool(ok), detail or ''))
|
||||
mark = 'PASS' if ok else 'FAIL'
|
||||
self.stdout.write(f' [{mark}] {name}' + (f' ({detail})' if detail else ''))
|
||||
|
||||
def fail(self, name, detail=''):
|
||||
self.record(name, False, detail)
|
||||
|
||||
def _wait_ready(self, msync, seconds):
|
||||
for _ in range(seconds * 2):
|
||||
if msync.IsConnected and msync.IsAuthenticated and msync.IsRegistered:
|
||||
return True
|
||||
time.sleep(0.5)
|
||||
return msync.IsConnected and msync.IsAuthenticated and msync.IsRegistered
|
||||
|
||||
def _report_status(self, msync):
|
||||
import gvsdsdk
|
||||
info = gvsdsdk.get_version_info()
|
||||
self.out(f'\n SDK 版本 : {info["Version"]} @ {info["Location"]}')
|
||||
self.out(f' SIO 连接 : {msync.IsConnected}')
|
||||
self.out(f' 密钥认证 : {msync.IsAuthenticated}')
|
||||
self.out(f' 服务注册 : {msync.IsRegistered}')
|
||||
self.out(f' 服务名称 : {msync.ServiceName}')
|
||||
self.out(f' 主服务器 : {msync.MasterURL}')
|
||||
|
||||
# ─── 实验步骤 ──────────────────────────────────────────
|
||||
|
||||
def _register_handlers(self):
|
||||
from apps.sub_server.local_handlers import RegisterExperimentHandlers
|
||||
from gvsdsdk.msync.local_tx import local_tx
|
||||
|
||||
RegisterExperimentHandlers()
|
||||
return all(local_tx.HasHandler(n) for n in ('demo_a', 'demo_b'))
|
||||
|
||||
def _create_and_execute(self, msync, name, steps, timeout_seconds=120):
|
||||
created = msync.TxCreate(name, steps, timeout_seconds=timeout_seconds)
|
||||
if not created:
|
||||
return None, 'TxCreate 失败'
|
||||
tx_code = created.get('TransactionCode')
|
||||
detail = msync.TxExecute(tx_code, access_token=msync.AccessToken)
|
||||
if detail is None:
|
||||
return tx_code, 'TxExecute 失败'
|
||||
return tx_code, detail
|
||||
|
||||
def _experiment_forward(self, msync):
|
||||
tx_code, detail = self._create_and_execute(msync, '实验-正向', [
|
||||
{'name': 'demo_a', 'ParticipantService': msync.ServiceName, 'ActionPayload': {'in': 'a'}},
|
||||
{'name': 'demo_b', 'ParticipantService': msync.ServiceName, 'ActionPayload': {'in': 'b'}},
|
||||
])
|
||||
if not detail:
|
||||
return False
|
||||
|
||||
self.out(f'\n 正向事务 {tx_code}: Status={detail.get("Status")} / Code={detail.get("StatusCode")}')
|
||||
committed = detail.get('StatusCode') == 2
|
||||
if not committed:
|
||||
self.out(f' 事务未提交: {detail}')
|
||||
return False
|
||||
|
||||
steps = {s['Index']: s for s in detail.get('Steps', [])}
|
||||
step_a, step_b = steps.get(0), steps.get(1)
|
||||
if not step_a or not step_b:
|
||||
return False
|
||||
|
||||
result_b = (step_b.get('Result') or {}).get('received_payload') or {}
|
||||
flow_ok = bool(result_b.get('demo_a_uuid')) # demo_b 收到 demo_a 的输出 → 数据流生效
|
||||
self.out(f' 数据流验证: demo_b 收到 demo_a_uuid = {result_b.get("demo_a_uuid")}')
|
||||
return committed and flow_ok
|
||||
|
||||
def _experiment_compensate(self, msync):
|
||||
tx_code, detail = self._create_and_execute(msync, '实验-补偿', [
|
||||
{'name': 'demo_a', 'ParticipantService': msync.ServiceName},
|
||||
{'name': 'demo_fail', 'ParticipantService': msync.ServiceName, 'MaxRetries': 0},
|
||||
])
|
||||
if not detail:
|
||||
return False
|
||||
self.out(f'\n 补偿事务 {tx_code}: Status={detail.get("Status")} / Code={detail.get("StatusCode")}')
|
||||
steps = {s['Index']: s for s in detail.get('Steps', [])}
|
||||
return detail.get('StatusCode') == 4 and steps.get(0, {}).get('Status') in ('已补偿', 'Compensated')
|
||||
|
||||
def _experiment_retry(self, msync):
|
||||
self._reset_flaky_state()
|
||||
tx_code, detail = self._create_and_execute(msync, '实验-重试', [
|
||||
{'name': 'demo_flaky', 'ParticipantService': msync.ServiceName, 'MaxRetries': 2},
|
||||
])
|
||||
if not detail:
|
||||
return False
|
||||
self.out(f'\n 重试事务 {tx_code}: Status={detail.get("Status")} / Code={detail.get("StatusCode")}')
|
||||
steps = {s['Index']: s for s in detail.get('Steps', [])}
|
||||
step = steps.get(0, {})
|
||||
ok = detail.get('StatusCode') == 2 and step.get('RetryCount', 0) >= 1
|
||||
self.out(f' 自动重试次数: {step.get("RetryCount")}(要求 >=1)')
|
||||
return ok
|
||||
|
||||
def _reset_flaky_state(self):
|
||||
"""重置 runserver 进程内的 demo_flaky 计数器(通过 local/reset 端点)"""
|
||||
import requests
|
||||
try:
|
||||
reset_url = f'{settings.MSYNC_SERVICE_DOMAIN}/api/msync/local/reset/'
|
||||
requests.post(reset_url, timeout=5)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _experiment_idempotency(self):
|
||||
from gvsdsdk.msync.local_tx import local_tx
|
||||
|
||||
r1 = local_tx.ExecuteStep('no_such_demo', {}, 'EXP:1:0')
|
||||
r2 = local_tx.ExecuteStep('no_such_demo', {}, 'EXP:1:0')
|
||||
r3 = local_tx.ExecuteStep('no_such_demo', {}, 'EXP:1:1')
|
||||
return r1 is r2 and r1 is not r3
|
||||
|
||||
# ─── 汇报 ──────────────────────────────────────────────
|
||||
|
||||
def _summary(self):
|
||||
self.out('\n' + '=' * 60)
|
||||
self.out('实验汇报')
|
||||
self.out('=' * 60)
|
||||
passed = sum(1 for _, ok, _ in self.results if ok)
|
||||
for name, ok, detail in self.results:
|
||||
mark = '✓' if ok else '✗'
|
||||
self.out(f' {mark} {name}' + (f' ({detail})' if detail else ''))
|
||||
self.out(f'\n结果: {passed}/{len(self.results)} 通过')
|
||||
if passed != len(self.results):
|
||||
self.stdout.write(self.style.ERROR('存在未通过的实验环节,请查看上方明细'))
|
||||
raise SystemExit(1)
|
||||
@@ -1,65 +0,0 @@
|
||||
# Generated by Django 6.0.5 on 2026-06-11 01:44
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BusinessUser',
|
||||
fields=[
|
||||
('UserID', models.AutoField(primary_key=True, serialize=False)),
|
||||
('Username', models.CharField(max_length=64, unique=True)),
|
||||
('Nickname', models.CharField(default='', max_length=128)),
|
||||
('OGMMappingID', models.CharField(db_index=True, default='', max_length=128)),
|
||||
('JoinTime', models.DateTimeField(auto_now_add=True)),
|
||||
('UserStatus', models.SmallIntegerField(default=1)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '业务用户',
|
||||
'verbose_name_plural': '业务用户',
|
||||
'db_table': 'sub_business_user',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SyncLog',
|
||||
fields=[
|
||||
('LogID', models.AutoField(primary_key=True, serialize=False)),
|
||||
('EventType', models.CharField(max_length=64)),
|
||||
('EventDetail', models.JSONField(default=dict)),
|
||||
('EventStatus', models.SmallIntegerField(default=1)),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '同步日志',
|
||||
'verbose_name_plural': '同步日志',
|
||||
'db_table': 'sub_sync_log',
|
||||
'ordering': ['-CreateTime'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BusinessOrder',
|
||||
fields=[
|
||||
('OrderID', models.AutoField(primary_key=True, serialize=False)),
|
||||
('OrderCode', models.CharField(db_index=True, max_length=64, unique=True)),
|
||||
('Amount', models.DecimalField(decimal_places=4, max_digits=18)),
|
||||
('OrderStatus', models.SmallIntegerField(default=0)),
|
||||
('OGMMappingID', models.CharField(db_index=True, default='', max_length=128)),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True)),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True)),
|
||||
('User', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='orders', to='sub_server.businessuser')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '业务订单',
|
||||
'verbose_name_plural': '业务订单',
|
||||
'db_table': 'sub_business_order',
|
||||
},
|
||||
),
|
||||
]
|
||||
Whitespace-only changes.
@@ -1,66 +0,0 @@
|
||||
"""模块:子服务器数据模型
|
||||
模拟一个真实子服务器的业务数据结构。"""
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class BusinessUser(models.Model):
|
||||
"""业务用户——模拟子服务器的本地用户数据"""
|
||||
|
||||
UserID = models.AutoField(primary_key=True, verbose_name='用户ID')
|
||||
Username = models.CharField(max_length=64, unique=True, verbose_name='用户名')
|
||||
Nickname = models.CharField(max_length=128, default='', verbose_name='昵称')
|
||||
OGMMappingID = models.CharField(max_length=128, default='', db_index=True, verbose_name='OGM映射ID')
|
||||
JoinTime = models.DateTimeField(auto_now_add=True, verbose_name='加入时间')
|
||||
UserStatus = models.SmallIntegerField(default=1, verbose_name='用户状态')
|
||||
|
||||
class Meta:
|
||||
db_table = 'sub_business_user'
|
||||
verbose_name = '业务用户'
|
||||
verbose_name_plural = '业务用户'
|
||||
|
||||
def __str__(self):
|
||||
return self.Username
|
||||
|
||||
|
||||
class BusinessOrder(models.Model):
|
||||
"""业务订单——模拟子服务器的本地订单数据"""
|
||||
|
||||
OrderID = models.AutoField(primary_key=True, verbose_name='订单ID')
|
||||
OrderCode = models.CharField(max_length=64, unique=True, db_index=True, verbose_name='订单编号')
|
||||
User = models.ForeignKey(
|
||||
BusinessUser, on_delete=models.CASCADE, related_name='orders',
|
||||
verbose_name='用户',
|
||||
)
|
||||
Amount = models.DecimalField(max_digits=18, decimal_places=4, verbose_name='金额')
|
||||
OrderStatus = models.SmallIntegerField(default=0, verbose_name='订单状态')
|
||||
OGMMappingID = models.CharField(max_length=128, default='', db_index=True, verbose_name='OGM映射ID')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'sub_business_order'
|
||||
verbose_name = '业务订单'
|
||||
verbose_name_plural = '业务订单'
|
||||
|
||||
def __str__(self):
|
||||
return self.OrderCode
|
||||
|
||||
|
||||
class SyncLog(models.Model):
|
||||
"""同步日志——记录与主服务器 MSYNC 通信的事件"""
|
||||
|
||||
LogID = models.AutoField(primary_key=True, verbose_name='日志ID')
|
||||
EventType = models.CharField(max_length=64, verbose_name='事件类型')
|
||||
EventDetail = models.JSONField(default=dict, verbose_name='事件详情')
|
||||
EventStatus = models.SmallIntegerField(default=1, verbose_name='事件状态')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'sub_sync_log'
|
||||
verbose_name = '同步日志'
|
||||
verbose_name_plural = '同步日志'
|
||||
ordering = ['-CreateTime']
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.EventType}@{self.CreateTime}'
|
||||
@@ -1,34 +0,0 @@
|
||||
from rest_framework import serializers
|
||||
from .models import BusinessUser, BusinessOrder, SyncLog
|
||||
|
||||
|
||||
class BusinessUserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = BusinessUser
|
||||
fields = ['UserID', 'Username', 'Nickname', 'OGMMappingID', 'JoinTime', 'UserStatus']
|
||||
read_only_fields = ['UserID', 'JoinTime', 'OGMMappingID']
|
||||
|
||||
|
||||
class BusinessOrderSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = BusinessOrder
|
||||
fields = ['OrderID', 'OrderCode', 'Amount', 'OrderStatus', 'OGMMappingID', 'CreateTime', 'UpdateTime', 'User']
|
||||
read_only_fields = ['OrderID', 'CreateTime', 'UpdateTime', 'OGMMappingID']
|
||||
|
||||
|
||||
class OrderCreateSerializer(serializers.Serializer):
|
||||
username = serializers.CharField()
|
||||
amount = serializers.DecimalField(max_digits=18, decimal_places=4)
|
||||
|
||||
|
||||
class OGMRegisterSerializer(serializers.Serializer):
|
||||
object_type = serializers.CharField()
|
||||
local_id = serializers.CharField()
|
||||
metadata = serializers.DictField(required=False, default=dict)
|
||||
|
||||
|
||||
class SyncLogSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = SyncLog
|
||||
fields = ['LogID', 'EventType', 'EventDetail', 'EventStatus', 'CreateTime']
|
||||
read_only_fields = ['LogID', 'CreateTime']
|
||||
@@ -1,81 +0,0 @@
|
||||
"""子服务器服务认证
|
||||
|
||||
主服务器调用子服务器 local 端点时携带 `Authorization: Service <token>`。
|
||||
子服务器无法本地验证令牌(无主库 ServiceToken 表 / MASTER_SECRET),
|
||||
因此通过主服务器 `/api/msync/auth/introspect/` 远程自省校验。
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
from django.conf import settings
|
||||
from rest_framework.authentication import BaseAuthentication
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_AUTH_HEADER = 'Authorization'
|
||||
SERVICE_AUTH_PREFIX = 'Service'
|
||||
INTROSPECT_PATH = '/api/msync/auth/introspect/'
|
||||
|
||||
|
||||
class ServicePrincipal:
|
||||
"""服务主体——标识调用方服务"""
|
||||
|
||||
def __init__(self, service_name):
|
||||
self.service_name = service_name
|
||||
self.service_scope = []
|
||||
|
||||
@property
|
||||
def is_authenticated(self):
|
||||
return True
|
||||
|
||||
def __str__(self):
|
||||
return f'Service:{self.service_name}'
|
||||
|
||||
|
||||
class SubServerServiceAuthentication(BaseAuthentication):
|
||||
"""DRF 认证类——校验主服务器 Service 令牌(远程自省)"""
|
||||
|
||||
def authenticate(self, request):
|
||||
auth_header = request.headers.get(SERVICE_AUTH_HEADER, '')
|
||||
if not auth_header.startswith(f'{SERVICE_AUTH_PREFIX} '):
|
||||
return None
|
||||
token = auth_header[len(SERVICE_AUTH_PREFIX) + 1:].strip()
|
||||
if not token:
|
||||
return None
|
||||
|
||||
payload = self._Introspect(token)
|
||||
if not payload:
|
||||
raise AuthenticationFailed('服务令牌验证失败')
|
||||
|
||||
return (ServicePrincipal(payload.get('sub', 'unknown')), token)
|
||||
|
||||
def authenticate_header(self, request):
|
||||
return 'Service'
|
||||
|
||||
@staticmethod
|
||||
def _Introspect(access_token):
|
||||
"""调用主服务器令牌自省端点"""
|
||||
master_url = str(getattr(settings, 'MSYNC_MASTER_URL', '')).rstrip('/')
|
||||
if not master_url:
|
||||
return None
|
||||
body = json.dumps({'AccessToken': access_token}).encode('utf-8')
|
||||
req = urllib.request.Request(
|
||||
f'{master_url}{INTROSPECT_PATH}',
|
||||
data=body,
|
||||
headers={'Content-Type': 'application/json'},
|
||||
method='POST',
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
data = json.loads(resp.read().decode('utf-8'))
|
||||
except (urllib.error.URLError, urllib.error.HTTPError, ValueError, OSError) as e:
|
||||
logger.error(f'令牌自省失败: {e}')
|
||||
return None
|
||||
|
||||
if not data.get('Valid'):
|
||||
return None
|
||||
return data.get('Payload') or {}
|
||||
@@ -1,217 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ATestCServer — MSYNC 连接状态</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:'Segoe UI',system-ui,-apple-system,sans-serif;background:#0f1923;color:#c0d0e0;min-height:100vh}
|
||||
.header{background:#1a2a3a;border-bottom:2px solid #2a4a6a;padding:20px 30px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px}
|
||||
.header h1{font-size:22px;color:#e0f0ff;font-weight:600}
|
||||
.header .badge{padding:6px 14px;border-radius:20px;font-size:13px;font-weight:600;letter-spacing:.5px}
|
||||
.badge-online{background:#0a3a0a;color:#5ae05a;border:1px solid #1a6a1a}
|
||||
.badge-offline{background:#3a0a0a;color:#e05a5a;border:1px solid #6a1a1a}
|
||||
.main{max-width:980px;margin:30px auto;padding:0 20px;display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:20px}
|
||||
.card{background:#152232;border:1px solid #1e3a50;border-radius:12px;padding:24px;transition:border-color .2s}
|
||||
.card:hover{border-color:#2a5a80}
|
||||
.card h3{font-size:14px;color:#6888a8;text-transform:uppercase;letter-spacing:1.5px;margin-bottom:14px}
|
||||
.card .value{font-size:28px;font-weight:700;color:#e8f8ff;margin-bottom:6px}
|
||||
.card .sub{font-size:12px;color:#5a7a9a}
|
||||
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
||||
.kv{display:flex;flex-direction:column;gap:3px}
|
||||
.kv .key{font-size:11px;color:#5a7a9a;text-transform:uppercase;letter-spacing:.8px}
|
||||
.kv .val{font-size:14px;color:#c8e0f8;word-break:break-all}
|
||||
.status-row{display:flex;align-items:center;gap:8px;margin:6px 0}
|
||||
.dot{width:10px;height:10px;border-radius:50%;display:inline-block}
|
||||
.dot-green{background:#4ade80;box-shadow:0 0 8px #4ade80}
|
||||
.dot-red{background:#ef4444;box-shadow:0 0 8px #ef4444}
|
||||
.dot-yellow{background:#f59e0b;box-shadow:0 0 8px #f59e0b}
|
||||
.dot-gray{background:#555;box-shadow:none}
|
||||
.divider{border:0;border-top:1px solid #1e3a50;margin:16px 0}
|
||||
.refresh{font-size:12px;color:#5a7a9a;margin-top:8px;text-align:right}
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
|
||||
.pulse{animation:pulse 2s infinite}
|
||||
.footer{text-align:center;padding:20px;font-size:12px;color:#3a5a7a}
|
||||
pre{background:#0a1620;border-radius:8px;padding:12px;overflow-x:auto;font-size:12px;color:#80a0c0;max-height:260px;overflow-y:auto}
|
||||
.services-list{list-style:none}
|
||||
.services-list li{padding:8px 10px;border-bottom:1px solid #1a2a3a;font-size:13px;display:flex;justify-content:space-between;color:#90b0d0}
|
||||
.services-list li:last-child{border-bottom:none}
|
||||
.section-title{font-size:15px;color:#a0c0e0;font-weight:600;margin-bottom:10px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div>
|
||||
<h1>ATestCServer</h1>
|
||||
<span style="font-size:12px;color:#5a7a9a">SMANAGE 子服务器 · HTTP :25080 · HTTPS :25443</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:12px">
|
||||
<span id="clock" style="font-size:14px;color:#6888a8"></span>
|
||||
<span id="topBadge" class="badge badge-offline">断开</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<!-- 连接状态卡片 -->
|
||||
<div class="card">
|
||||
<h3>连接状态</h3>
|
||||
<div class="status-row"><span id="dotConn" class="dot dot-gray"></span> <span id="txtConn">加载中...</span></div>
|
||||
<div class="status-row"><span id="dotAuth" class="dot dot-gray"></span> <span id="txtAuth">加载中...</span></div>
|
||||
<div class="status-row"><span id="dotReg" class="dot dot-gray"></span> <span id="txtReg">加载中...</span></div>
|
||||
<div class="status-row"><span id="dotHear" class="dot dot-gray"></span> <span id="txtHear">加载中...</span></div>
|
||||
<div class="refresh pulse" id="refresher">自动刷新中...</div>
|
||||
</div>
|
||||
|
||||
<!-- 服务信息卡片 -->
|
||||
<div class="card">
|
||||
<h3>服务标识</h3>
|
||||
<div class="grid-2">
|
||||
<div class="kv"><span class="key">服务名</span><span class="val" id="svcName">—</span></div>
|
||||
<div class="kv"><span class="key">域名</span><span class="val" id="svcDomain">—</span></div>
|
||||
<div class="kv"><span class="key">主服务器</span><span class="val" id="svcMaster">—</span></div>
|
||||
<div class="kv"><span class="key">Token</span><span class="val" id="svcToken">—</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 对等服务卡片 -->
|
||||
<div class="card" style="grid-column:span 2">
|
||||
<h3>对等服务</h3>
|
||||
<ul class="services-list" id="peerList"><li style="color:#5a7a9a">加载中...</li></ul>
|
||||
</div>
|
||||
|
||||
<!-- 最近事件卡片 -->
|
||||
<div class="card" style="grid-column:span 2">
|
||||
<h3>最近同步事件</h3>
|
||||
<ul class="services-list" id="eventLog"><li style="color:#5a7a9a">暂无事件</li></ul>
|
||||
</div>
|
||||
|
||||
<!-- 原始数据卡片 -->
|
||||
<div class="card" style="grid-column:span 2">
|
||||
<h3>原始响应</h3>
|
||||
<pre id="rawData">{}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
MSYNC Bridge · Heartbeat/30s · ATestCServer © SMANAGE
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const HEALTH_URL = '/health/';
|
||||
const STATUS_URL = '/api/sub/msync/status/';
|
||||
|
||||
function fmtTime(){const n=new Date();return n.toLocaleTimeString('zh-CN',{hour12:false})}
|
||||
|
||||
function updClock(){document.getElementById('clock').textContent=fmtTime()}
|
||||
|
||||
async function fetchStatus(){
|
||||
try{
|
||||
const[h,s]=await Promise.all([
|
||||
fetch(HEALTH_URL).then(r=>r.json()),
|
||||
fetch(STATUS_URL).then(r=>r.json())
|
||||
]);
|
||||
render(h?.data,h?.code===200,s?.data,s?.code===200);
|
||||
}catch(e){
|
||||
showError('请求失败: '+e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function dot(cls){return '<span class="dot '+cls+'"></span>'}
|
||||
|
||||
function showError(msg){
|
||||
document.getElementById('topBadge').className='badge badge-offline';
|
||||
document.getElementById('topBadge').textContent='错误';
|
||||
document.getElementById('dotConn').className='dot dot-red';
|
||||
document.getElementById('txtConn').innerHTML=msg;
|
||||
document.getElementById('refresher').textContent='刷新失败,10s后重试';
|
||||
}
|
||||
|
||||
function render(hdata,hok,sdata,sok){
|
||||
const connected = hdata?.MSync?.Connected || false;
|
||||
const authenticated = hdata?.MSync?.Authenticated || false;
|
||||
const registered = hdata?.MSync?.Registered || false;
|
||||
const running = sdata?.running || connected;
|
||||
const tokenRaw = hdata?.MSync?.AccessToken || sdata?.AccessToken || null;
|
||||
|
||||
// 顶部徽章
|
||||
const badge = document.getElementById('topBadge');
|
||||
if(connected && authenticated){
|
||||
badge.className='badge badge-online'; badge.textContent='已连接';
|
||||
}else if(connected){
|
||||
badge.className='badge badge-online'; badge.textContent='已注册';
|
||||
}else{
|
||||
badge.className='badge badge-offline'; badge.textContent='断开';
|
||||
}
|
||||
|
||||
// 连接状态
|
||||
document.getElementById('dotConn').className='dot '+(connected?'dot-green pulse':'dot-red');
|
||||
document.getElementById('txtConn').textContent='MSYNC 运行: '+(running?'是':'否');
|
||||
|
||||
// 认证状态——只有连接成功后才看 token,否则一律视为未认证
|
||||
const authEffective = connected && authenticated;
|
||||
const tokenEffective = connected && tokenRaw;
|
||||
document.getElementById('dotAuth').className='dot '+(authEffective?'dot-green':tokenEffective?'dot-yellow':'dot-red');
|
||||
document.getElementById('txtAuth').textContent='服务认证: '+(authEffective?'已认证':tokenEffective?'令牌存在(可能过期)':connected?'认证中':'未连接');
|
||||
|
||||
document.getElementById('dotReg').className='dot '+(registered?'dot-green':'dot-red');
|
||||
document.getElementById('txtReg').textContent='服务注册: '+(registered?'已注册':'未注册');
|
||||
|
||||
document.getElementById('dotHear').className='dot '+(connected&®istered?'dot-green pulse':'dot-gray');
|
||||
document.getElementById('txtHear').textContent='心跳线程: '+(connected&®istered?'运行中(间隔30s)':'未启动');
|
||||
|
||||
// 服务信息
|
||||
document.getElementById('svcName').textContent=hdata?.MSync?.ServiceName||sdata?.ServiceName||'—';
|
||||
document.getElementById('svcDomain').textContent=hdata?.MSync?.ServiceDomain||sdata?.ServiceDomain||'—';
|
||||
document.getElementById('svcMaster').textContent=hdata?.MSync?.MasterURL||sdata?.MasterURL||'—';
|
||||
|
||||
// Token 截断展示(防完整泄露)
|
||||
const tokenEl = document.getElementById('svcToken');
|
||||
if(tokenRaw && tokenRaw.length>40){
|
||||
tokenEl.textContent = tokenRaw.slice(0,30)+'…'+tokenRaw.slice(-15);
|
||||
tokenEl.title = tokenRaw;
|
||||
tokenEl.style.cursor = 'help';
|
||||
}else{
|
||||
tokenEl.textContent = tokenRaw||'—';
|
||||
}
|
||||
|
||||
// 对等服务(PascalCase 字段:Name/Domain)
|
||||
const peers=sdata?.PeerServices||[];
|
||||
const pl=document.getElementById('peerList');
|
||||
if(peers.length===0){
|
||||
pl.innerHTML='<li style="color:#5a7a9a">无对等服务(主服务器可能未上线,或尚无其他子服务器)</li>';
|
||||
}else{
|
||||
pl.innerHTML=peers.map(p=>
|
||||
'<li><span>'+(p.Name||p.name||'?')+'</span><span style="color:#6888a8;font-size:11px">'+(p.Domain||p.domain||'')+'</span></li>'
|
||||
).join('');
|
||||
}
|
||||
|
||||
// 事件日志
|
||||
fetch('/api/sub/sync/logs/').then(r=>r.json()).then(d=>{
|
||||
const logs=d?.data||[];
|
||||
const el=document.getElementById('eventLog');
|
||||
if(logs.length===0){
|
||||
el.innerHTML='<li style="color:#5a7a9a">暂无同步事件(需要与主服务器通信后产生)</li>';
|
||||
}else{
|
||||
el.innerHTML=logs.slice(0,10).map(l=>
|
||||
'<li><span>'+l.EventType+'</span><span style="color:#5a7a9a;font-size:11px">'+l.CreateTime+'</span></li>'
|
||||
).join('');
|
||||
}
|
||||
}).catch(()=>{});
|
||||
|
||||
// 原始数据
|
||||
document.getElementById('rawData').textContent=JSON.stringify({
|
||||
health: hdata?.msync||{},
|
||||
status: {running:running,authenticated:authenticated,registered:registered,peers:peers.length}
|
||||
},null,2);
|
||||
|
||||
document.getElementById('refresher').textContent='最后刷新: '+fmtTime();
|
||||
}
|
||||
|
||||
updClock();
|
||||
setInterval(updClock,1000);
|
||||
fetchStatus();
|
||||
setInterval(fetchStatus,10000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,21 +0,0 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'sub_server'
|
||||
|
||||
urlpatterns = [
|
||||
path('health/', views.HealthView.as_view(), name='health'),
|
||||
path('status/', views.StatusPageView, name='status_page'),
|
||||
path('msync/status/', views.MSyncStatusView.as_view(), name='msync_status'),
|
||||
path('users/', views.UserListView.as_view(), name='user_list'),
|
||||
path('users/<int:user_id>/', views.UserDetailView.as_view(), name='user_detail'),
|
||||
path('orders/', views.OrderListView.as_view(), name='order_list'),
|
||||
path('orders/<int:order_id>/', views.OrderDetailView.as_view(), name='order_detail'),
|
||||
path('ogm/register/', views.OGMRegisterView.as_view(), name='ogm_register'),
|
||||
path('ogm/resolve/<str:global_id>/', views.OGMResolveView.as_view(), name='ogm_resolve'),
|
||||
path('remote/call/', views.RemoteCallDemoView.as_view(), name='remote_call'),
|
||||
path('sync/logs/', views.SyncLogView.as_view(), name='sync_logs'),
|
||||
path('local/execute/', views.LocalExecuteView.as_view(), name='local_execute'),
|
||||
path('local/compensate/', views.LocalCompensateView.as_view(), name='local_compensate'),
|
||||
path('local/handlers/', views.LocalHandlersView.as_view(), name='local_handlers'),
|
||||
]
|
||||
@@ -1,420 +0,0 @@
|
||||
"""模块:子服务器 REST API 视图
|
||||
提供业务 CRUD 端点 + MSYNC 状态查询 + 服务间调用演示 + FormService ORM 管理。"""
|
||||
|
||||
import logging
|
||||
from django.conf import settings
|
||||
from django.shortcuts import render
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from .service_auth import SubServerServiceAuthentication
|
||||
from .models import BusinessUser, BusinessOrder, SyncLog
|
||||
from .serializers import (
|
||||
BusinessUserSerializer,
|
||||
BusinessOrderSerializer,
|
||||
OrderCreateSerializer,
|
||||
OGMRegisterSerializer,
|
||||
SyncLogSerializer,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _ok(data=None, msg='success'):
|
||||
return {'code': 200, 'msg': msg, 'data': data}
|
||||
|
||||
|
||||
def _err(msg, code=400):
|
||||
return {'code': code, 'msg': msg, 'data': None}
|
||||
|
||||
|
||||
def _get_msync():
|
||||
"""获取 MSYNC 客户端实例"""
|
||||
return getattr(settings, 'MSYNC_CLIENT', None)
|
||||
|
||||
|
||||
def _log_sync(event_type, detail, status=1):
|
||||
"""记录同步日志"""
|
||||
try:
|
||||
SyncLog.objects.create(
|
||||
EventType=event_type,
|
||||
EventDetail=detail,
|
||||
EventStatus=status,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class HealthView(APIView):
|
||||
authentication_classes = []
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def get(self, request):
|
||||
msync = _get_msync()
|
||||
token = msync.AccessToken if msync else None
|
||||
msync_status = {
|
||||
'Connected': msync.IsConnected if msync else False,
|
||||
'Authenticated': msync.IsAuthenticated if msync else False,
|
||||
'Registered': msync.IsRegistered if msync else False,
|
||||
'ServiceName': msync.ServiceName if msync else None,
|
||||
'ServiceDomain': msync.ServiceDomain if msync else None,
|
||||
'MasterURL': msync.MasterURL if msync else None,
|
||||
'AccessToken': 'exists' if token else 'none',
|
||||
}
|
||||
|
||||
# SDK 版本信息
|
||||
sdk_info = {'Name': 'gvsdsdk'}
|
||||
try:
|
||||
import gvsdsdk
|
||||
sdk_info.update(gvsdsdk.get_version_info())
|
||||
except ImportError as e:
|
||||
sdk_info['Error'] = str(e)
|
||||
|
||||
# SAAS 数据库连接状态
|
||||
saas_db_status = {'Configured': 'saas' in settings.DATABASES}
|
||||
try:
|
||||
from django.db import connections
|
||||
conn = connections['saas']
|
||||
conn.ensure_connection()
|
||||
saas_db_status['Connected'] = True
|
||||
# 尝试统计用户数验证连接可用
|
||||
from gvsdsdk.models import User
|
||||
saas_db_status['UserCount'] = User.objects.using('saas').count()
|
||||
except Exception as e:
|
||||
saas_db_status['Connected'] = False
|
||||
saas_db_status['Error'] = str(e)
|
||||
|
||||
return Response(_ok({
|
||||
'Server': 'ATestCServer',
|
||||
'Status': 'running',
|
||||
'MSync': msync_status,
|
||||
'SDK': sdk_info,
|
||||
'SAASDB': saas_db_status,
|
||||
}))
|
||||
|
||||
|
||||
class MSyncStatusView(APIView):
|
||||
authentication_classes = []
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def get(self, request):
|
||||
msync = _get_msync()
|
||||
if not msync:
|
||||
return Response(_err('MSYNC 未初始化', 500), status=500)
|
||||
|
||||
services = msync.DiscoverAll()
|
||||
token = msync.AccessToken
|
||||
return Response(_ok({
|
||||
'ServiceName': msync.ServiceName,
|
||||
'ServiceDomain': msync.ServiceDomain,
|
||||
'MasterURL': msync.MasterURL,
|
||||
'Authenticated': msync.IsAuthenticated,
|
||||
'Registered': msync.IsRegistered,
|
||||
'Connected': msync.IsConnected,
|
||||
'AccessToken': 'exists' if token else 'none',
|
||||
'PeerServices': services,
|
||||
}))
|
||||
|
||||
|
||||
class UserListView(APIView):
|
||||
|
||||
def get(self, request):
|
||||
users = BusinessUser.objects.filter(UserStatus=1).order_by('-JoinTime')
|
||||
return Response(_ok(BusinessUserSerializer(users, many=True).data))
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('username', '').strip()
|
||||
nickname = request.data.get('nickname', '')
|
||||
|
||||
if not username:
|
||||
return Response(_err('用户名不能为空', 400), status=400)
|
||||
|
||||
user, created = BusinessUser.objects.get_or_create(
|
||||
Username=username,
|
||||
defaults={'Nickname': nickname or username},
|
||||
)
|
||||
|
||||
if created:
|
||||
msync = _get_msync()
|
||||
if msync:
|
||||
try:
|
||||
ogm_result = msync.OGMRegister(
|
||||
object_type='user',
|
||||
local_id=str(user.UserID),
|
||||
service_name=msync.ServiceName,
|
||||
metadata={'username': username, 'nickname': nickname or username},
|
||||
)
|
||||
if ogm_result:
|
||||
user.OGMMappingID = ogm_result.get('ObjectGlobalID', '')
|
||||
user.save(update_fields=['OGMMappingID'])
|
||||
_log_sync('ogm_register', {
|
||||
'object_type': 'user',
|
||||
'local_id': str(user.UserID),
|
||||
'global_id': user.OGMMappingID,
|
||||
})
|
||||
except Exception as e:
|
||||
logger.warning(f'OGM注册失败: {e}')
|
||||
|
||||
status_code = 201 if created else 200
|
||||
return Response(_ok(BusinessUserSerializer(user).data), status=status_code)
|
||||
|
||||
|
||||
class UserDetailView(APIView):
|
||||
|
||||
def get(self, request, user_id):
|
||||
try:
|
||||
user = BusinessUser.objects.get(UserID=user_id)
|
||||
except BusinessUser.DoesNotExist:
|
||||
return Response(_err('用户不存在', 404), status=404)
|
||||
return Response(_ok(BusinessUserSerializer(user).data))
|
||||
|
||||
|
||||
class OrderListView(APIView):
|
||||
|
||||
def get(self, request):
|
||||
orders = BusinessOrder.objects.all().order_by('-CreateTime')[:50]
|
||||
return Response(_ok(BusinessOrderSerializer(orders, many=True).data))
|
||||
|
||||
def post(self, request):
|
||||
ser = OrderCreateSerializer(data=request.data)
|
||||
if not ser.is_valid():
|
||||
return Response(_err(ser.errors, 400), status=400)
|
||||
|
||||
username = ser.validated_data['username']
|
||||
amount = ser.validated_data['amount']
|
||||
|
||||
try:
|
||||
user = BusinessUser.objects.get(Username=username)
|
||||
except BusinessUser.DoesNotExist:
|
||||
return Response(_err('用户不存在', 404), status=404)
|
||||
|
||||
import uuid
|
||||
order = BusinessOrder.objects.create(
|
||||
OrderCode=f'SUB-{uuid.uuid4().hex[:12].upper()}',
|
||||
User=user,
|
||||
Amount=amount,
|
||||
OrderStatus=0,
|
||||
)
|
||||
|
||||
msync = _get_msync()
|
||||
if msync:
|
||||
try:
|
||||
ogm_result = msync.OGMRegister(
|
||||
object_type='order',
|
||||
local_id=str(order.OrderID),
|
||||
service_name=msync.ServiceName,
|
||||
metadata={
|
||||
'order_code': order.OrderCode,
|
||||
'amount': str(amount),
|
||||
'user_id': str(user.UserID),
|
||||
},
|
||||
)
|
||||
if ogm_result:
|
||||
order.OGMMappingID = ogm_result.get('ObjectGlobalID', '')
|
||||
order.save(update_fields=['OGMMappingID'])
|
||||
_log_sync('ogm_register', {
|
||||
'object_type': 'order',
|
||||
'local_id': str(order.OrderID),
|
||||
'global_id': order.OGMMappingID,
|
||||
})
|
||||
except Exception as e:
|
||||
logger.warning(f'订单OGM注册失败: {e}')
|
||||
|
||||
return Response(_ok(BusinessOrderSerializer(order).data), status=201)
|
||||
|
||||
|
||||
class OrderDetailView(APIView):
|
||||
|
||||
def get(self, request, order_id):
|
||||
try:
|
||||
order = BusinessOrder.objects.get(OrderID=order_id)
|
||||
except BusinessOrder.DoesNotExist:
|
||||
return Response(_err('订单不存在', 404), status=404)
|
||||
return Response(_ok(BusinessOrderSerializer(order).data))
|
||||
|
||||
|
||||
class OGMRegisterView(APIView):
|
||||
|
||||
def post(self, request):
|
||||
ser = OGMRegisterSerializer(data=request.data)
|
||||
if not ser.is_valid():
|
||||
return Response(_err(ser.errors, 400), status=400)
|
||||
|
||||
msync = _get_msync()
|
||||
if not msync:
|
||||
return Response(_err('MSYNC 未初始化', 500), status=500)
|
||||
|
||||
result = msync.OGMRegister(
|
||||
object_type=ser.validated_data['object_type'],
|
||||
local_id=ser.validated_data['local_id'],
|
||||
service_name=msync.ServiceName,
|
||||
metadata=ser.validated_data.get('metadata', {}),
|
||||
)
|
||||
|
||||
if result:
|
||||
_log_sync('ogm_register_manual', {
|
||||
'object_type': ser.validated_data['object_type'],
|
||||
'local_id': ser.validated_data['local_id'],
|
||||
'global_id': result.get('ObjectGlobalID', ''),
|
||||
})
|
||||
return Response(_ok(result), status=201)
|
||||
|
||||
return Response(_err('OGM注册失败', 500), status=500)
|
||||
|
||||
|
||||
class OGMResolveView(APIView):
|
||||
|
||||
def get(self, request, global_id):
|
||||
msync = _get_msync()
|
||||
if not msync:
|
||||
return Response(_err('MSYNC 未初始化', 500), status=500)
|
||||
|
||||
result = msync.OGMResolve(global_id)
|
||||
if result:
|
||||
return Response(_ok(result))
|
||||
return Response(_err('映射不存在', 404), status=404)
|
||||
|
||||
|
||||
class RemoteCallDemoView(APIView):
|
||||
|
||||
def get(self, request):
|
||||
msync = _get_msync()
|
||||
if not msync:
|
||||
return Response(_err('MSYNC 未初始化', 500), status=500)
|
||||
|
||||
services = msync.DiscoverAll()
|
||||
results = []
|
||||
|
||||
for svc in services:
|
||||
svc_name = svc.get('ServiceName', '')
|
||||
if svc_name == msync.ServiceName:
|
||||
continue
|
||||
|
||||
target_domain = svc.get('ServiceDomain', '')
|
||||
resp = msync.CallRemote(
|
||||
service_name=svc_name,
|
||||
method='GET',
|
||||
path='/health/',
|
||||
)
|
||||
|
||||
if resp is None:
|
||||
results.append({
|
||||
'service': svc_name,
|
||||
'domain': target_domain,
|
||||
'error': '远端调用失败',
|
||||
})
|
||||
else:
|
||||
# CallRemote 返回 requests.Response(或 dict 时兼容)
|
||||
if isinstance(resp, dict):
|
||||
body = resp
|
||||
else:
|
||||
try:
|
||||
body = resp.json()
|
||||
except ValueError:
|
||||
body = {'code': resp.status_code, 'msg': resp.text[:200], 'data': None}
|
||||
results.append({
|
||||
'service': svc_name,
|
||||
'domain': target_domain,
|
||||
'code': body.get('code'),
|
||||
'msg': body.get('msg'),
|
||||
'data': body.get('data'),
|
||||
})
|
||||
|
||||
return Response(_ok(results))
|
||||
|
||||
|
||||
class SyncLogView(APIView):
|
||||
authentication_classes = []
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def get(self, request):
|
||||
logs = SyncLog.objects.all()[:50]
|
||||
return Response(_ok(SyncLogSerializer(logs, many=True).data))
|
||||
|
||||
|
||||
def StatusPageView(request):
|
||||
"""连接状态仪表盘页面"""
|
||||
return render(request, 'status.html', {
|
||||
'service_name': settings.MSYNC_SERVICE_NAME,
|
||||
'service_domain': settings.MSYNC_SERVICE_DOMAIN,
|
||||
'master_url': settings.MSYNC_MASTER_URL,
|
||||
})
|
||||
|
||||
|
||||
class LocalExecuteView(APIView):
|
||||
"""本地事务执行端点——由主服务器 Saga 编排引擎调用(需主服务器 Service 令牌)"""
|
||||
|
||||
authentication_classes = [SubServerServiceAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
from gvsdsdk.msync.local_tx import local_tx
|
||||
|
||||
step_name = request.data.get('StepName', '')
|
||||
payload = request.data.get('payload') or {}
|
||||
idempotency_key = request.headers.get('Idempotency-Key')
|
||||
|
||||
if not step_name:
|
||||
return Response(_err('StepName 不能为空', 400), status=400)
|
||||
|
||||
result = local_tx.ExecuteStep(
|
||||
name=step_name,
|
||||
payload=payload,
|
||||
idempotency_key=idempotency_key,
|
||||
)
|
||||
|
||||
if result['success']:
|
||||
return Response(_ok(result['result']))
|
||||
return Response(_err(result['error'], 500), status=500)
|
||||
|
||||
|
||||
class LocalCompensateView(APIView):
|
||||
"""本地事务补偿端点——由主服务器 Saga 编排引擎调用(需主服务器 Service 令牌)"""
|
||||
|
||||
authentication_classes = [SubServerServiceAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
from gvsdsdk.msync.local_tx import local_tx
|
||||
|
||||
step_name = request.data.get('StepName', '')
|
||||
payload = request.data.get('payload') or {}
|
||||
action_result = request.data.get('ActionResult') or {}
|
||||
idempotency_key = request.headers.get('Idempotency-Key')
|
||||
|
||||
if not step_name:
|
||||
return Response(_err('StepName 不能为空', 400), status=400)
|
||||
|
||||
result = local_tx.CompensateStep(
|
||||
name=step_name,
|
||||
payload=payload,
|
||||
action_result=action_result,
|
||||
idempotency_key=idempotency_key,
|
||||
)
|
||||
|
||||
if result['success']:
|
||||
return Response(_ok(result['result']))
|
||||
return Response(_err(result['error'], 500), status=500)
|
||||
|
||||
|
||||
class LocalHandlersView(APIView):
|
||||
"""列出已注册的本地事务步骤处理器"""
|
||||
|
||||
authentication_classes = [SubServerServiceAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
from gvsdsdk.msync.local_tx import local_tx
|
||||
return Response(_ok(local_tx.ListHandlers()))
|
||||
|
||||
|
||||
class LocalResetView(APIView):
|
||||
"""重置实验处理器状态(demo_flaky 重试计数清零),仅限本地回环使用"""
|
||||
|
||||
authentication_classes = []
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def post(self, request):
|
||||
from .local_handlers import ResetExperimentState
|
||||
ResetExperimentState()
|
||||
return Response(_ok({'reset': True}))
|
||||
@@ -1,19 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDDzCCAfegAwIBAgIUHtoO8DpIL/M32xypjXvfWJ4DBncwDQYJKoZIhvcNAQEL
|
||||
BQAwFzEVMBMGA1UEAwwMQVRlc3RDU2VydmVyMB4XDTI2MDYxMDE3NDQwMloXDTI3
|
||||
MDYxMDE3NDQwMlowFzEVMBMGA1UEAwwMQVRlc3RDU2VydmVyMIIBIjANBgkqhkiG
|
||||
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAudYM0z+CR/wmG1b26549CJg7OU0xWONDExZM
|
||||
AVLByEoIj7r/GxiS/FFxA3Fu6a9T98ddd+j80Qmimkjlp6kLl6wHASkvcpyiDQiw
|
||||
Glp7xwE7hLVbyHw8Sd8z/cLidPaSft4qCh6GZW5yE/XqnGgmlFXpS+458QMmoQck
|
||||
enROeBnYQvu0DA34pHiPNiICn8S3luc+qQ1XFcDLBE8LERCypdJsO21n6J/94lRG
|
||||
AJXuEyOzbjcIShRUZpy7KRmQaeo7NLjbanvpKajhLITWKd9fdATkut3hcPjsPdtt
|
||||
f0icu3ScJaQKGMWuIcfHFK9rC9ACj67XwsZ7bR79D2viEDK9NQIDAQABo1MwUTAd
|
||||
BgNVHQ4EFgQUCdTC/KUqxAEVGXmuoPP8hh+7ZpowHwYDVR0jBBgwFoAUCdTC/KUq
|
||||
xAEVGXmuoPP8hh+7ZpowDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC
|
||||
AQEAfk1BH3jURvnBby2ViIq1hohnzAQvpkpedmiDmtkQXilbAF2Wwc1Fb386L8Ba
|
||||
eOgEYlglsPIvYoMtW3Jjd+2Xw6ZlRObYK+yBr+SUSJ3/DVOislYStBoq5Z2dTedV
|
||||
N+C8xVRg3fTYsZbk4zzZrQ0p9EdEFVkNAwxh/Q+lipZhv4IW5OXMnYYI2nfq1lJ3
|
||||
sx/wQT6X/t9aHAX3ain2OifquuE+NPs98D6e2Ge0PeLTgJBdvVO1cguuQTfhz71c
|
||||
Jk/nSZCcxBraoIVnDQbsThv2TUaCFt+7H6lqTyoFCd+dzMjoG75Ndhg/QUKLkAyr
|
||||
RQ8FTNs63xPctOKSFGUwA0JY/Q==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,28 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC51gzTP4JH/CYb
|
||||
Vvbrnj0ImDs5TTFY40MTFkwBUsHISgiPuv8bGJL8UXEDcW7pr1P3x1136PzRCaKa
|
||||
SOWnqQuXrAcBKS9ynKINCLAaWnvHATuEtVvIfDxJ3zP9wuJ09pJ+3ioKHoZlbnIT
|
||||
9eqcaCaUVelL7jnxAyahByR6dE54GdhC+7QMDfikeI82IgKfxLeW5z6pDVcVwMsE
|
||||
TwsRELKl0mw7bWfon/3iVEYAle4TI7NuNwhKFFRmnLspGZBp6js0uNtqe+kpqOEs
|
||||
hNYp3190BOS63eFw+Ow9221/SJy7dJwlpAoYxa4hx8cUr2sL0AKPrtfCxnttHv0P
|
||||
a+IQMr01AgMBAAECggEAAxbtxEG7Au2y0FryeeitEp+bbLkoUYDmBHqWDYA+9LcQ
|
||||
x7TSEr2M8zzhHsCzZkHsueLREbIuwqKHaCkuTEINHnRQrm4o03vPGTuCPoovkaAe
|
||||
ljsz60VFOVMvBVSmnxwwEHLEYDmPOfrxyh5MbneWi0b9cC5/gT20q2dq9kWNNZ9T
|
||||
fDe1PAGuPoNujW+NqSs83SXtFpP52JX7T5TjzBtXDB3JYw10j/fdeMfGct/cxvp/
|
||||
MK+jeXuXJbRtsyzVk2Ji9gJQo1RQdKuMoHAMjRglWnbNFeB06jKUXxAC+NlsligD
|
||||
kLWrn/YZSsX7LRSeMzh7SThu69E3B+T0Ht+SY0niIQKBgQDxuQmHU3en3sziRNkf
|
||||
jAZ+q6A1AxJ07tGfsCWDXNPM4Y0NzfkUw+SkKrn0WE9LXJzwCdwCTSCD4qhJw+/Z
|
||||
JmV3VegomE52uBMp5JlegNPH5wT6tfi+BpMN8RsQjsT+NXpbfI/XyIAOkkiVEReH
|
||||
pBHY/pAs+/JZcWjOG7VFzcpJkQKBgQDEz/rnH6yl298pkhhMiSSZvWSaLTCYsr36
|
||||
ZWZlfMmRiZYxYJemmiH+RQD64GF4l2qhd+msnx6Jv3WM0Et1kNPzQxGVDq6ZEPZE
|
||||
khve9dBZMvGoBnWZVcMlMRXg+mocOrKWA61jL0tV+JlPkpi9GzDRuWkBUpBkOxM2
|
||||
7SToYvTHZQKBgDr+uTehBHQhKYoT2ZYce71Qr/IgRNHrM5gwyocMEdlQNzfmTGNo
|
||||
CBaVIvtizj+LY/Ylwd+B6Wym1gHi41fxShRdECNQQN4ywOKZm2D4Ojfwx0NVRFdE
|
||||
jYF6lettnOCaNrm7ZfP5SF/GDhbrq/176qs2PgPsndJ8nmKnWo+l0TgxAoGBAIac
|
||||
3jahAnw7A8syIq6OitsgoCOXzwpSDwCpxDyb+l4x5emF8JPCDogUeFQfjmtl3Uft
|
||||
dYVdHA0JSAR1H3edBGr5L5hs/1ywLlC+QpPgO44W+xR6voqPDzdr+FP10wfV2foj
|
||||
L7+MRJHbxDiZwyBs7Zo0n66I4zu68Cx8TOcpb9TFAoGBAJIlo7Ajm5Wc8vomkQOD
|
||||
CCqxKbpPKUnoqvhKGs2UsP4j4LhuAWScOMkI429Jm7e035IdV/7Mnc40YsoOfYKi
|
||||
ePga4nL9ce9Ie/ay8iOuT8Dp/QpXMtJO1CUwMFSdOOLx/CWMB92XV+6YEAc9f6oR
|
||||
B3KS6OnbIZhWoEceLLiwZAlb
|
||||
-----END PRIVATE KEY-----
|
||||
Binary file not shown.
@@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
def main():
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'ATestCServer.settings'
|
||||
os.environ['DB_NAME'] = 'atestc'
|
||||
# 安全:禁止硬编码默认密钥/口令,缺失时显式报错(settings.py 亦有
|
||||
# ImproperlyConfigured 兜底)。MSYNC_SERVICE_SECRET 与 DB_PASSWORD
|
||||
# 由部署环境(环境变量)提供。
|
||||
required_envs = ['DJANGO_SECRET_KEY', 'MSYNC_SERVICE_SECRET', 'DB_PASSWORD']
|
||||
missing = [name for name in required_envs if not os.environ.get(name)]
|
||||
if missing:
|
||||
raise SystemExit(
|
||||
'缺少必需的环境变量: ' + ', '.join(missing)
|
||||
+ '(禁止使用默认值,请通过环境变量注入)'
|
||||
)
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Make sure it's installed and available."
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,9 +0,0 @@
|
||||
"""MSYNC 客户端
|
||||
|
||||
模块入口,导出:
|
||||
- MSyncSocketClient: Socket.IO 客户端(实时通道)
|
||||
"""
|
||||
|
||||
from .sio_client import MSyncSocketClient
|
||||
|
||||
__all__ = ['MSyncSocketClient']
|
||||
@@ -1,513 +0,0 @@
|
||||
"""模块:MSYNC Socket.IO 客户端
|
||||
|
||||
替代 HTTP REST,通过 Socket.IO 与 SMANAGE 主服务器实时通信。
|
||||
所有方法名、事件字段全部使用 PascalCase。
|
||||
|
||||
优势:
|
||||
- 连接复用,无需每次请求建立 TCP/TLS
|
||||
- 内置心跳保活,替代应用层心跳线程
|
||||
- 服务端可主动推送事件(服务上下线通知)
|
||||
- 断线自动重连
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
import socketio
|
||||
|
||||
from gvsdsdk.msync.auth import GenerateChallenge, BuildAuthPayload, SignPayload
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MSyncSocketClient:
|
||||
"""MSYNC Socket.IO 客户端——子服务器接入 SMANAGE 的实时通道"""
|
||||
|
||||
def __init__(self, master_url, service_name, service_domain,
|
||||
service_secret, scope=None, heartbeat_interval=30,
|
||||
service_type=0, metadata=None):
|
||||
self.MasterURL = master_url.rstrip('/')
|
||||
self.ServiceName = service_name
|
||||
self.ServiceDomain = service_domain
|
||||
self.ServiceSecret = service_secret
|
||||
self.Scope = scope or ['ogm:read', 'ogm:write', 'registry:read']
|
||||
self.HeartbeatInterval = heartbeat_interval
|
||||
self.ServiceType = service_type
|
||||
self.Metadata = metadata or {}
|
||||
|
||||
self._sio = socketio.Client(
|
||||
reconnection=True,
|
||||
reconnection_attempts=0,
|
||||
reconnection_delay=2,
|
||||
reconnection_delay_max=30,
|
||||
logger=True,
|
||||
engineio_logger=True,
|
||||
)
|
||||
|
||||
self._token_data = None
|
||||
self._registered = False
|
||||
self._running = False
|
||||
self._heartbeat_thread = None
|
||||
self._initial_connected = False
|
||||
self._reconnect_lock = threading.Lock()
|
||||
|
||||
self._SetupEvents()
|
||||
|
||||
def _SetupEvents(self):
|
||||
@self._sio.on('connect', namespace='/msync')
|
||||
def on_connect():
|
||||
logger.info(f'[SIO] ← connect ns=/msync')
|
||||
if self._initial_connected:
|
||||
logger.info(f'[SIO] 重连 detected, 重新注册/认证')
|
||||
t = threading.Thread(target=self._DoReconnect, daemon=True)
|
||||
t.start()
|
||||
|
||||
@self._sio.on('disconnect', namespace='/msync')
|
||||
def on_disconnect():
|
||||
logger.warning(f'[SIO] ← disconnect ns=/msync')
|
||||
self._registered = False
|
||||
self._token_data = None
|
||||
logger.info(f'[SIO] 状态已清理: registered=False, token_data=None')
|
||||
|
||||
@self._sio.on('msync:service:online', namespace='/msync')
|
||||
def on_service_online(data):
|
||||
logger.info(f'[SIO] ← msync:service:online data={data}')
|
||||
|
||||
@self._sio.on('msync:service:offline', namespace='/msync')
|
||||
def on_service_offline(data):
|
||||
logger.info(f'[SIO] ← msync:service:offline data={data}')
|
||||
|
||||
@self._sio.on('msync:force:deregister', namespace='/msync')
|
||||
def on_force_deregister(data):
|
||||
logger.warning(f'[SIO] ← msync:force:deregister data={data}')
|
||||
self._registered = False
|
||||
self._token_data = None
|
||||
# 延迟后自动重新注册
|
||||
t = threading.Thread(target=self._DoReconnect, daemon=True)
|
||||
t.start()
|
||||
|
||||
@self._sio.on('*')
|
||||
def catch_all(event, data):
|
||||
logger.info(f'[SIO] ← unhandled event="{event}" data={data}')
|
||||
|
||||
# ---------- 连接管理 ----------
|
||||
|
||||
def Connect(self):
|
||||
self._running = True
|
||||
url = self.MasterURL
|
||||
try:
|
||||
logger.info(f'[SIO] → connect url={url} ns=/msync')
|
||||
self._sio.connect(url, namespaces=['/msync'], wait=True, wait_timeout=15)
|
||||
except Exception as e:
|
||||
logger.warning(f'Socket.IO 连接失败: {e},启动兜底重连线程')
|
||||
self._StartBootstrap()
|
||||
return
|
||||
|
||||
self._OnReady()
|
||||
|
||||
def _StartBootstrap(self):
|
||||
"""初次连接失败的兜底:循环重连,成功后走完整注册/认证流程
|
||||
|
||||
python-socketio 的 reconnection 仅在"已连接后断开"时生效,
|
||||
初次 connect 失败不会自动重连,需自行兜底。
|
||||
"""
|
||||
|
||||
def bootstrap():
|
||||
while self._running and not self._sio.connected:
|
||||
try:
|
||||
logger.info(f'[SIO] 兜底重连尝试: {self.MasterURL}')
|
||||
self._sio.connect(
|
||||
self.MasterURL,
|
||||
namespaces=['/msync'],
|
||||
wait=True,
|
||||
wait_timeout=10,
|
||||
)
|
||||
break
|
||||
except Exception as e:
|
||||
logger.warning(f'[SIO] 兜底重连失败: {e}')
|
||||
time.sleep(3)
|
||||
if self._sio.connected:
|
||||
self._OnReady()
|
||||
|
||||
threading.Thread(target=bootstrap, daemon=True, name='msync-bootstrap').start()
|
||||
|
||||
def _OnReady(self):
|
||||
"""连接建立后的统一就绪流程:注册/认证/心跳/ORM 中转"""
|
||||
logger.info(f'[SIO] Socket 已连接, 开始注册/认证...')
|
||||
for attempt in range(1, 4):
|
||||
reg_ok = self._DoRegister()
|
||||
auth_ok = self._DoAuth()
|
||||
if reg_ok and auth_ok:
|
||||
break
|
||||
logger.warning(f'[SIO] 首次注册/认证失败 第 {attempt}/3 次 (reg={reg_ok}, auth={auth_ok})')
|
||||
if attempt < 3:
|
||||
time.sleep(2)
|
||||
self._DoHeartbeat()
|
||||
self._initial_connected = True
|
||||
|
||||
self._StartHeartbeat()
|
||||
|
||||
# 注册 ORM 中转处理器
|
||||
try:
|
||||
from gvsdsdk.msync.orm_handler import ORMRelayHandler
|
||||
self._orm_handler = ORMRelayHandler()
|
||||
self._orm_handler.Register(self)
|
||||
except Exception as e:
|
||||
logger.warning(f'[ORM] ORM 中转处理器注册失败: {e}')
|
||||
|
||||
logger.info(f'MSYNC Socket.IO 客户端已启动: {self.ServiceName}')
|
||||
|
||||
def Disconnect(self):
|
||||
self._running = False
|
||||
try:
|
||||
self._DoDeregister()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self._sio.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
logger.info(f'MSYNC Socket.IO 客户端已断开: {self.ServiceName}')
|
||||
|
||||
def _DoReconnect(self, max_retries=3):
|
||||
"""重连后重新注册/认证,失败时自动重试(内部持有互斥锁)"""
|
||||
if not self._reconnect_lock.acquire(blocking=False):
|
||||
logger.warning('[SIO] 重连已在进行中,跳过本次重连')
|
||||
return
|
||||
try:
|
||||
for attempt in range(1, max_retries + 1):
|
||||
logger.info(f'[SIO] 重连注册/认证 第 {attempt}/{max_retries} 次')
|
||||
reg_ok = self._DoRegister()
|
||||
auth_ok = self._DoAuth()
|
||||
if reg_ok and auth_ok:
|
||||
logger.info(f'[SIO] 重连注册/认证成功')
|
||||
return
|
||||
logger.warning(f'[SIO] 重连注册/认证失败 (reg={reg_ok}, auth={auth_ok}),{2}s 后重试')
|
||||
if attempt < max_retries:
|
||||
time.sleep(2)
|
||||
logger.error(f'[SIO] 重连注册/认证 {max_retries} 次均失败,等待 Socket.IO 下次重连触发')
|
||||
finally:
|
||||
self._reconnect_lock.release()
|
||||
|
||||
def _StartHeartbeat(self):
|
||||
if self._heartbeat_thread and self._heartbeat_thread.is_alive():
|
||||
return
|
||||
self._heartbeat_thread = threading.Thread(
|
||||
target=self._HeartbeatLoop,
|
||||
daemon=True,
|
||||
name=f'msync-heartbeat-{self.ServiceName}',
|
||||
)
|
||||
self._heartbeat_thread.start()
|
||||
logger.info(f'心跳线程已启动, 间隔={self.HeartbeatInterval}s')
|
||||
|
||||
def _HeartbeatLoop(self):
|
||||
consecutive_failures = 0
|
||||
while self._running:
|
||||
time.sleep(self.HeartbeatInterval)
|
||||
try:
|
||||
if not self._sio.connected:
|
||||
continue
|
||||
# 检查令牌是否即将过期,主动续期
|
||||
if not self.IsAuthenticated:
|
||||
logger.info('[SIO] 令牌已过期或即将过期,主动续期')
|
||||
try:
|
||||
self.EnsureAuthenticated()
|
||||
except Exception as e:
|
||||
logger.warning(f'[SIO] 令牌续期失败: {e},触发重连')
|
||||
consecutive_failures += 1
|
||||
if consecutive_failures >= 3:
|
||||
logger.warning('[SIO] 连续失败次数过多,强制重连')
|
||||
self._ForceReconnect()
|
||||
consecutive_failures = 0
|
||||
continue
|
||||
# 发送心跳
|
||||
result = self._Call('msync:heartbeat', {})
|
||||
if result and result.get('code') not in (200, 0):
|
||||
code = result.get('code')
|
||||
msg = result.get('msg', '')
|
||||
logger.warning(f'[SIO] 心跳异常: code={code} msg={msg}')
|
||||
# 404 = 服务不存在/已禁用,需要立即重新注册
|
||||
if code == 404:
|
||||
logger.warning('[SIO] 服务已被注销,立即重新注册')
|
||||
try:
|
||||
reg_ok = self._DoRegister()
|
||||
auth_ok = self._DoAuth()
|
||||
if reg_ok and auth_ok:
|
||||
logger.info('[SIO] 重新注册/认证成功')
|
||||
consecutive_failures = 0
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.error(f'[SIO] 重新注册失败: {e}')
|
||||
# 注册也失败,强制重连
|
||||
self._ForceReconnect()
|
||||
consecutive_failures = 0
|
||||
continue
|
||||
consecutive_failures += 1
|
||||
if consecutive_failures >= 3:
|
||||
logger.warning('[SIO] 心跳连续失败次数过多,强制重连')
|
||||
self._ForceReconnect()
|
||||
consecutive_failures = 0
|
||||
elif result:
|
||||
consecutive_failures = 0
|
||||
except Exception as e:
|
||||
logger.warning(f'[SIO] 心跳异常: {e}')
|
||||
consecutive_failures += 1
|
||||
if consecutive_failures >= 3:
|
||||
self._ForceReconnect()
|
||||
consecutive_failures = 0
|
||||
|
||||
def _Call(self, event, data, timeout=15):
|
||||
if not self._sio.connected:
|
||||
logger.warning(f'[SIO] ✗ 未连接,无法发送 event={event}')
|
||||
return None
|
||||
try:
|
||||
logger.info(f'[SIO] → event="{event}" ns=/msync data_keys={list(data.keys()) if isinstance(data, dict) else type(data).__name__}')
|
||||
result = self._sio.call(event, data, namespace='/msync', timeout=timeout)
|
||||
logger.info(f'[SIO] ← ack event="{event}" result={result}')
|
||||
return result
|
||||
except socketio.exceptions.TimeoutError:
|
||||
logger.error(f'[SIO] ✗ timeout event="{event}" ({timeout}s)')
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f'[SIO] ✗ call failed event="{event}": {type(e).__name__}: {e}')
|
||||
return None
|
||||
|
||||
# ---------- 注册与认证 ----------
|
||||
|
||||
def _DoRegister(self):
|
||||
payload = {
|
||||
'ServiceName': self.ServiceName,
|
||||
'ServiceDomain': self.ServiceDomain,
|
||||
'ServiceSecretHash': hashlib.sha256(self.ServiceSecret.encode()).hexdigest(),
|
||||
'ServiceSecret': self.ServiceSecret,
|
||||
'ServiceScope': self.Scope,
|
||||
'ServiceType': self.ServiceType,
|
||||
'ServiceMetadata': self.Metadata,
|
||||
}
|
||||
result = self._Call('msync:register', payload)
|
||||
if result and result.get('code') == 200:
|
||||
self._registered = True
|
||||
logger.info(f'服务注册成功: {self.ServiceName}')
|
||||
return True
|
||||
if result and result.get('code') == 409:
|
||||
logger.warning(f'服务已存在(409冲突),可能已用不同密钥注册: {self.ServiceName},尝试认证')
|
||||
return True
|
||||
logger.warning(f'服务注册失败: result={result}')
|
||||
return False
|
||||
|
||||
def _DoAuth(self):
|
||||
challenge = GenerateChallenge()
|
||||
payload = BuildAuthPayload(self.ServiceName, challenge)
|
||||
signature = SignPayload(payload, self.ServiceSecret)
|
||||
|
||||
# 调试:打印签名详情
|
||||
import json
|
||||
canonical = json.dumps(payload, sort_keys=True, separators=(',', ':'))
|
||||
logger.info(f'[AUTH] 客户端签名: service={self.ServiceName}, canonical={canonical}, sig={signature}')
|
||||
|
||||
result = self._Call('msync:auth', {
|
||||
'ServiceName': self.ServiceName,
|
||||
'Payload': payload,
|
||||
'Signature': signature,
|
||||
})
|
||||
if result and result.get('code') == 200 and result.get('data'):
|
||||
token_data = result['data']
|
||||
self._token_data = {
|
||||
'AccessToken': token_data.get('AccessToken'),
|
||||
'RefreshToken': token_data.get('RefreshToken'),
|
||||
'Scope': token_data.get('Scope', []),
|
||||
'ExpiresAt': token_data.get('ExpiresAt'),
|
||||
}
|
||||
logger.info(f'服务认证成功: {self.ServiceName}')
|
||||
return True
|
||||
logger.warning(f'服务认证失败: result={result}')
|
||||
return False
|
||||
|
||||
def _DoDeregister(self):
|
||||
self._Call('msync:deregister', {'ServiceName': self.ServiceName})
|
||||
self._registered = False
|
||||
|
||||
def _DoHeartbeat(self):
|
||||
result = self._Call('msync:heartbeat', {})
|
||||
if result and result.get('code') == 200:
|
||||
logger.info(f'初始心跳成功: {self.ServiceName}')
|
||||
else:
|
||||
logger.warning(f'初始心跳失败: {result}')
|
||||
|
||||
# ---------- 状态 ----------
|
||||
|
||||
@property
|
||||
def AccessToken(self):
|
||||
return self._token_data.get('AccessToken') if self._token_data else None
|
||||
|
||||
@property
|
||||
def RefreshToken(self):
|
||||
return self._token_data.get('RefreshToken') if self._token_data else None
|
||||
|
||||
@property
|
||||
def IsAuthenticated(self):
|
||||
if not self._token_data:
|
||||
return False
|
||||
expires = self._token_data.get('ExpiresAt', 0)
|
||||
if expires and time.time() > expires - 60:
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def IsRegistered(self):
|
||||
return self._registered
|
||||
|
||||
@property
|
||||
def IsConnected(self):
|
||||
return self._sio.connected
|
||||
|
||||
def EnsureAuthenticated(self):
|
||||
"""确保认证有效,过期则续期,续期失败则断开重连"""
|
||||
if not self.IsAuthenticated:
|
||||
if self._token_data and self._token_data.get('RefreshToken'):
|
||||
result = self._RefreshToken()
|
||||
if result:
|
||||
return result
|
||||
logger.warning('[SIO] 令牌刷新失败,尝试完整认证')
|
||||
auth_ok = self._DoAuth()
|
||||
if auth_ok:
|
||||
return self._token_data
|
||||
# 认证彻底失败,断开连接触发 Socket.IO 自动重连
|
||||
logger.error('[SIO] 完整认证也失败,断开连接触发自动重连')
|
||||
self._ForceReconnect()
|
||||
return None
|
||||
return self._token_data
|
||||
|
||||
def _RefreshToken(self):
|
||||
result = self._Call('msync:auth:refresh', {
|
||||
'RefreshToken': self._token_data.get('RefreshToken', ''),
|
||||
})
|
||||
if result and result.get('code') == 200 and result.get('data'):
|
||||
token_data = result['data']
|
||||
self._token_data = {
|
||||
'AccessToken': token_data.get('AccessToken'),
|
||||
'RefreshToken': token_data.get('RefreshToken'),
|
||||
'Scope': token_data.get('Scope', []),
|
||||
'ExpiresAt': token_data.get('ExpiresAt'),
|
||||
}
|
||||
logger.info(f'令牌刷新成功: {self.ServiceName}')
|
||||
return self._token_data
|
||||
logger.info('令牌刷新失败,回退到完整认证')
|
||||
return None
|
||||
|
||||
def _ForceReconnect(self):
|
||||
"""强制断开连接,让 Socket.IO 自动重连"""
|
||||
try:
|
||||
self._registered = False
|
||||
self._token_data = None
|
||||
self._sio.disconnect()
|
||||
except Exception as e:
|
||||
logger.warning(f'[SIO] 强制断开失败: {e}')
|
||||
|
||||
def _Headers(self):
|
||||
token = self.AccessToken
|
||||
if not token:
|
||||
self.EnsureAuthenticated()
|
||||
token = self.AccessToken
|
||||
return {'Authorization': f'Bearer {token}'} if token else {}
|
||||
|
||||
# ---------- 服务发现 ----------
|
||||
|
||||
def DiscoverAll(self, active_only=True):
|
||||
result = self._Call('msync:discover', {'ActiveOnly': active_only})
|
||||
if result and result.get('code') == 200:
|
||||
return result.get('data', [])
|
||||
return []
|
||||
|
||||
def Discover(self, service_name):
|
||||
result = self._Call('msync:discover:one', {'ServiceName': service_name})
|
||||
if result and result.get('code') == 200:
|
||||
return result.get('data')
|
||||
return None
|
||||
|
||||
# ---------- OGM ----------
|
||||
|
||||
def OGMRegister(self, object_type, local_id, service_name,
|
||||
global_id=None, metadata=None):
|
||||
body = {
|
||||
'ObjectType': object_type,
|
||||
'ObjectLocalID': str(local_id),
|
||||
'ServiceName': service_name,
|
||||
}
|
||||
if global_id:
|
||||
body['ObjectGlobalID'] = global_id
|
||||
if metadata:
|
||||
body['Metadata'] = metadata
|
||||
result = self._Call('msync:ogm:register', body)
|
||||
if result and result.get('code') == 200:
|
||||
return result.get('data')
|
||||
return None
|
||||
|
||||
def OGMResolve(self, global_id):
|
||||
result = self._Call('msync:ogm:resolve', {'ObjectGlobalID': global_id})
|
||||
if result and result.get('code') == 200:
|
||||
return result.get('data')
|
||||
return None
|
||||
|
||||
def OGMQuery(self, object_type=None, service_name=None, local_id=None):
|
||||
body = {}
|
||||
if object_type:
|
||||
body['ObjectType'] = object_type
|
||||
if service_name:
|
||||
body['ServiceName'] = service_name
|
||||
if local_id:
|
||||
body['ObjectLocalID'] = local_id
|
||||
result = self._Call('msync:ogm:query', body)
|
||||
if result and result.get('code') == 200:
|
||||
return result.get('data')
|
||||
return None
|
||||
|
||||
def OGMDeregister(self, global_id):
|
||||
result = self._Call('msync:ogm:deregister', {'ObjectGlobalID': global_id})
|
||||
return result is not None and result.get('code') == 200
|
||||
|
||||
# ---------- 事务 ----------
|
||||
|
||||
def TxCreate(self, name, steps, **kwargs):
|
||||
body = {'TransactionName': name, 'Steps': steps}
|
||||
for key in ('TransactionDescription', 'TransactionType', 'InitiatorServiceName',
|
||||
'ReferenceOGMUUID', 'Payload', 'TimeoutSeconds'):
|
||||
kwarg_key = key[0].lower() + key[1:]
|
||||
if kwarg_key in kwargs:
|
||||
body[key] = kwargs.pop(kwarg_key)
|
||||
body.update(kwargs)
|
||||
result = self._Call('msync:tx:create', body, timeout=30)
|
||||
if result and result.get('code') == 200:
|
||||
return result.get('data')
|
||||
return None
|
||||
|
||||
def TxExecute(self, transaction_code, access_token=None):
|
||||
result = self._Call('msync:tx:execute', {
|
||||
'TransactionCode': transaction_code,
|
||||
'AccessToken': access_token,
|
||||
}, timeout=60)
|
||||
if result and result.get('code') == 200:
|
||||
return result.get('data')
|
||||
return None
|
||||
|
||||
def TxDetail(self, transaction_code):
|
||||
result = self._Call('msync:tx:detail', {'TransactionCode': transaction_code})
|
||||
if result and result.get('code') == 200:
|
||||
return result.get('data')
|
||||
return None
|
||||
|
||||
# ---------- 远程调用 ----------
|
||||
|
||||
def CallRemote(self, service_name, method, path, data=None, json=None, **kwargs):
|
||||
import requests
|
||||
service_info = self.Discover(service_name)
|
||||
if not service_info:
|
||||
logger.error(f'目标服务未发现: {service_name}')
|
||||
return None
|
||||
domain = service_info.get('ServiceDomain', '')
|
||||
url = f'{domain.rstrip("/")}{path}'
|
||||
kwargs.setdefault('headers', {}).update(self._Headers())
|
||||
kwargs.setdefault('timeout', 30)
|
||||
method_func = getattr(requests, method.lower(), requests.get)
|
||||
return method_func(url, data=data, json=json, **kwargs)
|
||||
@@ -1,23 +0,0 @@
|
||||
# ATestCServer 子服务器依赖
|
||||
# 安装顺序:先装 SDK,再装项目依赖
|
||||
# 1) pip install -e ../gvsdsdk
|
||||
# 2) pip install -r requirements.txt
|
||||
|
||||
# === GVS-DSDK(双端共享,必须装,**版本与主服务器严格一致**) ===
|
||||
gvsdsdk==1.0.0
|
||||
|
||||
# === Django 核心 ===
|
||||
django>=4.2,<5.0
|
||||
djangorestframework>=3.14,<4.0
|
||||
|
||||
# === 数据库(连接 SAAS 主库 MySQL) ===
|
||||
mysqlclient>=2.2
|
||||
|
||||
# === MSYNC Socket.IO 客户端(子服务器需要) ===
|
||||
python-socketio[client]>=5.0,<6.0
|
||||
websocket-client>=1.5
|
||||
requests>=2.28
|
||||
|
||||
# === 加密(与主服务器同版本) ===
|
||||
pyOpenSSL>=23.0
|
||||
pycryptodome>=3.18
|
||||
@@ -1,35 +0,0 @@
|
||||
# ATestCServer 启动脚本
|
||||
# HTTP 端口 25080,HTTPS 端口 25443
|
||||
#
|
||||
# 用法:
|
||||
# .\start.ps1 # 普通启动
|
||||
# .\start.ps1 -Migrate # 带数据库迁移
|
||||
|
||||
param(
|
||||
[switch]$Migrate = $false
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
Set-Location $ScriptDir
|
||||
|
||||
Write-Host "============================" -ForegroundColor Cyan
|
||||
Write-Host " ATestCServer 子服务器启动" -ForegroundColor Cyan
|
||||
Write-Host " HTTP : http://127.0.0.1:25080" -ForegroundColor Green
|
||||
Write-Host " HTTPS : https://127.0.0.1:25443" -ForegroundColor Green
|
||||
Write-Host " MSYNC → https://www.gvsds.com" -ForegroundColor Yellow
|
||||
Write-Host " (父) → http://127.0.0.1:25080 (本地回环)" -ForegroundColor Yellow
|
||||
Write-Host "============================" -ForegroundColor Cyan
|
||||
|
||||
$env:PYTHONPATH = "$ScriptDir;$ScriptDir\..\gvsdsdk"
|
||||
|
||||
if ($Migrate) {
|
||||
Write-Host "[*] 执行数据库迁移..." -ForegroundColor Magenta
|
||||
python manage.py makemigrations sub_server
|
||||
python manage.py migrate
|
||||
Write-Host "[√] 迁移完成" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host "[*] 启动 Django 开发服务器..." -ForegroundColor Magenta
|
||||
|
||||
python manage.py runserver 0.0.0.0:25080
|
||||
-1
Submodule GVSite deleted from f86622d413.
@@ -0,0 +1,90 @@
|
||||
# ERP 架构债治理清单
|
||||
|
||||
> 生成日期:2026-08-13
|
||||
> 来源:对 d:\SMANAGE 后端(gvsds+gvsdsdk)、前端(GERP Vue)、仓库卫生的三路并行架构审查结论
|
||||
> 状态:按阶段依次执行,每阶段完成后提交,直到收敛
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — 止血(低风险,立即执行)
|
||||
|
||||
| # | 问题 | 动作 | 状态 |
|
||||
|---|---|---|---|
|
||||
| 0.1 | ATestCServer 私钥/SQLite 可入库 | .gitignore 加 `/ATestCServer/`,git rm --cached 清理,目录移入 backup\ | |
|
||||
| 0.2 | media/ 未忽略 | .gitignore 加 `/media/` + git rm --cached | |
|
||||
| 0.3 | 根目录日志/状态文件未忽略 | .gitignore 加 service.log、service_err.log、ips.txt、iptimes.txt、times.txt、timer.txt | |
|
||||
| 0.4 | GVSite/deploy.ps1 必然失败 Bug | L195-196 `$ProdAssets/$DistAssets` 改为 `$ProdGvsite/$DistGvsite` 或删除冗余校验 | |
|
||||
| 0.5 | GVSite 嵌套仓库 gitlink 风险 | .gitignore 加 `/GVSite/` + git rm --cached 检查 | |
|
||||
| 0.6 | frp token 明文 + `/FRP/` 大小写错 | token 移环境变量;gitignore 改小写 `frp/` | |
|
||||
| 0.7 | 前端死依赖(FormKit/tinymce-i18n/@vxe-ui/plugin-render-element/vite-plugin-static-copy) | 从 package.json 移除 + main.ts 去掉 FormKit 注册 | |
|
||||
|
||||
## Phase 1 — 架构债收敛(核心)
|
||||
|
||||
| # | 问题 | 动作 | 状态 |
|
||||
|---|---|---|---|
|
||||
| 1.1 | 模型双轨制(最大负债) | 定唯一模型源:gvsds 侧改 `from gvsdsdk.models import *`(Meta 别名)或建字段一致性校验脚本;stats/payment/commission 三处副本同步 | |
|
||||
| 1.2 | 数据权限硬编码 | `ApplyScopeToQueryset`/form_service 30+ 处 `if table_name` 改策略注册表 | |
|
||||
| 1.3 | 角色解析双实现 | auth.py `ResolveRoleContext` 与 SDK `CurrentRoleMiddleware` 合并为唯一实现 | |
|
||||
| 1.4 | AUTH_USER_MODEL 未设置 | 设 `AUTH_USER_MODEL='core.User'`,删除 auth.py 硬编码覆盖 | |
|
||||
| 1.5 | 部分表无 TenantUUID | Dept/Workgroup/Shop/User 补列+回填迁移,替换 form_service 反射补偿 | |
|
||||
|
||||
## Phase 2 — 安全与运维加固
|
||||
|
||||
| # | 问题 | 动作 | 状态 |
|
||||
|---|---|---|---|
|
||||
| 2.1 | Access Token 168h + refresh 不黑名单 | 缩至 2-4h + `BLACKLIST_AFTER_ROTATION=True` | |
|
||||
| 2.2 | core UserPassword 硬编码默认哈希 | 移除默认值,首登强制改密 | |
|
||||
| 2.3 | IP 过滤默认 disabled + 顺序靠后 | 改显式配置(漏配即 fail-fast),移到认证中间件前 | |
|
||||
| 2.4 | Celery 任务无重试/日志/周期清理 | 套 `RetryOnFailure`;PurgeOldStats 支持全周期类型;补 FileHandler | |
|
||||
| 2.5 | 攻击日志同步写库放大 DoS | 异步化/限频合并 AbnormalUserLog 写入 | |
|
||||
| 2.6 | 异常 print 到 stdout | 改 logging,日志落盘可审计 | |
|
||||
| 2.7 | USE_TZ=False | 评估改 True 并迁移存量时间戳 | |
|
||||
|
||||
## Phase 3 — 前端质量
|
||||
|
||||
| # | 问题 | 动作 | 状态 |
|
||||
|---|---|---|---|
|
||||
| 3.1 | Sku.vue 1870 行全 Mock 已上生产路由 | 移出路由或删除 | |
|
||||
| 3.2 | CrudTable 1201 行职责混乱 | 草稿/卡片树/批量创建抽独立 composable;补 el-form rules;批量删除改并发限流 | |
|
||||
| 3.3 | tab 关闭不销毁组件 | removeTab 时 v-if 卸载 + onDeactivated 清理,或 KeepAlive max 上限 | |
|
||||
| 3.4 | api 层不统一 | 提供 `requestRaw()`;Menu/Editor/Manager 迁 api 层 | |
|
||||
| 3.5 | 双表格栈 + Element Plus 全量引入 | 统一 vxe-table;明确全量或按需二选一 | |
|
||||
| 3.6 | 动态路由空壳 / 死状态 / 43 处 any | 实现真 addRoute 或删空壳;清死状态;类型收口 | |
|
||||
| 3.7 | tinymce 整包 40MB 入 dist | 只拷 min.js + 插件 + zh_CN + oxide 皮肤 | |
|
||||
|
||||
## Phase 4 — 清理归档
|
||||
|
||||
| # | 问题 | 动作 | 状态 |
|
||||
|---|---|---|---|
|
||||
| 4.1 | TempLate 死目录仍公网可达 | 先删 SUBDOMAIN_CONFIG 配置项,再移 backup\ 归档 | |
|
||||
| 4.2 | 子域配置名大小写不一致 | 对齐目录或删配置 | |
|
||||
| 4.3 | 部署脚本三套不统一 | 抽公共 deploy 模板参数化 | |
|
||||
| 4.4 | 根目录散落工具/Version 死代码/双生产入口 | 归 tools\ + docs\;Version 归档;生产入口二选一 | |
|
||||
| 4.5 | 巨型文件拆分 | crb/views.py、auth/managers.py、form_service.py 按领域拆包 | |
|
||||
|
||||
---
|
||||
|
||||
## 详细分析依据
|
||||
|
||||
### 后端(gvsds + gvsdsdk)
|
||||
- H1 模型双轨制:gerp/models.py(929 行)与 gvsdsdk/models.py(1418 行)对同一批表定义两套 ORM,字段类型不一致(FK vs UUIDField);stats/payment/commission 有显式副本。migrations 冻结后 gvsds 模型层仅服务 admin/serializer
|
||||
- H2 数据权限硬编码:role_scope.py L176-195 仅认识 User/UserRole/Order 三张表;form_service.py 30+ 处表级特判
|
||||
- H3 无 TenantUUID:User/Dept/Workgroup/Shop 仅 CompanyUUID,form_service 用 `CompanyUUID__TenantUUID` 反射补偿
|
||||
- H4 角色解析双实现:auth.py `ResolveRoleContext`(JWT 链路)与 gvsdsdk/auth/middleware.py `CurrentRoleMiddleware`(Session 链路)逐行相似
|
||||
- H5 未设 AUTH_USER_MODEL:admin/sessions 用 auth.User,业务用 core.User,靠 auth.py L101 硬编码覆盖
|
||||
- 中优先级:7 库路由粗糙(AbnormalUserLog 显式 .using 例外);根 urls.py 287 行巨型化 + 视图双入口 + 生产自托管静态/catch-all;巨型文件(crb/views.py 2637、auth/managers.py 2290、form_service.py 1726、gvsdsdk/models.py 1418);安全弱点(168h token、默认密码哈希、print 堆栈);Celery 无重试/无日志落盘/beat 任务名不一致
|
||||
|
||||
### 前端(GERP)
|
||||
- H1 FormKit 死注册(main.ts L10,35),构建含独立 formkit chunk
|
||||
- H2 大文件:Sku.vue 1870(全 Mock + 12 TODO)、Mailbox.vue 1650、Editor.vue 1431、Manager.vue 1392(15 TODO + 直接 XHR)、CrudTable.vue 1201、Menu.vue 1129、EntityPicker.vue 1024
|
||||
- H3 CrudTable 职责混乱:表格+表单+卡片树+草稿+批量创建;form 无 rules;批量删除串行 N 请求
|
||||
- H4 v-show 常驻内存泄漏:removeTab 不销毁组件实例
|
||||
- 中优先级:Element Plus 全量引入名不副实;vxe/el-table 双栈;api 层不统一(responseType hack ×2、直接 XHR);动态路由空壳 + meta 无类型;stores 死状态 + AdminCheckResult 双份定义;43 处 any;tinymce 整包 40MB;4 个未使用依赖
|
||||
|
||||
### 仓库卫生
|
||||
- H1 ATestCServer 私钥/SQLite 可入库(半成品骨架)
|
||||
- H2 media/ 未忽略
|
||||
- H3 根目录运行时文件未忽略(service.log 含堆栈、ips.txt 1743 行)
|
||||
- H4 GVSite/deploy.ps1 L195-196 未定义变量必然失败
|
||||
- H5 GVSite 未整目录忽略(gitlink 风险);TempLate 子域配置名大小写不一致
|
||||
- 中优先级:TempLate 死目录仍公网服务;三套部署脚本不统一;gvsdsdk 版本矩阵不一致(主 1.1.0/子 1.0.0);frp token 明文 + gitignore 大小写错
|
||||
-1304
File diff suppressed because it is too large.
Load diff
Binary file not shown.
|
Before Width: | Height: | Size: 5.5 KiB |
-42
@@ -1,42 +0,0 @@
|
||||
[EXCEPTION] CmeBalancesViewSet: 身份认证信息未提供。
|
||||
[EXCEPTION] MenuListCompatView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserInfoView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserInfoView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserInfoView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserInfoView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserInfoView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserInfoView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserInfoView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserInfoView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserAvatarView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserAvatarView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserAvatarView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserAvatarView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserAvatarView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserAvatarView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserAvatarView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserAvatarView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserAvatarView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserAvatarView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserAvatarView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserInfoView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserInfoView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserInfoView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserInfoView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserInfoView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserInfoView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserInfoView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserInfoView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserRolesView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserAvatarView: 身份认证信息未提供。
|
||||
[EXCEPTION] DashboardStatsView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserInfoView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserAvatarView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserAvatarView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserAvatarView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserAvatarView: 身份认证信息未提供。
|
||||
[EXCEPTION] FormAPICompatView: 身份认证信息未提供。
|
||||
[EXCEPTION] FormAPICompatView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserRolesView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserInfoView: 身份认证信息未提供。
|
||||
[EXCEPTION] GetUserInfoView: 身份认证信息未提供。
|
||||
Reference in new issue
Block a user