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

This commit is contained in:
WorkBuddy
2026-10-15 09:47:31 +08:00
parent 827c23db1d
commit 3c2b967274
149 changed files with 4324 additions and 348 deletions
+35
View File
@@ -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; });
+1 -1
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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()
+30
View File
@@ -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()
+60
View File
@@ -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;
}
+33
View File
@@ -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; }
});
+79
View File
@@ -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; });