yeg
2026-09-08 59dcbfff38ce1e7ed19af509c3bc346deaf5ebc8
tests/e2e/support/ticket04-stack.ts
@@ -9,7 +9,7 @@
}
const webRepo = resolve(import.meta.dirname, '../../..')
const backendRepo = resolve(webRepo, '../BatteryDetection')
const backendRepo = resolve(process.env.TICKET04_BACKEND_REPO || resolve(webRepo, '../BatteryDetection'))
const artifactDir = resolve(webRepo, 'tests/e2e/.artifacts')
const backendJar = resolve(backendRepo, 'ruoyi-admin/target/ruoyi-admin.jar')
const apiPort = Number(process.env.TICKET04_API_PORT || 18001)
@@ -87,7 +87,7 @@
  readonly backendUrl = `http://127.0.0.1:${apiPort}`
  async start() {
  async start(profiles = 'druid,dev-simulator') {
    await Promise.all([mysqlPort, redisPort, apiPort, simulatorPort].map(assertPortAvailable))
    this.docker = resolveDocker()
    mkdirSync(artifactDir, { recursive: true })
@@ -107,7 +107,7 @@
      ])
      await this.waitForContainers()
      this.buildBackend()
      await this.startBackend()
      await this.startBackend(profiles)
      this.disableCaptchaInIsolatedDatabase()
    } catch (error) {
      await this.stop()
@@ -115,9 +115,21 @@
    }
  }
  async restartBackend() {
  async restartBackend(profiles = 'druid,dev-simulator') {
    await this.stopBackend()
    await this.startBackend()
    await this.startBackend(profiles)
  }
  queryDatabase(sql: string) {
    if (!/^SELECT\s/i.test(sql)) throw new Error('验收查询只允许 SELECT')
    return this.dockerRun(['exec', this.mysqlContainer, 'mysql', '-uroot', `-p${mysqlPassword}`,
      '--database=battery_detection', '--batch', '--skip-column-names', '-e', sql]).stdout.trim()
  }
  // 仅操作本次启动的临时容器,用于受控验收数据及事务故障注入。
  executeIsolatedSql(sql: string) {
    return this.dockerRun(['exec', this.mysqlContainer, 'mysql', '-uroot', `-p${mysqlPassword}`,
      '--database=battery_detection', '--default-character-set=utf8mb4', '-e', sql]).stdout
  }
  redisCommand(...args: string[]) {
@@ -156,6 +168,15 @@
    if (!this.docker) return
    this.dockerRun(['rm', '--force', this.mysqlContainer], true)
    this.dockerRun(['rm', '--force', this.redisContainer], true)
    // WSL 的端口转发可能晚于容器退出释放;确认本次端口已释放后才交给下一份验收。
    await waitUntil(async () => {
      try {
        await Promise.all([mysqlPort, redisPort, apiPort, simulatorPort].map(assertPortAvailable))
        return true
      } catch {
        return false
      }
    }, '本次隔离环境的端口未在清理后释放', 30_000)
  }
  private dockerRun(args: string[], allowFailure = false) {
@@ -169,8 +190,13 @@
  }
  private async waitForContainers() {
    await waitUntil(() => {
      const logs = this.dockerRun(['logs', this.mysqlContainer], true)
      return `${logs.stdout}\n${logs.stderr}`.includes('MySQL init process done. Ready for start up.')
    }, '隔离 MySQL 8 初始化未完成')
    await waitUntil(() => this.dockerRun([
      'exec', this.mysqlContainer, 'mysqladmin', 'ping', '-uroot', `-p${mysqlPassword}`, '--silent',
      'exec', this.mysqlContainer, 'mysql', '-uroot', `-p${mysqlPassword}`,
      '--database=battery_detection', '--skip-column-names', '-e', 'SELECT 1',
    ], true).status === 0, '隔离 MySQL 8 未就绪')
    await waitUntil(() => this.dockerRun([
      'exec', this.redisContainer, 'redis-cli', '-a', redisPassword, 'ping',
@@ -184,10 +210,10 @@
    if (!existsSync(backendJar)) throw new Error(`未生成后端可执行包: ${backendJar}`)
  }
  private async startBackend() {
  private async startBackend(profiles = 'druid,dev-simulator') {
    const logPath = resolve(artifactDir, 'backend.log')
    const log = createWriteStream(logPath, { flags: 'a' })
    const jdbcUrl = `jdbc:mysql://127.0.0.1:${mysqlPort}/battery_detection?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8`
    const jdbcUrl = `jdbc:mysql://127.0.0.1:${mysqlPort}/battery_detection?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&connectionTimeZone=UTC&forceConnectionTimeZoneToSession=true`
    this.backend = spawn('java', [
      '-jar', backendJar,
      `--server.port=${apiPort}`,
@@ -198,6 +224,7 @@
      `--spring.data.redis.port=${redisPort}`,
      `--spring.data.redis.password=${redisPassword}`,
      '--spring.data.redis.database=0',
      `--spring.profiles.active=${profiles}`,
      '--spring.devtools.restart.enabled=false',
      `--battery.dev-simulator.port=${simulatorPort}`,
      `--ruoyi.profile=${artifactDir}`,
@@ -212,7 +239,7 @@
    await waitUntil(async () => {
      if (this.backend?.exitCode !== null) throw new Error(`后端提前退出,详见 ${logPath}`)
      try {
        return (await fetch(`${this.backendUrl}/dev/bts-simulator/state`)).ok
        return (await fetch(`${this.backendUrl}/captchaImage`)).ok
      } catch {
        return false
      }