import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; const root = new URL('..', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1'); const read = (path) => readFileSync(join(root, path), 'utf8'); const app = read('admin/src/App.vue'); for (const pattern of [ 'OperationsOverviewPanel', "activeModule = 'overview'", "activeModule = 'cleaning'", "activeModule = 'stores'", "activeModule = 'orders'", "activeModule = 'payments'", "activeModule = 'thirdParty'", "activeModule = 'people'", "activeModule = 'devices'", "activeModule = 'platformApps'", "activeModule = 'content'", "activeModule = 'franchise'", "activeModule = 'system'", '平台运营总览', 'StoresRoomsPanel', 'OrdersPanel', 'PaymentsPanel', 'ThirdPartyPanel', 'MembersStaffPanel', 'DevicesPanel', 'PlatformAppsPanel', 'ContentManagementPanel', 'FranchisePanel', 'LogsSystemPanel', 'AdminLoginPanel', '运营总览', 'savedToken', 'handleAuthenticated', 'handleLogout', 'loadCleaningWorkspace' ]) { assert.match(app, new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); } const overview = read('admin/src/components/OperationsOverviewPanel.vue'); for (const pattern of [ 'listManagedStores', 'getBusinessStatistics', '近 7 日', '近 30 日', '近 90 日', '成功实收', '预订金额', '下单会员', '验券成功', '当前房态', '支付渠道', '每日收入趋势', '核心业务导航', 'open-cleaning', '后续增量', 'collectedAmountCents', 'availableRoomTotal', 'paymentChannels', 'dailyRevenue', 'errorMessage' ]) { assert.match(overview, new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); } const api = read('admin/src/api.ts'); assert.match(api, /request\(session, '\/stores'\)/); assert.match(api, /request\(session, `\/statistics\?\$\{params\}`\)/); for (const pattern of [ 'createManagedStore', 'updateManagedStore', 'archiveManagedStore', 'listManagedRooms', 'createManagedRoom', 'updateManagedRoom', 'updateManagedRoomStatus', 'addManagedRoomDisabledPeriod' ]) { assert.match(api, new RegExp(pattern)); } const stores = read('admin/src/components/StoresRoomsPanel.vue'); for (const pattern of [ '门店与房间管理', '搜索门店、城市或地址', '每周营业时间', 'wifiPassword', 'listManagedStores', 'listManagedRooms', 'updateManagedRoomStatus', '新增房间', '停用时段', 'configurationStatus', 'operationalStatus', 'archiveManagedRoom' ]) { assert.match(stores, new RegExp(pattern)); } const orders = read('admin/src/components/OrdersPanel.vue'); for (const pattern of [ '订单筛选与处置', 'listManagedOrders', 'getManagedOrder', 'listManagedOrderHistory', 'executeManagedOrderAction', 'addManagedOrderNote', 'adjustManagedOrderTime', 'renewManagedOrder', 'changeManagedOrderRoom', '导出当前页', '状态流水', 'allowedActions' ]) { assert.match(orders, new RegExp(pattern)); } const payments = read('admin/src/components/PaymentsPanel.vue'); for (const pattern of [ '支付、退款与分账', 'getProfitSharingSnapshot', 'saveCollectionAccount', 'saveProfitShareReceiver', 'saveProfitSharePolicy', 'createWechatRefund', 'requestWechatReconciliation', 'executeProfitSharing', '凭据只保存环境引用', 'clientRequestId', '生产 API', '非生产 Mock' ]) { assert.match(payments, new RegExp(pattern)); } const thirdParty = read('admin/src/components/ThirdPartyPanel.vue'); for (const pattern of [ 'listThirdPartyRecords', 'getThirdPartySetup', 'redeemGroupVoucher', 'redeemGroupVoucherManually', 'saveThirdPartyConfig', 'saveThirdPartyMapping', 'voucherCode', 'PENDING_MAPPING' ]) { assert.match(thirdParty, new RegExp(pattern)); } const thirdPartyRoutes = read('backend/src/routes/third-party.ts'); assert.match(thirdPartyRoutes, /'\/admin-api\/third-party\/setup'/); assert.match(thirdPartyRoutes, /'\/admin-api\/group-vouchers\/redeem'/); const people = read('admin/src/components/MembersStaffPanel.vue'); for (const pattern of [ 'listMembers', 'getMember', 'listStaffUsers', 'createStaffUser', 'updateStaffUser', 'resetStaffSessions', 'memberDetail', 'staffForm' ]) { assert.match(people, new RegExp(pattern)); } const userRoutes = read('backend/src/routes/user-management.ts'); assert.match(userRoutes, /userType: z\.enum\(\['STAFF'\]\)\.optional\(\)/); const devices = read('admin/src/components/DevicesPanel.vue'); for (const pattern of [ 'getDeviceTopology', 'createDeviceAsset', 'bindDeviceChannel', 'bindSubDevice', 'addDeviceMaintenance', 'controlDevice', 'openAlerts', 'Sub‑1G' ]) { assert.match(devices, new RegExp(pattern)); } const platformApps = read('admin/src/components/PlatformAppsPanel.vue'); for (const pattern of ['listPlatformApplications', 'updatePlatformApplicationConfig', 'bindPlatformApplication', 'AppSecret', 'extraConfigText']) { assert.match(platformApps, new RegExp(pattern)); } const platformRoutes = read('backend/src/routes/platform-management.ts'); assert.match(platformRoutes, /'\/admin-api\/platform-apps'/); assert.match(platformRoutes, /PLATFORM_MANAGEMENT_FORBIDDEN/); const content = read('admin/src/components/ContentManagementPanel.vue'); for (const pattern of [ 'listMediaAssets', 'uploadMediaImage', 'listDecorations', 'saveDecoration', 'publishDecoration', 'listAdvertisements', 'saveAdvertisement', 'updateAdvertisement', '媒体素材', '门店装修', '广告投放' ]) { assert.match(content, new RegExp(pattern)); } const contentRoutes = read('backend/src/routes/content-management.ts'); assert.match(contentRoutes, /'\/admin-api\/media\/images'/); assert.match(contentRoutes, /'\/admin-api\/decorations'/); assert.match(contentRoutes, /'\/admin-api\/advertisements\/:id'/); const franchise = read('admin/src/components/FranchisePanel.vue'); for (const pattern of ['listFranchiseApplications', 'getFranchiseApplication', 'assignFranchiseApplication', 'addFranchiseFollowUp', '加盟申请与跟进队列', '跟进历史']) { assert.match(franchise, new RegExp(pattern)); } const franchiseRoutes = read('backend/src/routes/franchise.ts'); assert.match(franchiseRoutes, /'\/app-api\/franchise-applications'/); assert.match(franchiseRoutes, /'\/admin-api\/franchise-applications\/:id\/follow-ups'/); const systemPanel = read('admin/src/components/LogsSystemPanel.vue'); for (const pattern of ['listAuditLogs', 'getSystemOverview', 'updateTenantSystemConfig', '安全审计日志', '导出当前页', 'Schema 迁移', '脱敏元数据']) { assert.match(systemPanel, new RegExp(pattern)); } const systemRoutes = read('backend/src/routes/system-operations.ts'); assert.match(systemRoutes, /'\/admin-api\/audit-logs'/); assert.match(systemRoutes, /'\/admin-api\/system\/overview'/); assert.match(systemRoutes, /'\/admin-api\/system\/tenant'/); const systemRepository = read('backend/src/operations/system-operations-repository.ts'); assert.match(systemRepository, /sanitizeMetadata/); assert.match(systemRepository, /TENANT_SYSTEM_CONFIG_UPDATED/); const adminLogin = read('admin/src/components/AdminLoginPanel.vue'); for (const pattern of ['adminPasswordLogin', '租户代码', '安全登录', '连续失败 5 次']) { assert.match(adminLogin, new RegExp(pattern)); } const adminAuthRoutes = read('backend/src/routes/admin-auth.ts'); for (const pattern of ['/admin-api/auth/login', '/admin-api/auth/refresh', '/admin-api/auth/me', '/admin-api/auth/logout', '/admin-api/auth/credentials/:userId']) { assert.match(adminAuthRoutes, new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); } const adminAuthRepository = read('backend/src/auth/admin-auth-repository.ts'); assert.match(adminAuthRepository, /ADMIN_LOGIN_SUCCEEDED/); assert.match(adminAuthRepository, /ADMIN_LOGIN_FAILED/); assert.match(adminAuthRepository, /ADMIN_REFRESH_REUSED/); const adminAuthMigration = read('database/migrations/2026081004_m08d_admin_password_auth.up.sql'); assert.match(adminAuthMigration, /qipai_admin_credentials/); assert.match(adminAuthMigration, /refresh_token_hash/); const routes = read('backend/src/routes/business-statistics.ts'); assert.match(routes, /'\/app-api\/management\/statistics', '\/admin-api\/statistics'/); const routeTest = read('backend/tests/business-statistics.test.mjs'); assert.match(routeTest, /url: '\/admin-api\/statistics\?/); const storeRepository = read('backend/src/stores/store-room-repository.ts'); assert.match(storeRepository, /wifi_password = COALESCE\(\?, wifi_password\)/); assert.match(storeRepository, /FROM qipai_store_business_hours/); assert.match(storeRepository, /wifiConfigured/); const orderQueryRoutes = read('backend/src/routes/order-query.ts'); assert.match(orderQueryRoutes, /'\/admin-api\/orders'/); const orderStateRoutes = read('backend/src/routes/order-state.ts'); assert.match(orderStateRoutes, /'\/admin-api\/orders\/:orderId\/history'/); const styles = read('admin/src/styles.css'); for (const pattern of [ '.overview-page', '.overview-toolbar', '.overview-metrics', '.overview-grid', '.revenue-bars', '.module-grid', '.store-layout', '.store-card', '.hours-editor', '.order-page-summary', '.history-card', '.payment-metrics', '.payment-tools-grid', '.third-party-metrics', '.settings-preview', '.people-metrics', '.member-detail-grid', '.device-metrics', '.device-topology-grid', '.platform-app-metrics', '.content-metrics', '.asset-gallery', '.component-editor', '.franchise-metrics', '.system-metrics', '.audit-filters', '.login-shell', '.session-box', '@media (max-width: 980px)', '@media (max-width: 560px)' ]) { assert.match(styles, new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); } console.log('PASS: M08-D admin operations overview and scoped business statistics are present.');