release: freeze 2026-10-15 validated release baseline
CI/CD Pipeline / UI Contracts (source-structure gates) (push) Canceled after 0s
CI/CD Pipeline / E2E (live stack) (push) Canceled after 0s
CI/CD Pipeline / Backend Build & Test (push) Canceled after 0s
CI/CD Pipeline / Admin Web Build (push) Canceled after 0s
CI/CD Pipeline / Mini Program Compile Check (push) Canceled after 0s
CI/CD Pipeline / Migration Validation (push) Canceled after 0s
CI/CD Pipeline / Test Shell Guard (Phase 23 Gate) (push) Canceled after 0s
CI/CD Pipeline / UI Contracts (source-structure gates) (push) Canceled after 0s
CI/CD Pipeline / E2E (live stack) (push) Canceled after 0s
CI/CD Pipeline / Backend Build & Test (push) Canceled after 0s
CI/CD Pipeline / Admin Web Build (push) Canceled after 0s
CI/CD Pipeline / Mini Program Compile Check (push) Canceled after 0s
CI/CD Pipeline / Migration Validation (push) Canceled after 0s
CI/CD Pipeline / Test Shell Guard (Phase 23 Gate) (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const vm = require('node:vm');
|
||||
const ts = require('../admin-web/node_modules/typescript');
|
||||
|
||||
(async () => {
|
||||
let pending = 2;
|
||||
const responses = {
|
||||
'/operator/analytics/dashboard': { totalUsers: 12, approvedProfiles: 10 },
|
||||
'/operator/analytics/v11-dashboard': { localEligibility: { pending: 99 } },
|
||||
'/operator/activities': [],
|
||||
'/operator/matchmaker/management/services': [
|
||||
{ serviceId: 'demo', status: 'waiting_user', userDisplayNumber: 'YH2026051502', latestFollowupContent: 'Demo验收跟进', latestFollowupAt: new Date().toISOString() },
|
||||
],
|
||||
};
|
||||
const exports = {};
|
||||
const source = fs.readFileSync('admin-web/src/services/dashboard.ts', 'utf8');
|
||||
vm.runInNewContext(ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS } }).outputText, {
|
||||
exports,
|
||||
require: () => ({ default: { get: async url => ({ success: true, data:
|
||||
url === '/operator/reviews/stats' ? { pending, processing: 1 } : responses[url] }) } }),
|
||||
});
|
||||
const perms = ['analytics.view', 'activity.view', 'matchmaker.view', 'matchmaker.assign', 'review.view'];
|
||||
let model = await exports.loadDashboard(perms);
|
||||
assert.equal(model.metrics.find(m => m.key === 'pending_reviews').value, 3);
|
||||
assert.equal(model.metrics.find(m => m.key === 'matchmaker_todos').value, 1);
|
||||
assert.equal(model.followups[0].content, 'Demo验收跟进');
|
||||
assert.equal(exports.summarizeMatchmakerServices(responses['/operator/matchmaker/management/services']).total, 1);
|
||||
assert.equal(model.metrics.find(m => m.key === 'matchmaker_todos').route, '/matchmaker');
|
||||
pending = 1;
|
||||
model = await exports.loadDashboard(perms);
|
||||
assert.equal(model.metrics.find(m => m.key === 'pending_reviews').value, 2);
|
||||
assert.match(fs.readFileSync('admin-web/src/views/DashboardView.vue', 'utf8'), /onActivated\(retryLoad\)/);
|
||||
console.log('PASS: 审核队列真实计数、审核后减一、管理服务待办与跟进、缓存返回刷新');
|
||||
})().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
@@ -97,7 +97,7 @@ def _ensure_activity(which: str = "A") -> str:
|
||||
proc = subprocess.run(
|
||||
["docker", "exec", "-i", PG_CONTAINER, "psql", "-U", "postgres", "-d", PG_DB,
|
||||
"-v", "ON_ERROR_STOP=1", "-q"],
|
||||
input=seed, capture_output=True, text=True,
|
||||
input=seed, capture_output=True, text=True, encoding="utf-8",
|
||||
)
|
||||
assert proc.returncode == 0, (
|
||||
f"互选前置数据 seed 失败(需要容器 {PG_CONTAINER} 可访问):\n"
|
||||
|
||||
+13
-3
@@ -51,6 +51,9 @@ class ApiClient:
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
) -> requests.Response:
|
||||
# 登录接口会设置运营 HttpOnly Cookie;无 token 用例必须真正模拟未认证请求。
|
||||
if token is None:
|
||||
self.session.cookies.clear()
|
||||
merged = {"Authorization": f"Bearer {token}"} if token else {}
|
||||
if headers:
|
||||
# 显式传入的头覆盖默认头(幂等键等按用例定制)
|
||||
@@ -117,10 +120,17 @@ def operator_password() -> str:
|
||||
|
||||
|
||||
ADMIN_USERNAME = "e2e_admin"
|
||||
# 本机活栈可通过环境变量切换到已授权的演示运营账号;CI 默认仍使用 seed 账号。
|
||||
LOGIN_USERNAME = os.environ.get("E2E_OPERATOR_USERNAME", ADMIN_USERNAME)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def admin_token(api: ApiClient, operator_password: str) -> str:
|
||||
def admin_password(operator_password: str) -> str:
|
||||
return os.environ.get("E2E_ADMIN_PASSWORD", operator_password)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def admin_token(api: ApiClient, operator_password: str, admin_password: str) -> str:
|
||||
"""超级管理员 token。
|
||||
|
||||
必须使用 **seed 脚本创建的账号**,而不是 `admin`。
|
||||
@@ -135,10 +145,10 @@ def admin_token(api: ApiClient, operator_password: str) -> str:
|
||||
data = api.ok(
|
||||
"POST",
|
||||
"/auth/operator/login",
|
||||
json={"username": ADMIN_USERNAME, "password": operator_password},
|
||||
json={"username": LOGIN_USERNAME, "password": admin_password},
|
||||
)
|
||||
token = data.get("accessToken")
|
||||
assert token, f"{ADMIN_USERNAME} 登录未返回 accessToken"
|
||||
assert token, f"{LOGIN_USERNAME} 登录未返回 accessToken"
|
||||
return token
|
||||
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ def _ensure_station_seed() -> None:
|
||||
proc = subprocess.run(
|
||||
["docker", "exec", "-i", PG_CONTAINER, "psql", "-U", "postgres", "-d", PG_DB,
|
||||
"-v", "ON_ERROR_STOP=1", "-q"],
|
||||
input=seed, capture_output=True, text=True,
|
||||
input=seed, capture_output=True, text=True, encoding="utf-8",
|
||||
)
|
||||
assert proc.returncode == 0, (
|
||||
f"服务站前置数据 seed 失败(需要容器 {PG_CONTAINER} 可访问):\n"
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).parents[2]
|
||||
|
||||
|
||||
class MatchmakerEntryContractTest(unittest.TestCase):
|
||||
def test_user_login_exposes_staff_entry(self):
|
||||
source = (ROOT / "miniapp/pages/login/login.wxml").read_text(encoding="utf-8")
|
||||
script = (ROOT / "miniapp/pages/login/login.ts").read_text(encoding="utf-8")
|
||||
self.assertIn("红娘工作人员入口", source)
|
||||
self.assertIn("onOperatorEntry", source)
|
||||
self.assertIn("ROUTE.OPERATOR.LOGIN", script)
|
||||
|
||||
def test_app_routes_existing_operator_session_to_workbench(self):
|
||||
source = (ROOT / "miniapp/app.js").read_text(encoding="utf-8")
|
||||
self.assertIn("operatorToken", source)
|
||||
self.assertIn("matchmaker-workbench", source)
|
||||
self.assertIn("stores.bootstrap()", source)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,16 @@
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).parents[2]
|
||||
|
||||
|
||||
class MatchmakerLoginContractTest(unittest.TestCase):
|
||||
def test_login_uses_public_operator_auth_endpoint(self):
|
||||
source = (ROOT / "miniapp/pages/matchmaker-login/matchmaker-login.js").read_text(encoding="utf-8")
|
||||
self.assertIn("/auth/operator/login", source)
|
||||
self.assertNotIn("/operator/auth/login", source)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,35 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const vm = require('node:vm');
|
||||
const path = require('node:path');
|
||||
|
||||
(async () => {
|
||||
let page;
|
||||
let status = 'waiting_user';
|
||||
const requests = [];
|
||||
vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../../miniapp/pages/matchmaker-progress/matchmaker-progress.js'), 'utf8'), {
|
||||
Page: definition => { page = definition; },
|
||||
wx: { getStorageSync: () => 'test-token', showToast: () => {} },
|
||||
require: name => name.endsWith('/api') ? {
|
||||
get: async url => {
|
||||
requests.push(url);
|
||||
assert.equal(url, '/matchmaker/services');
|
||||
return [{ id: 'other', status: 'completed' }, { id: 'demo-service', status }];
|
||||
},
|
||||
} : { matchmakerService: { confirmService: async id => {
|
||||
assert.equal(id, 'demo-service');
|
||||
status = 'in_service';
|
||||
} } },
|
||||
});
|
||||
page.setData = values => Object.assign(page.data, values);
|
||||
await page.loadService('demo-service');
|
||||
assert.equal(page.data.service.id, 'demo-service');
|
||||
await page.onConfirmService();
|
||||
assert.equal(page.data.service.status, 'in_service');
|
||||
assert.equal(page.data.confirming, false);
|
||||
assert.equal(requests.length, 2);
|
||||
await page.loadService('missing');
|
||||
assert.equal(page.data.service, null);
|
||||
assert.equal(page.data.loading, false);
|
||||
console.log('PASS: 本人列表定位、确认后刷新及缺失服务处理');
|
||||
})().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
@@ -0,0 +1,30 @@
|
||||
from pathlib import Path
|
||||
import json
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).parents[2]
|
||||
|
||||
|
||||
class MatchmakerRecommendationContractTest(unittest.TestCase):
|
||||
def test_recommendation_page_is_registered_and_user_decision_only(self):
|
||||
app = json.loads((ROOT / "miniapp/app.json").read_text(encoding="utf-8"))
|
||||
self.assertIn("pages/matchmaker-recommendations/matchmaker-recommendations", app["pages"])
|
||||
source = (ROOT / "miniapp/pages/matchmaker-recommendations/matchmaker-recommendations.js").read_text(encoding="utf-8")
|
||||
self.assertIn("fetchRecommendations", source)
|
||||
self.assertIn("matchStore.like", source)
|
||||
self.assertIn("matchStore.pass", source)
|
||||
self.assertNotIn("operatorToken", source)
|
||||
|
||||
def test_matchmaker_service_routes_to_dedicated_recommendation_page(self):
|
||||
source = (ROOT / "miniapp/pages/matchmaker/matchmaker.ts").read_text(encoding="utf-8")
|
||||
self.assertIn("MATCHMAKER_RECOMMENDATIONS", source)
|
||||
self.assertNotIn("ROUTE.MAIN.FATE_CENTER, { id: serviceId }", source)
|
||||
|
||||
def test_recommendation_store_accepts_array_api_response(self):
|
||||
source = (ROOT / "miniapp/stores/matchmakerStore.js").read_text(encoding="utf-8")
|
||||
self.assertIn("Array.isArray(result)", source)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,29 @@
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).parents[2]
|
||||
|
||||
|
||||
class MatchmakerReferralTaskContractTest(unittest.TestCase):
|
||||
def test_workbench_loads_operator_referral_history(self):
|
||||
source = (ROOT / "miniapp/pages/matchmaker-workbench/matchmaker-workbench.js").read_text(encoding="utf-8")
|
||||
wxml = (ROOT / "miniapp/pages/matchmaker-workbench/matchmaker-workbench.wxml").read_text(encoding="utf-8")
|
||||
self.assertIn("/referrals/operator/history", source)
|
||||
self.assertIn("referralTasks", source)
|
||||
self.assertIn("referralTasks.length", wxml)
|
||||
self.assertIn("{{item.statusLabel}}", wxml)
|
||||
self.assertIn("{{item.targetLabel}}", wxml)
|
||||
self.assertIn("查看详情", wxml)
|
||||
self.assertIn("发起转介", wxml)
|
||||
self.assertIn("/referrals/activity", source)
|
||||
self.assertIn("/referrals/service-station", source)
|
||||
self.assertIn("/operator/matchmaker/recommendations", source)
|
||||
self.assertIn("推荐进展", wxml)
|
||||
self.assertNotIn("matchStore.like", source)
|
||||
self.assertNotIn("matchStore.pass", source)
|
||||
self.assertNotIn("emergencyContactPhone", source)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,17 @@
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).parents[2]
|
||||
|
||||
|
||||
class MatchmakerServiceCardContractTest(unittest.TestCase):
|
||||
def test_store_normalizes_api_entity_fields_for_card_navigation(self):
|
||||
source = (ROOT / "miniapp/stores/matchmakerStore.js").read_text(encoding="utf-8")
|
||||
self.assertIn("serviceId: service.serviceId || service.id", source)
|
||||
self.assertIn("requirement: service.requirement || service.description || ''", source)
|
||||
self.assertIn(".map((item) => this.normalizeService(item))", source)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,26 @@
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).parents[2]
|
||||
|
||||
|
||||
class MatchmakerServiceConfirmationContractTest(unittest.TestCase):
|
||||
def test_backend_exposes_user_confirmation_and_writes_in_service(self):
|
||||
controller = (ROOT / "backend/src/main/java/com/danyang/match/platform/matchmaker/controller/MatchmakerController.java").read_text(encoding="utf-8")
|
||||
service = (ROOT / "backend/src/main/java/com/danyang/match/platform/matchmaker/service/MatchmakerService.java").read_text(encoding="utf-8")
|
||||
mapper = (ROOT / "backend/src/main/java/com/danyang/match/platform/matchmaker/mapper/MatchmakerServiceMapper.java").read_text(encoding="utf-8")
|
||||
self.assertIn('@PostMapping("/services/{id}/confirm")', controller)
|
||||
self.assertIn("confirmService(id, principal.id())", controller)
|
||||
self.assertIn("confirmWaitingForUser", mapper)
|
||||
|
||||
def test_progress_page_only_offers_confirmation_for_waiting_user(self):
|
||||
source = (ROOT / "miniapp/pages/matchmaker-progress/matchmaker-progress.js").read_text(encoding="utf-8")
|
||||
wxml = (ROOT / "miniapp/pages/matchmaker-progress/matchmaker-progress.wxml").read_text(encoding="utf-8")
|
||||
self.assertIn("confirmService", source)
|
||||
self.assertIn("this.data.service.status !== 'waiting_user'", source)
|
||||
self.assertIn("bindtap=\"onConfirmService\"", wxml)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,30 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const vm = require('node:vm');
|
||||
const source = fs.readFileSync('miniapp/app.js', 'utf8');
|
||||
|
||||
function launch(token, path) {
|
||||
const calls = [];
|
||||
let app;
|
||||
vm.runInNewContext(source, {
|
||||
require: () => ({
|
||||
bootstrap: () => calls.push('user-bootstrap'),
|
||||
appStore: { init: () => { calls.push('config'); return Promise.resolve(); } },
|
||||
}),
|
||||
App: (definition) => { app = definition; },
|
||||
wx: {
|
||||
getStorageSync: () => token,
|
||||
reLaunch: ({ url }) => calls.push(url),
|
||||
},
|
||||
});
|
||||
app.onLaunch({ path });
|
||||
return calls;
|
||||
}
|
||||
|
||||
assert.deepEqual(launch('', 'pages/index/index'), ['user-bootstrap']);
|
||||
assert.deepEqual(launch('staff-session', 'pages/index/index'), ['config', '/pages/matchmaker-workbench/matchmaker-workbench']);
|
||||
for (const path of ['login', 'workbench', 'service-detail']) {
|
||||
assert.deepEqual(launch('', `pages/matchmaker-${path}/matchmaker-${path}`), ['config']);
|
||||
}
|
||||
assert.deepEqual(launch('', 'pages/matchmaker-progress/matchmaker-progress'), ['user-bootstrap']);
|
||||
console.log('PASS: user startup, staff session, staff deep links and user service route');
|
||||
@@ -0,0 +1,46 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const vm = require('node:vm');
|
||||
const path = require('node:path');
|
||||
const root = path.resolve(__dirname, '../..');
|
||||
function load(file, api, service) {
|
||||
let page;
|
||||
vm.runInNewContext(fs.readFileSync(path.join(root, file), 'utf8'), {
|
||||
Page: value => { page = value; },
|
||||
wx: { getStorageSync: () => 'test', showToast() {}, navigateBack() {} },
|
||||
require: name => name.endsWith('/api') ? api : service,
|
||||
});
|
||||
page.setData = data => Object.assign(page.data, data);
|
||||
return page;
|
||||
}
|
||||
(async () => {
|
||||
const id = '00000000-0000-0000-0000-000000000001';
|
||||
const calls = [];
|
||||
const service = { referralService: {
|
||||
getMyReferralHistory: async () => [{ id, type: 'activity', activityId: id,
|
||||
activityDisplayId: 'HD20261014001', referralDisplayId: 'ZF20261014001', status: 'pending' }],
|
||||
confirmReferral: async (value, type) => { calls.push({ value, type }); },
|
||||
} };
|
||||
const referral = load('miniapp/pages/referral-confirmation/referral-confirmation.js', {}, service);
|
||||
await referral.load();
|
||||
assert.equal(referral.data.referrals[0].detail, 'HD20261014001');
|
||||
assert.equal(referral.data.referrals[0].referralDisplayId, 'ZF20261014001');
|
||||
await referral.confirm({ currentTarget: { dataset: { index: 0 } } });
|
||||
assert.equal(calls[0].value, id);
|
||||
assert.equal(calls[0].type, 'activity');
|
||||
service.referralService.getMyReferralHistory = async () => [{ id, type: 'activity', activityId: id }];
|
||||
await referral.load();
|
||||
assert.equal(referral.data.referrals[0].detail, '公益活动');
|
||||
const urls = [];
|
||||
const detail = load('miniapp/pages/matchmaker-service-detail/matchmaker-service-detail.js', {
|
||||
get: async url => { urls.push(url); return { serviceId: id, serviceDisplayId: 'FW20261014001', userDisplayNumber: 'YH20261014002', followups: [] }; },
|
||||
}, {});
|
||||
detail.data.serviceId = id;
|
||||
await detail.loadDetail();
|
||||
assert.equal(urls[0], `/operator/matchmaker/services/${id}`);
|
||||
assert.equal(detail.data.detail.serviceDisplayId, 'FW20261014001');
|
||||
const template = fs.readFileSync(path.join(root, 'miniapp/pages/matchmaker-service-detail/matchmaker-service-detail.wxml'), 'utf8');
|
||||
assert(template.includes('detail.serviceDisplayId'));
|
||||
assert(!template.includes('{{detail.serviceId}}'));
|
||||
console.log('PASS: 小程序真实页面脚本显示业务编号、缺号不回退 UUID、确认和详情请求保留内部 UUID。');
|
||||
})().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
@@ -0,0 +1,31 @@
|
||||
from pathlib import Path
|
||||
import json
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).parents[2]
|
||||
|
||||
|
||||
class ReferralPageContractTest(unittest.TestCase):
|
||||
def test_referral_confirmation_page_is_registered_and_user_only(self):
|
||||
app = json.loads((ROOT / "miniapp/app.json").read_text(encoding="utf-8"))
|
||||
self.assertIn("pages/referral-confirmation/referral-confirmation", app["pages"])
|
||||
page = ROOT / "miniapp/pages/referral-confirmation/referral-confirmation.js"
|
||||
source = page.read_text(encoding="utf-8")
|
||||
self.assertIn("getMyReferralHistory", source)
|
||||
self.assertIn("confirmReferral", source)
|
||||
self.assertIn("load()", source)
|
||||
self.assertNotIn("operatorToken", source)
|
||||
service_source = (ROOT / "miniapp/services/referralService.js").read_text(encoding="utf-8")
|
||||
self.assertIn("referral-confirm:${targetId}:${targetType}", service_source)
|
||||
|
||||
def test_referral_page_does_not_offer_operator_actions_or_sensitive_fields(self):
|
||||
source = (ROOT / "miniapp/pages/referral-confirmation/referral-confirmation.wxml").read_text(encoding="utf-8")
|
||||
self.assertNotIn("代用户", source)
|
||||
self.assertNotIn("手机号", source)
|
||||
self.assertNotIn("身份证", source)
|
||||
self.assertIn("确认转介", source)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,60 @@
|
||||
// shortcut: 静态检查覆盖直接绑定;动态文案和接口映射仍须行为测试与人工验收。
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const assert = require('node:assert/strict');
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const internal = { test: value => (value.match(/\b(?:id|\w*Id)\b/g) || [])
|
||||
.some(field => !/displayId$/i.test(field)) };
|
||||
function scan(source) {
|
||||
const findings = [];
|
||||
const add = (index, kind, value) => findings.push({
|
||||
line: source.slice(0, index).split('\n').length, kind, value
|
||||
});
|
||||
// 脚本和属性保留行数,但不把路由、事件参数、条件判断当可见文本。
|
||||
const markup = source.replace(/<script\b[\s\S]*?<\/script>/g, s => s.replace(/[^\n]/g, ' '));
|
||||
const visible = markup.replace(/<[^>]*>/g, s => s.replace(/[^\n]/g, ' '));
|
||||
for (const match of visible.matchAll(/\{\{([\s\S]*?)\}\}/g)) {
|
||||
if (/^\s*(?:categoryLabel\(row\.categoryId\)|orgMap\[row\.organizationId\](?:\s*\|\|\s*'-')?)\s*$/.test(match[1])) continue;
|
||||
if (internal.test(match[1])) add(match.index, '可见插值需审查', match[1].trim());
|
||||
}
|
||||
for (const match of markup.matchAll(/<el-table-column\b[^>]*\bprop=["']([^"']+)["'][^>]*>/g)) {
|
||||
if (match[1] === 'organizationId' && !match[0].endsWith('/>')) {
|
||||
const end = markup.indexOf('</el-table-column>', match.index);
|
||||
if (end >= 0 && /orgMap\[row\.organizationId\]/.test(markup.slice(match.index, end))) continue;
|
||||
}
|
||||
if (internal.test(match[1])) add(match.index, '表格字段需审查', match[1]);
|
||||
}
|
||||
for (const match of markup.matchAll(/<el-input\b[^>]*\bv-model=["']([^"']+)["'][^>]*>/g)) {
|
||||
if (internal.test(match[1])) add(match.index, '编号输入需解析', match[1]);
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
function files(dir) {
|
||||
return fs.readdirSync(dir, { withFileTypes: true }).flatMap(entry => {
|
||||
const target = path.join(dir, entry.name);
|
||||
return entry.isDirectory() ? files(target) : /\.(vue|wxml)$/.test(entry.name) ? [target] : [];
|
||||
});
|
||||
}
|
||||
if (process.argv.includes('--self-check')) {
|
||||
assert.equal(scan('<text>{{item.userId}}</text>').length, 1);
|
||||
assert.equal(scan('<text>{{item.displayId}}</text>').length, 0);
|
||||
assert.equal(scan('<view data-id="{{item.id}}" wx:if="{{item.userId}}">姓名</view>').length, 0);
|
||||
assert.equal(scan('<el-table-column prop="operatorId" label="红娘" />').length, 1);
|
||||
assert.equal(scan('<el-input v-model="form.targetUserId" />').length, 1);
|
||||
assert.equal(scan('<script>const x = "{{item.id}}"</script>').length, 0);
|
||||
assert.equal(scan('<text>{{orgMap[row.organizationId]}}</text>').length, 0);
|
||||
assert.equal(scan('<text>{{otherMap[row.userId]}}</text>').length, 1);
|
||||
assert.equal(scan('<el-table-column prop="organizationId" />').length, 1);
|
||||
console.log('PASS: 9 项检查器自检(仅允许已审查的名称映射)。');
|
||||
} else {
|
||||
const targets = ['admin-web/src', 'miniapp/pages', 'miniapp/components'].flatMap(dir => files(path.join(root, dir)));
|
||||
let count = 0;
|
||||
for (const file of targets) {
|
||||
for (const finding of scan(fs.readFileSync(file, 'utf8'))) {
|
||||
console.log(`${path.relative(root, file).replaceAll('\\', '/')}:${finding.line} ${finding.kind}: ${finding.value}`);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
console.log(`${count ? 'FAIL' : 'PASS'}: ${targets.length} 个模板,${count} 个待审查位置。静态结果不代表端到端验收。`);
|
||||
process.exitCode = count ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// 使用独立验证库;不写入运营演示库。
|
||||
const { execFileSync, execFile } = require('node:child_process');
|
||||
const assert = require('node:assert/strict');
|
||||
const args = ['exec', 'danyang-match-postgres', 'psql', '-U', 'postgres', '-d', 'public_id_validation', '-v', 'ON_ERROR_STOP=1', '-At', '-c'];
|
||||
const sql = query => execFileSync('docker', [...args, query], { encoding: 'utf8' }).trim();
|
||||
const parallel = query => new Promise((resolve, reject) => execFile('docker', [...args, query], (error, stdout) => error ? reject(error) : resolve(stdout.trim())));
|
||||
(async () => {
|
||||
sql("DELETE FROM public_display_id_counters WHERE local_date='2099-01-01'");
|
||||
const ids = await Promise.all(Array.from({ length: 20 }, () => parallel("SELECT public_next_display_id('RW','2099-01-01')")));
|
||||
assert.equal(new Set(ids).size, 20);
|
||||
assert(ids.every(id => /^RW20990101\d{3}$/.test(id)));
|
||||
assert.equal(sql("SELECT public_next_display_id('YH','2099-01-01','male')"), 'YH20990101001');
|
||||
assert.equal(sql("SELECT public_next_display_id('YH','2099-01-01','male')"), 'YH20990101003');
|
||||
assert.equal(sql("SELECT public_next_display_id('YH','2099-01-01','female')"), 'YH20990101002');
|
||||
sql("UPDATE public_display_id_counters SET last_sequence=998 WHERE prefix='RW' AND local_date='2099-01-01'");
|
||||
assert.equal(sql("SELECT public_next_display_id('RW','2099-01-01')"), 'RW20990101999');
|
||||
assert.throws(() => sql("SELECT public_next_display_id('RW','2099-01-01')"));
|
||||
assert.throws(() => sql("SELECT public_next_display_id('ZZ','2099-01-01')"));
|
||||
assert.throws(() => sql("SELECT public_next_display_id('YH','2099-01-01','unknown')"));
|
||||
sql("BEGIN; SELECT public_next_display_id('HD','2099-01-01'); ROLLBACK;");
|
||||
assert.equal(sql("SELECT COUNT(*) FROM public_display_id_counters WHERE prefix='HD' AND local_date='2099-01-01'"), '0');
|
||||
assert.equal(sql("SELECT COUNT(*) FROM public_display_ids WHERE display_id IS NOT NULL AND display_id !~ '^[A-Z]{2}[0-9]{11}$'"), '0');
|
||||
assert.equal(sql("SELECT COUNT(*) FROM (SELECT display_id FROM public_display_ids WHERE display_id IS NOT NULL GROUP BY display_id HAVING COUNT(*)>1) s"), '0');
|
||||
assert.equal(sql("SELECT COUNT(*) FROM public_display_id_aliases WHERE entity_type IN ('users','operators') AND new_display_id IS NULL"), '0');
|
||||
sql("BEGIN; INSERT INTO matchmaker_followups(id,service_id,operator_id,followup_type,content,created_at) SELECT gen_random_uuid(),id,assigned_operator_id,'system','编号事务测试','2099-01-01' FROM matchmaker_services WHERE assigned_operator_id IS NOT NULL LIMIT 1; SELECT 1 / 0; ROLLBACK;");
|
||||
})().catch(error => {
|
||||
// 最后一项故意使事务失败,连接退出自动回滚;检查实体和计数器均未遗留。
|
||||
if (String(error.stderr || error.message).includes('division by zero')) {
|
||||
assert.equal(sql("SELECT COUNT(*) FROM matchmaker_followups WHERE content='编号事务测试'"), '0');
|
||||
assert.equal(sql("SELECT COUNT(*) FROM public_display_id_counters WHERE prefix='GJ' AND local_date='2099-01-01'"), '0');
|
||||
console.log('PASS: PostgreSQL 历史回填、20 路并发、奇偶、999 边界、非法参数和实体事务回滚。');
|
||||
} else { console.error(error.message); process.exitCode = 1; }
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'C:/Users/13070/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright');
|
||||
const base = process.env.DEMO_BASE_URL || 'http://localhost:6688';
|
||||
const uuid = /[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}/i;
|
||||
const code = /^[A-Z]{2}\d{11}$/;
|
||||
const output = path.resolve(__dirname, '../.mimosa/public-id-page-evidence');
|
||||
fs.mkdirSync(output, { recursive: true });
|
||||
(async () => {
|
||||
assert(process.env.DEMO_OPERATOR_PASSWORD, '请通过环境变量提供演示账号密码,不写入测试文件');
|
||||
const browser = await chromium.launch({ channel: 'msedge', headless: true });
|
||||
try {
|
||||
const context = await browser.newContext({ viewport: { width: 1366, height: 768 } });
|
||||
const page = await context.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', error => errors.push(error.message));
|
||||
await page.goto(`${base}/admin/login`);
|
||||
await page.getByPlaceholder('请输入用户名').fill(process.env.DEMO_OPERATOR_USERNAME || 'demo_admin');
|
||||
await page.locator('input[type=password]').fill(process.env.DEMO_OPERATOR_PASSWORD);
|
||||
await page.getByRole('button', { name: '登录管理后台' }).click();
|
||||
await page.waitForURL('**/admin/dashboard');
|
||||
const endpoints = [
|
||||
'/operator/users?page=1&pageSize=20', '/operator/operators', '/operator/activities',
|
||||
'/operator/reviews/pending?page=1&pageSize=20', '/operator/risk/reports/pending',
|
||||
'/operator/matchmaker/management/services', '/operator/matchmaker/recommendations',
|
||||
'/service-appointments/operator/list', '/referrals/operator/history',
|
||||
'/activities/operator/activity-interest-summary',
|
||||
];
|
||||
let checked = 0;
|
||||
const results = [];
|
||||
for (const endpoint of endpoints) {
|
||||
const response = await context.request.get(`${base}/api/v1${endpoint}`);
|
||||
assert(response.ok(), `${endpoint}: HTTP ${response.status()}`);
|
||||
const body = await response.json();
|
||||
assert(body.success, `${endpoint}: ${body.message}`);
|
||||
function inspect(value) {
|
||||
if (!value || typeof value !== 'object') return;
|
||||
for (const [key, child] of Object.entries(value)) {
|
||||
if (/DisplayId$|DisplayNumber$|^displayId$|^displayNumber$/.test(key) && child != null) {
|
||||
assert(code.test(child), `${endpoint}: ${key} 格式错误`); checked++;
|
||||
}
|
||||
if (typeof child === 'object') inspect(child);
|
||||
}
|
||||
}
|
||||
inspect(body.data);
|
||||
results.push({ endpoint, status: response.status() });
|
||||
}
|
||||
assert(checked > 0, '接口没有可验收编号');
|
||||
const pages = [
|
||||
['users', '用户管理'], ['operators', '系统设置'], ['activities', '活动管理'],
|
||||
['review', '资料审核'], ['risk/reports', '举报风控'], ['service-appointment', '服务预约'],
|
||||
['referral', '公益红娘'], ['activities/interests', '匹配管理'], ['matchmaker', '红娘服务'],
|
||||
];
|
||||
for (const [route, label] of pages) {
|
||||
await page.getByText(label, { exact: true }).first().click();
|
||||
await page.waitForTimeout(300);
|
||||
await page.waitForLoadState('networkidle');
|
||||
assert(!page.url().includes('/login'), `${route}: 被退回登录`);
|
||||
const text = await page.locator('body').innerText();
|
||||
assert(!uuid.test(text), `${route}: 可见文本仍有 UUID`);
|
||||
await page.screenshot({ path: path.join(output, `${route.replaceAll('/', '-')}-1366.png`), fullPage: true });
|
||||
if (route === 'matchmaker') {
|
||||
await page.getByRole('tab', { name: '联系授权审批' }).click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
assert(!uuid.test(await page.locator('body').innerText()), '联系授权仍有 UUID');
|
||||
await page.screenshot({ path: path.join(output, 'contact-authorization-1366.png'), fullPage: true });
|
||||
await page.setViewportSize({ width: 1920, height: 1080 });
|
||||
await page.screenshot({ path: path.join(output, 'contact-authorization-1920.png'), fullPage: true });
|
||||
}
|
||||
}
|
||||
assert.equal(errors.length, 0, `页面运行错误: ${errors.join('; ')}`);
|
||||
const anonymous = await browser.newContext();
|
||||
const denied = await anonymous.request.get(`${base}/api/v1/operator/matchmaker/management/services`);
|
||||
assert([401, 403].includes(denied.status()), '未登录不应读取管理服务');
|
||||
fs.writeFileSync(path.join(output, 'results.json'), JSON.stringify({ results, checkedDisplayFields: checked, pages: 10, anonymousStatus: denied.status(), errors }, null, 2));
|
||||
console.log(`PASS: ${results.length} 类真实接口、${checked} 个展示字段、10 个页面/区域、匿名访问拒绝;截图已保存。`);
|
||||
} finally { await browser.close(); }
|
||||
})().catch(error => { console.error(error.message); process.exitCode = 1; });
|
||||
Reference in New Issue
Block a user