From 298dcdc3eb336865cc4756541b5c6ce0e9bbd37a Mon Sep 17 00:00:00 2001 From: yml2213 Date: Mon, 31 Aug 2026 11:49:51 +0800 Subject: [PATCH] =?UTF-8?q?artifacts=E6=96=87=E4=BB=B6=E5=A4=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + device-limit-artifacts/DIFF_FILE.patch | 229 ------------------------ device-limit-artifacts/MODIFIED_FILE.ts | 140 --------------- device-limit-artifacts/ROLLBACK.sh | 24 --- device-limit-artifacts/VERIFICATION.txt | 64 ------- 5 files changed, 2 insertions(+), 457 deletions(-) delete mode 100644 device-limit-artifacts/DIFF_FILE.patch delete mode 100644 device-limit-artifacts/MODIFIED_FILE.ts delete mode 100755 device-limit-artifacts/ROLLBACK.sh delete mode 100644 device-limit-artifacts/VERIFICATION.txt diff --git a/.gitignore b/.gitignore index af10d20c..75617859 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ send_code/ # 服务器本地备份暂存目录(deploy/backup-db.sh) /backups/ + +artifacts/ \ No newline at end of file diff --git a/device-limit-artifacts/DIFF_FILE.patch b/device-limit-artifacts/DIFF_FILE.patch deleted file mode 100644 index dc0c67a0..00000000 --- a/device-limit-artifacts/DIFF_FILE.patch +++ /dev/null @@ -1,229 +0,0 @@ -diff --git a/apps/backend/scripts/check-sql-guard.ts b/apps/backend/scripts/check-sql-guard.ts -index 859fcb76..504bbb1c 100644 ---- a/apps/backend/scripts/check-sql-guard.ts -+++ b/apps/backend/scripts/check-sql-guard.ts -@@ -4,6 +4,7 @@ - */ - import fs from 'node:fs' - import path from 'node:path' -+import { execFileSync } from 'node:child_process' - import { fileURLToPath } from 'node:url' - - const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)) -@@ -57,6 +58,24 @@ const checks: Array<{ name: string; file: string; patterns: string[] }> = [ - ] - - const failures: string[] = [] -+ -+// 已应用的历史迁移必须保持字节不变,所有结构或数据修复都必须新增迁移文件。 -+try { -+ const modifiedMigrations = execFileSync( -+ 'git', -+ ['diff', '--name-only', '--diff-filter=M', 'HEAD', '--', 'src/db/migrations'], -+ { cwd: BACKEND_ROOT, encoding: 'utf8' }, -+ ) -+ .split('\n') -+ .map((file) => file.trim()) -+ .filter((file) => file.endsWith('.sql')) -+ if (modifiedMigrations.length > 0) { -+ failures.push(`历史迁移文件不可修改: ${modifiedMigrations.join(', ')}`) -+ } -+} catch (error) { -+ failures.push(`历史迁移完整性检查失败: ${error instanceof Error ? error.message : String(error)}`) -+} -+ - for (const check of checks) { - if (!fs.existsSync(check.file)) { - failures.push(`${check.name}: 文件不存在 ${check.file}`) -diff --git a/apps/backend/src/db/migrations/069_worker_session_device_type.sql b/apps/backend/src/db/migrations/069_worker_session_device_type.sql -new file mode 100644 -index 00000000..033b921e ---- /dev/null -+++ b/apps/backend/src/db/migrations/069_worker_session_device_type.sql -@@ -0,0 +1,8 @@ -+-- 将历史平板会话归入手机设备桶,确保新旧数据使用同一套 PC/手机上限。 -+UPDATE worker_sessions -+SET device_type = 'mobile', updated_at = NOW() -+WHERE device_type = 'tablet'; -+ -+UPDATE worker_sessions -+SET device_type = 'pc', updated_at = NOW() -+WHERE device_type IS NULL OR device_type NOT IN ('pc', 'mobile'); -diff --git a/apps/backend/src/repositories/worker-platform/worker-session-repo.ts b/apps/backend/src/repositories/worker-platform/worker-session-repo.ts -index c59eb71d..fa1eaf1b 100644 ---- a/apps/backend/src/repositories/worker-platform/worker-session-repo.ts -+++ b/apps/backend/src/repositories/worker-platform/worker-session-repo.ts -@@ -41,9 +41,9 @@ export async function createWorkerSessionRecord( - ) - const activeResult = await client.query( - `SELECT * FROM worker_sessions -- WHERE worker_id = $1 AND status = 'active' AND expires_at > $2 -+ WHERE worker_id = $1 AND device_type = $2 AND status = 'active' AND expires_at > $3 - ORDER BY last_seen_at DESC, id DESC`, -- [input.workerId, input.now], -+ [input.workerId, input.deviceType, input.now], - ) - const activeSessions = activeResult.rows - if (activeSessions.length >= input.maxDevices) { -diff --git a/apps/backend/src/services/worker-platform/worker-auth-policy.test.ts b/apps/backend/src/services/worker-platform/worker-auth-policy.test.ts -new file mode 100644 -index 00000000..2da1ff5e ---- /dev/null -+++ b/apps/backend/src/services/worker-platform/worker-auth-policy.test.ts -@@ -0,0 +1,11 @@ -+import assert from 'node:assert/strict' -+import test from 'node:test' -+ -+import { normalizeWorkerDeviceType } from './worker-auth-policy.js' -+ -+test('worker device types use separate PC and mobile buckets', () => { -+ assert.equal(normalizeWorkerDeviceType('pc'), 'pc') -+ assert.equal(normalizeWorkerDeviceType('desktop'), 'pc') -+ assert.equal(normalizeWorkerDeviceType('mobile'), 'mobile') -+ assert.equal(normalizeWorkerDeviceType('tablet'), 'mobile') -+}) -diff --git a/apps/backend/src/services/worker-platform/worker-auth-policy.ts b/apps/backend/src/services/worker-platform/worker-auth-policy.ts -index 2698a7b4..11444cf5 100644 ---- a/apps/backend/src/services/worker-platform/worker-auth-policy.ts -+++ b/apps/backend/src/services/worker-platform/worker-auth-policy.ts -@@ -28,7 +28,7 @@ export function normalizeWorkerDeviceType(value: unknown): string { - const type = String(value || '') - .trim() - .toLowerCase() -- return type === 'mobile' || type === 'tablet' ? type : 'pc' -+ return type === 'mobile' || type === 'tablet' ? 'mobile' : 'pc' - } - - export function normalizeWorkerDeviceId(value: unknown, fallback: string): string { -diff --git a/apps/backend/src/services/worker-platform/worker-login-service.ts b/apps/backend/src/services/worker-platform/worker-login-service.ts -index c6de2243..36ec08fd 100644 ---- a/apps/backend/src/services/worker-platform/worker-login-service.ts -+++ b/apps/backend/src/services/worker-platform/worker-login-service.ts -@@ -141,10 +141,14 @@ async function createWorkerLoginSession( - maxDevices: WORKER_MAX_DEVICES, - }) - if (!sessionRecord.created) { -- throw createHttpError('在线设备已达到 3 台,请先删除旧的登录设备', { -- statusCode: 409, -- errorCode: 'worker_device_limit', -- }) -+ const deviceLabel = deviceType === 'pc' ? 'PC' : '手机' -+ throw createHttpError( -+ `${deviceLabel}设备已达到 ${WORKER_MAX_DEVICES} 台,请先删除旧的登录设备`, -+ { -+ statusCode: 409, -+ errorCode: 'worker_device_limit', -+ }, -+ ) - } - return { ...session, passwordWeak } - } -diff --git a/apps/backend/src/services/worker-platform/worker-session-auth-service.ts b/apps/backend/src/services/worker-platform/worker-session-auth-service.ts -index f7e23119..7e69cbe1 100644 ---- a/apps/backend/src/services/worker-platform/worker-session-auth-service.ts -+++ b/apps/backend/src/services/worker-platform/worker-session-auth-service.ts -@@ -135,8 +135,18 @@ export async function verifyWorkerSessionToken(token: unknown): Promise { -+ const deviceType = item.device_type === 'mobile' ? 'mobile' : 'pc' -+ counts[deviceType] += 1 -+ return counts -+ }, -+ { pc: 0, mobile: 0 }, -+ ) - return { - maxDevices: WORKER_MAX_DEVICES, -+ maxDevicesByType: { pc: WORKER_MAX_DEVICES, mobile: WORKER_MAX_DEVICES }, -+ deviceCounts, - items: sessions.map((item) => mapWorkerSession(item, session.sessionId)), - } - } -diff --git a/apps/frontend/src/pages/worker/WorkerLoginPage.tsx b/apps/frontend/src/pages/worker/WorkerLoginPage.tsx -index e525a32c..c9daceac 100644 ---- a/apps/frontend/src/pages/worker/WorkerLoginPage.tsx -+++ b/apps/frontend/src/pages/worker/WorkerLoginPage.tsx -@@ -662,7 +662,7 @@ export default function WorkerLoginPage() { - - void - devices: WorkerSessionDevice[] - maxDevices: number -+ deviceCounts: { pc: number; mobile: number } - devicesLoading: boolean - removeWorkerDevice: (device: WorkerSessionDevice) => Promise - passwordOpen: boolean -@@ -88,7 +90,7 @@ export function WorkerProfileAccountModals({ - - - size="small" -diff --git a/apps/frontend/src/pages/worker/WorkerProfilePage.tsx b/apps/frontend/src/pages/worker/WorkerProfilePage.tsx -index 5641b936..67231aa4 100644 ---- a/apps/frontend/src/pages/worker/WorkerProfilePage.tsx -+++ b/apps/frontend/src/pages/worker/WorkerProfilePage.tsx -@@ -658,6 +658,7 @@ export default function WorkerProfilePage() { - setDevicesOpen={setDevicesOpen} - devices={devicesQuery.data?.data.items || []} - maxDevices={devicesQuery.data?.data.maxDevices || 3} -+ deviceCounts={devicesQuery.data?.data.deviceCounts || { pc: 0, mobile: 0 }} - devicesLoading={devicesQuery.isLoading || devicesQuery.isFetching} - removeWorkerDevice={removeWorkerDevice} - passwordOpen={passwordOpen} -diff --git a/apps/frontend/src/services/worker.ts b/apps/frontend/src/services/worker.ts -index 162ab6d1..1197a9ac 100644 ---- a/apps/frontend/src/services/worker.ts -+++ b/apps/frontend/src/services/worker.ts -@@ -94,7 +94,7 @@ export function loginWorkerBySmsCode(payload: { phone: string; code: string }) { - } - - export function fetchWorkerSessions() { -- return apiGet<{ maxDevices: number; items: WorkerSessionDevice[] }>('/api/v1/worker/auth/devices') -+ return apiGet('/api/v1/worker/auth/devices') - } - - export function deleteWorkerSession(sessionId: string) { -@@ -102,10 +102,14 @@ export function deleteWorkerSession(sessionId: string) { - } - - export function manageWorkerSessions(payload: { username: string; password: string }) { -- return apiPost<{ maxDevices: number; items: WorkerSessionDevice[] }>( -- '/api/v1/worker/auth/devices/manage/list', -- payload, -- ) -+ return apiPost('/api/v1/worker/auth/devices/manage/list', payload) -+} -+ -+export type WorkerDeviceLimitsResponse = { -+ maxDevices: number -+ maxDevicesByType: { pc: number; mobile: number } -+ deviceCounts: { pc: number; mobile: number } -+ items: WorkerSessionDevice[] - } - - export function deleteWorkerSessionWithCredentials(payload: { diff --git a/device-limit-artifacts/MODIFIED_FILE.ts b/device-limit-artifacts/MODIFIED_FILE.ts deleted file mode 100644 index fa1eaf1b..00000000 --- a/device-limit-artifacts/MODIFIED_FILE.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { query, withTransaction } from '../../db/client.js' -import type { WorkerSessionRow } from './types.js' - -export type CreateWorkerSessionInput = { - sessionId: string - workerId: number - deviceId: string - deviceType: string - deviceName: string - userAgent: string - ipAddress: string - issuedAt: string - expiresAt: string - now: string - maxDevices: number -} - -export type CreateWorkerSessionResult = { - created: boolean - activeSessions: WorkerSessionRow[] - session: WorkerSessionRow | null -} - -export async function createWorkerSessionRecord( - input: CreateWorkerSessionInput, -): Promise { - return withTransaction(async (client) => { - // 对账号加行锁,避免并发登录同时绕过设备数量限制。 - await client.query('SELECT id FROM worker_users WHERE id = $1 FOR UPDATE', [input.workerId]) - await client.query( - `UPDATE worker_sessions - SET status = 'revoked', revoked_at = $2, updated_at = $2 - WHERE worker_id = $1 AND status = 'active' AND expires_at <= $2`, - [input.workerId, input.now], - ) - await client.query( - `UPDATE worker_sessions - SET status = 'revoked', revoked_at = $3, updated_at = $3 - WHERE worker_id = $1 AND device_id = $2 AND status = 'active'`, - [input.workerId, input.deviceId, input.now], - ) - const activeResult = await client.query( - `SELECT * FROM worker_sessions - WHERE worker_id = $1 AND device_type = $2 AND status = 'active' AND expires_at > $3 - ORDER BY last_seen_at DESC, id DESC`, - [input.workerId, input.deviceType, input.now], - ) - const activeSessions = activeResult.rows - if (activeSessions.length >= input.maxDevices) { - return { created: false, activeSessions, session: null } - } - const result = await client.query( - `INSERT INTO worker_sessions ( - session_id, worker_id, device_id, device_type, device_name, - user_agent, ip_address, status, issued_at, last_seen_at, - expires_at, created_at, updated_at - ) VALUES ($1, $2, $3, $4, $5, $6, $7, 'active', $8, $8, $9, $10, $10) - RETURNING *`, - [ - input.sessionId, - input.workerId, - input.deviceId, - input.deviceType, - input.deviceName, - input.userAgent, - input.ipAddress, - input.issuedAt, - input.expiresAt, - input.now, - ], - ) - return { created: true, activeSessions, session: result.rows[0] || null } - }) -} - -export async function getWorkerSessionBySessionId( - sessionId: string, -): Promise { - const result = await query( - 'SELECT * FROM worker_sessions WHERE session_id = $1 LIMIT 1', - [String(sessionId || '').trim()], - ) - return result.rows[0] || null -} - -export async function touchWorkerSession(sessionId: string, now: string): Promise { - const result = await query( - `UPDATE worker_sessions - SET last_seen_at = $2, updated_at = $2 - WHERE session_id = $1 AND status = 'active' AND expires_at > $2`, - [String(sessionId || '').trim(), now], - ) - return (result.rowCount || 0) > 0 -} - -export async function revokeWorkerSession( - workerId: number | string, - sessionId: string, - now: string, -): Promise { - const result = await query( - `UPDATE worker_sessions - SET status = 'revoked', revoked_at = $3, updated_at = $3 - WHERE worker_id = $1 AND session_id = $2 AND status = 'active'`, - [Number(workerId), String(sessionId || '').trim(), now], - ) - return (result.rowCount || 0) > 0 -} - -export async function revokeAllWorkerSessions( - workerId: number | string, - now: string, -): Promise { - const result = await query( - `UPDATE worker_sessions - SET status = 'revoked', revoked_at = $2, updated_at = $2 - WHERE worker_id = $1 AND status = 'active'`, - [Number(workerId), now], - ) - return result.rowCount || 0 -} - -export async function listWorkerSessions( - workerId: number | string, - now: string, -): Promise { - await query( - `UPDATE worker_sessions - SET status = 'revoked', revoked_at = $2, updated_at = $2 - WHERE worker_id = $1 AND status = 'active' AND expires_at <= $2`, - [Number(workerId), now], - ) - const result = await query( - `SELECT * FROM worker_sessions - WHERE worker_id = $1 AND status = 'active' AND expires_at > $2 - ORDER BY device_type ASC, last_seen_at DESC, id DESC`, - [Number(workerId), now], - ) - return result.rows -} diff --git a/device-limit-artifacts/ROLLBACK.sh b/device-limit-artifacts/ROLLBACK.sh deleted file mode 100755 index 8bd02576..00000000 --- a/device-limit-artifacts/ROLLBACK.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="${1:?usage: ROLLBACK.sh }" -SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" - -while IFS= read -r relative_path; do - mkdir -p "$ROOT/$(dirname "$relative_path")" - cp "$SCRIPT_DIR/baseline-copy/$relative_path" "$ROOT/$relative_path" -done <<'PATHS' -apps/backend/src/db/migrations/029_worker_sessions.sql -apps/backend/src/repositories/worker-platform/worker-session-repo.ts -apps/backend/src/services/worker-platform/worker-auth-policy.ts -apps/backend/src/services/worker-platform/worker-login-service.ts -apps/backend/src/services/worker-platform/worker-session-auth-service.ts -apps/frontend/src/pages/worker/WorkerLoginPage.tsx -apps/frontend/src/pages/worker/WorkerProfileAccountModals.tsx -apps/frontend/src/pages/worker/WorkerProfilePage.tsx -apps/frontend/src/services/worker.ts -PATHS - -rm -f "$ROOT/apps/backend/src/db/migrations/069_worker_session_device_type.sql" -rm -f "$ROOT/apps/backend/src/services/worker-platform/worker-auth-policy.test.ts" -printf 'restored device-limit baseline in %s\n' "$ROOT" diff --git a/device-limit-artifacts/VERIFICATION.txt b/device-limit-artifacts/VERIFICATION.txt deleted file mode 100644 index 552aae8c..00000000 --- a/device-limit-artifacts/VERIFICATION.txt +++ /dev/null @@ -1,64 +0,0 @@ -Worker login device limit verification - -Changed branch/fields: -- worker_sessions device_type bucket: pc and mobile each allow at most 3 active devices -- tablet input and historical tablet rows normalize to mobile -- SQL guard rejects modifications to tracked historical migration files -- worker device APIs expose maxDevicesByType and deviceCounts -- worker login/device-management UI states separate PC and phone quotas - -Artifacts: -- MODIFIED_FILE: /Users/yml/codes/order_site/device-limit-artifacts/MODIFIED_FILE.ts -- DIFF_FILE: /Users/yml/codes/order_site/device-limit-artifacts/DIFF_FILE.patch -- VERIFICATION.txt: /Users/yml/codes/order_site/device-limit-artifacts/VERIFICATION.txt -- ROLLBACK.sh: /Users/yml/codes/order_site/device-limit-artifacts/ROLLBACK.sh - -Source paths changed: -- /Users/yml/codes/order_site/apps/backend/src/repositories/worker-platform/worker-session-repo.ts -- /Users/yml/codes/order_site/apps/backend/src/services/worker-platform/worker-auth-policy.ts -- /Users/yml/codes/order_site/apps/backend/src/services/worker-platform/worker-login-service.ts -- /Users/yml/codes/order_site/apps/backend/src/services/worker-platform/worker-session-auth-service.ts -- /Users/yml/codes/order_site/apps/backend/src/db/migrations/069_worker_session_device_type.sql -- /Users/yml/codes/order_site/apps/backend/scripts/check-sql-guard.ts -- /Users/yml/codes/order_site/apps/backend/src/services/worker-platform/worker-auth-policy.test.ts -- /Users/yml/codes/order_site/apps/frontend/src/services/worker.ts -- /Users/yml/codes/order_site/apps/frontend/src/pages/worker/WorkerLoginPage.tsx -- /Users/yml/codes/order_site/apps/frontend/src/pages/worker/WorkerProfileAccountModals.tsx -- /Users/yml/codes/order_site/apps/frontend/src/pages/worker/WorkerProfilePage.tsx - -BASELINE -Command: npm test -Working directory: /tmp/order_site_device_baseline_0830/apps/backend -Input: detached HEAD ffb71f22 source before device-limit changes -Literal result: tests 378; pass 376; fail 0; skipped 2; exit status 0 - -Command: npm test -Working directory: /tmp/order_site_device_baseline_0830/apps/frontend -Input: detached HEAD ffb71f22 source before device-limit changes -Literal result: tests 15; pass 15; fail 0; skipped 0; exit status 0 - -Baseline worker-session-repo SHA-256: 9136d67c8f5cb3641979f8e5b577d9f4bd44f0b399862d2feaa69f2002d19ef1 - -MODIFIED -Command: npm run check:sql -Working directory: /Users/yml/codes/order_site/apps/backend -Input: current tree with immutable historical migration guard enabled -Literal result: [sql-guard] SQL 约束检查通过; exit status 0 - -Command: npm run format:check && npm run lint:check && npm run typecheck && npm test && npm run build -Working directory: /Users/yml/codes/order_site/apps/backend -Input: modified per-device session quota implementation and migration -Literal result: format passed; lint passed; typecheck passed; tests 379; pass 377; fail 0; skipped 2; build completed; exit status 0 - -Command: npm run format:check && npm run lint:check && npm run typecheck && npm test && npm run build -Working directory: /Users/yml/codes/order_site/apps/frontend -Input: modified per-device quota API types and management UI -Literal result: format passed; lint passed; typecheck passed; tests 15; pass 15; fail 0; skipped 0; build completed; exit status 0 - -Modified MODIFIED_FILE SHA-256: 33147b25bce759bc1af079f495d347196fda003e74f280dd41e1a194c75ff443 - -ROLLBACK -Command: device-limit-artifacts/ROLLBACK.sh device-limit-artifacts/rollback-copy-0830 -Input: independent copy containing modified backend/frontend files, migration, and regression test -Literal result: restored device-limit baseline in device-limit-artifacts/rollback-copy-0830; worker-session-repo SHA-256 restored to 9136d67c8f5cb3641979f8e5b577d9f4bd44f0b399862d2feaa69f2002d19ef1; new migration and regression test removed; exit status 0 -Restored behavior/status: rollback copy uses the original aggregate device-limit query and UI wording; working source and MODIFIED_FILE.ts remain changed.