feat(plugins): 4 个业务插件代码 + 前台公开页面改造

plugins/navManager:导航 CRUD + 导航分类 CRUD(独立 blueprint)
plugins/friendManager:友情链接 CRUD
plugins/aboutManager:关于本站 CRUD
plugins/siteStats:访问统计主页 + JSON 接口 + /site/* GET 埋点 + nav 卡片点击埋点

applications/view/public/nav.py:导航聚合页 / 分类详情 / JSON 接口,支持 visit_count 注入
applications/view/public/about.py:关于本站公开页
applications/view/public/friend.py:友情链接公开页

templates/public/base.html:响应式首页(搜索框 / 分类抽屉 / 5 主题切换 / footer 隐私小字)
templates/public/index.html / category.html:卡片右下角访问次数
templates/public/about.html / friend.html:关于 + 友链前台页

docs/plugins-development.md:插件开发完整指南(生命周期 + 4 个示例 + FAQ + framework 迁移)

scripts/:探针与端到端验证脚本(probe_*/verify_*/test_*)
This commit is contained in:
bwstudio
2026-09-06 18:18:06 +08:00
parent 1ea69c2c34
commit 0ffc103e46
53 changed files with 4705 additions and 89 deletions
+142
View File
@@ -0,0 +1,142 @@
"""
导航分类功能端到端探针
======================
不依赖浏览器 + ddddocr + 调试 header,直接用 Flask test_client 走完整流程:
1. 登录 admin
2. GET /system/nav-category/ (主页面 HTML)
3. GET /system/nav-category/data (table_api JSON)
4. POST /system/nav-category/save (新增)
5. PUT /system/nav-category/update (更新)
6. PUT /system/nav-category/enable (启用/禁用)
7. DELETE /system/nav-category/remove/<id> (删除)
8. 验证每个 API 的 {code, message, data} 协议
退出码:全部通过 → 0;任一失败 → 1
"""
import json
import sys
import traceback
sys.path.insert(0, r'D:\bwstudio\pear-admin-flask')
from applications import create_app
app = create_app()
client = app.test_client()
def _login():
"""admin/123456 登录拿 cookie session。"""
# 拿一张 captcha —— 通过 test_client 共享 session_transaction
r = client.get('/system/passport/getCaptcha')
assert r.status_code == 200, r.status_code
with client.session_transaction() as sess:
code = sess.get('code') or ''
code = code.lower()
assert code, 'captcha code missing'
r = client.post('/system/passport/login', data={
'username': 'admin',
'password': '123456',
'captcha': code,
}, follow_redirects=False)
assert r.status_code in (200, 302), r.status_code
def _show(label, resp):
body = resp.get_data(as_text=True)
print(f' [{resp.status_code}] {label} -> {body[:200]}{"..." if len(body) > 200 else ""}')
def main():
_login()
print('=== 已登录,开始 E2E 探针 ===\n')
# 1. 分类主页面 HTML
r = client.get('/system/nav-category/')
print(f'[HTML main] {r.status_code} content-type={r.content_type}')
assert r.status_code == 200
assert b'nav-category/data' in r.data or b'\u5bfc\u822a\u5206\u7c7b' in r.data, 'main 页面未包含预期内容'
print(' [OK] 分类主页面返回 200,含预期关键字\n')
# 2. data 接口(table_api 协议:{code, data, count, msg, limit}
r = client.get('/system/nav-category/data')
assert r.status_code == 200
payload = r.get_json()
assert payload['code'] == 0, payload
before_ids = {row['id'] for row in payload['data']}
print(f' [OK] data 接口 code=0count={payload["count"]},已有分类 {len(before_ids)} 条:{sorted(before_ids)}\n')
# 3. 新增(success_api 协议:{success, msg}
unique_name = f'E2E临时分类{int(__import__("time").time())}'
r = client.post('/system/nav-category/save',
json={'name': unique_name, 'icon': 'layui-icon-test',
'description': 'e2e created', 'sort': 1, 'enable': 1})
payload = r.get_json()
assert payload.get('success') is True, payload
print(f' [OK] 新增成功:{unique_name}\n')
# 4. 拿到 id
r = client.get('/system/nav-category/data')
payload = r.get_json()
new_id = next((row['id'] for row in payload['data'] if row['name'] == unique_name), None)
assert new_id is not None, '新增的分类查不到'
print(f' [OK] 查回新增分类 id={new_id}\n')
# 5. 更新
r = client.put('/system/nav-category/update',
json={'catId': new_id, 'name': unique_name + '_改',
'icon': 'layui-icon-edit', 'description': 'e2e updated',
'sort': 99, 'enable': 1})
payload = r.get_json()
assert payload.get('success') is True, payload
print(f' [OK] 更新成功\n')
# 6. 禁用 → 启用
r = client.put('/system/nav-category/disable', json={'catId': new_id})
assert r.get_json().get('success') is True
r = client.put('/system/nav-category/enable', json={'catId': new_id})
assert r.get_json().get('success') is True
print(f' [OK] 禁用→启用通过\n')
# 7. 删除
r = client.delete(f'/system/nav-category/remove/{new_id}')
assert r.get_json().get('success') is True, r.get_json()
print(f' [OK] 删除成功\n')
# 8. 查回 count 必须等于开始前
r = client.get('/system/nav-category/data')
payload = r.get_json()
after_ids = {row['id'] for row in payload['data']}
assert before_ids == after_ids, f'分类集合变化:before={before_ids}, after={after_ids}'
print(f' [OK] 删除后 count 还原,新增的 id 已不在集合中\n')
# 9. 重名检测
existing_name = None
if payload['data']:
existing_name = payload['data'][0]['name']
if existing_name:
r = client.post('/system/nav-category/save',
json={'name': existing_name, 'icon': 'x', 'description': '', 'sort': 0, 'enable': 1})
body = r.get_json()
assert body.get('success') is False, body
print(f' [OK] 重名拦截正常:{body["msg"]}\n')
print('=== ALL PASS ===')
if __name__ == '__main__':
try:
main()
sys.exit(0)
except AssertionError as e:
print(f'\n!!! ASSERT FAIL: {e}')
traceback.print_exc()
sys.exit(1)
except Exception as e:
print(f'\n!!! UNEXPECTED: {e}')
traceback.print_exc()
sys.exit(1)