#!/usr/bin/env bash # S01 第四轮 F6 —— sms_verification_codes 表结构契约门禁 # # 目的:防止冒烟脚本再次使用**不存在的列**(F6:expires_at), # 且不允许测试自己维护一份与生产无关的假字段列表来自证正确。 # # 契约来源(本脚本同时使用前两个,并交叉校验二者一致): # 1. 实体 @Column:mydreamjob-server/.../auth/SmsVerificationCode.java # 2. 建表 Flyway:mydreamjob-server/.../db/migration/V1__base_schema.sql # 3. 真实 MySQL SHOW COLUMNS —— 属**真实验收阶段**,本脚本只输出对账命令, # 不在自动化阶段伪造该来源。 # # 重要:expires_at 是 personal_data_export_tasks(V30) / ability_attempts(V35) / # talent_invitations(V40) 三张表的**真实列**。因此本门禁只对 # sms_verification_codes 这一个表断言"不存在 expires_at", # 不得写成全库禁令(那会误报,并掩盖真正的字段错误)。 # # 运行: bash docs/tests/smoke-test-schema-contract-test.sh set -u REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" SMOKE="$REPO_ROOT/docs/smoke-test.sh" ENTITY="$REPO_ROOT/mydreamjob-server/src/main/java/com/mydreamjob/auth/SmsVerificationCode.java" MIGRATION_DIR="$REPO_ROOT/mydreamjob-server/src/main/resources/db/migration" FLYWAY="$MIGRATION_DIR/V1__base_schema.sql" TABLE="sms_verification_codes" PASS=0 FAIL=0 ok() { echo "[PASS] $1"; PASS=$((PASS + 1)); } bad() { echo "[FAIL] $1"; FAIL=$((FAIL + 1)); } check() { if [ "$1" = "1" ]; then ok "$2"; else bad "$2"; fi; } echo "=== S01 F6: sms_verification_codes 表结构契约门禁 ===" echo "实体来源 : $ENTITY" echo "Flyway来源: $FLYWAY" echo "脚本 : $SMOKE" echo "" # ── 契约来源 1:实体 @Column ────────────────────────────────── entity_cols="" if [ -f "$ENTITY" ]; then # 主键 id 在实体中以 @Id 标注(无 @Column(name=...)),单独补入 grep -qE '^[[:space:]]*@Id[[:space:]]*$' "$ENTITY" && entity_cols="id" entity_cols="$entity_cols $(grep -oE '@Column\(name[[:space:]]*=[[:space:]]*"[A-Za-z0-9_]+"' "$ENTITY" \ | sed 's/.*"\(.*\)"/\1/')" entity_cols="$(printf '%s' "$entity_cols" | tr ' ' '\n' | grep -v '^$' | sort -u | tr '\n' ' ')" fi # ── 契约来源 2:建表 Flyway ─────────────────────────────────── flyway_cols="" if [ -f "$FLYWAY" ]; then flyway_cols="$(awk -v t="$TABLE" ' $0 ~ "^CREATE TABLE (IF NOT EXISTS )?" t "([[:space:]]|\\()" { in_t=1; next } in_t && /^\)/ { in_t=0 } in_t { line=$0 sub(/^[[:space:]]+/, "", line) if (line ~ /^(INDEX|KEY|PRIMARY|CONSTRAINT|UNIQUE|FOREIGN)/) next if (line ~ /^[A-Za-z0-9_]+[[:space:]]/) { split(line, a, /[[:space:]]/); print a[1] } } ' "$FLYWAY" | sort -u | tr '\n' ' ')" fi check "$([ -f "$ENTITY" ] && echo 1 || echo 0)" "C1. 实体文件存在" check "$([ -f "$FLYWAY" ] && echo 1 || echo 0)" "C2. 建表 Flyway 存在" check "$([ -n "$entity_cols" ] && echo 1 || echo 0)" "C3. 实体 @Column 解析出非空列集合" check "$([ -n "$flyway_cols" ] && echo 1 || echo 0)" "C4. Flyway 解析出非空列集合" # 两个独立来源必须一致(否则说明解析失败或生产定义本身不一致) check "$([ "$(printf '%s' "$entity_cols" | tr ' ' '\n' | grep -c .)" -gt 0 ] \ && [ "$entity_cols" = "$flyway_cols" ] && echo 1 || echo 0)" \ "C5. 实体 @Column 与建表 Flyway 的列集合一致" echo " 实体列 : ${entity_cols:-<空>}" echo " Flyway列: ${flyway_cols:-<空>}" echo "" # ── 字段存在性(表限定)─────────────────────────────────────── check "$(case " $flyway_cols " in *" expired_at "*) echo 1 ;; *) echo 0 ;; esac)" \ "C6. sms_verification_codes 存在 expired_at 列" check "$(case " $flyway_cols " in *" expires_at "*) echo 0 ;; *) echo 1 ;; esac)" \ "C7. sms_verification_codes **不存在** expires_at 列" check "$(case " $entity_cols " in *" expired_at "*) echo 1 ;; *) echo 0 ;; esac)" \ "C8. 实体 expired_at 映射存在" # ── 反向护栏:expires_at 确实是别的表的真实列 ────────────────── # 若此条失败,说明有人把它当成了"全库不存在的列"——那会让本门禁退化成误报。 other_tables_with_expires_at=0 for f in "$MIGRATION_DIR"/V30__personal_data_export_tasks.sql \ "$MIGRATION_DIR"/V35__ability_assessment.sql \ "$MIGRATION_DIR"/V40__enterprise_talent_invitations.sql; do [ -f "$f" ] || continue grep -qE '^[[:space:]]*expires_at[[:space:]]' "$f" && other_tables_with_expires_at=$((other_tables_with_expires_at + 1)) done check "$([ "$other_tables_with_expires_at" -gt 0 ] && echo 1 || echo 0)" \ "C9. expires_at 确实是其他表的真实列(故门禁必须按表限定,不得全局禁用)" # ── 脚本侧:表名与列名 ──────────────────────────────────────── sql_block="$(awk '/SELECT code FROM sms_verification_codes/,/LIMIT 1;/' "$SMOKE" 2>/dev/null)" # 只保留双引号内的 SQL 片段,剥掉 shell 语法(printf / 引号 / 续行反斜杠), # 否则 printf、%s 之类会被误当成列名。 sql_text="$(printf '%s' "$sql_block" | grep -oE '"[^"]*"' | tr -d '"' | tr '\n' ' ')" check "$([ -n "$sql_text" ] && echo 1 || echo 0)" "C10. 能从 smoke-test.sh 提取取码 SQL 文本" check "$(printf '%s' "$sql_text" | grep -q "FROM $TABLE" && echo 1 || echo 0)" \ "C11. 取码 SQL 查询表为 $TABLE" check "$(printf '%s' "$sql_text" | grep -q 'expired_at' && echo 1 || echo 0)" \ "C12. 取码 SQL 使用 expired_at" check "$(printf '%s' "$sql_text" | grep -q 'expires_at' && echo 0 || echo 1)" \ "C13. 取码 SQL **不含** expires_at" # 脚本 SQL 使用的每个列名都必须落在真实列集合内 used_cols="$(printf '%s' "$sql_text" \ | sed "s/'[^']*'//g" \ | grep -oE '\b[A-Za-z_][A-Za-z0-9_]*\b' \ | grep -viE '^(SELECT|code|FROM|WHERE|AND|OR|ORDER|BY|DESC|ASC|LIMIT|NOW)$|^'$TABLE'$' \ | sort -u | tr '\n' ' ')" unknown="" for c in $used_cols; do case " $flyway_cols " in *" $c "*) ;; *) unknown="$unknown $c" ;; esac done check "$([ -z "$unknown" ] && echo 1 || echo 0)" \ "C14. 取码 SQL 使用的列全部存在于 $TABLE${unknown:+(不存在的列:$unknown)}" # 取码列 code 必须在真实列集合内(正向确认解析没跑偏) check "$(case " $flyway_cols " in *" code "*) echo 1 ;; *) echo 0 ;; esac)" \ "C15. 取码 SQL 的列 code 是 $TABLE 的真实列" echo "" echo "── 真实验收阶段必须执行(本脚本不伪造该来源)───────────────" echo "docker exec -i sh -c \\" echo " 'MYSQL_PWD=\"\$MYSQL_PASSWORD\" mysql -u\"\$MYSQL_USER\" -N -B \"\${MYSQL_DATABASE:-mydreamjob}\" <<<\"SHOW COLUMNS FROM $TABLE;\"'" echo "对账要求:SHOW COLUMNS 的 Field 集合必须等于上方 Flyway列;" echo " 必须包含 expired_at;必须不含 expires_at。" echo "" echo "============================================" echo "PASS=$PASS FAIL=$FAIL" echo "============================================" [ "$FAIL" -eq 0 ]