sync_user_to_sso.sh 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. #!/bin/bash
  2. # ===========================================
  3. # 统一身份认证用户数据同步脚本
  4. # 参考:学生换班级 process_student_class_change_fixed.sh
  5. # 功能:定时将校园系统用户/组织数据同步到认证中间库
  6. # 用法:
  7. # ./sync_user_to_sso.sh # 守护进程,按 CYCLE_INTERVAL 循环
  8. # ./sync_user_to_sso.sh --once # 仅执行一次同步
  9. # ./sync_user_to_sso.sh --full # 强制全量同步(一次)
  10. # ===========================================
  11. set -euo pipefail
  12. export LANG=C
  13. export LC_ALL=C
  14. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  15. CONFIG_FILE="${SCRIPT_DIR}/data.conf"
  16. RUN_ONCE=false
  17. FORCE_FULL=false
  18. for arg in "$@"; do
  19. case "$arg" in
  20. --once) RUN_ONCE=true ;;
  21. --full) FORCE_FULL=true ;;
  22. --config=*) CONFIG_FILE="${arg#*=}" ;;
  23. esac
  24. done
  25. if [[ ! -f "$CONFIG_FILE" ]]; then
  26. echo "错误:配置文件 $CONFIG_FILE 未找到"
  27. exit 1
  28. fi
  29. source "$CONFIG_FILE"
  30. mkdir -p "$LOG_DIR"
  31. LOG_FILE="${LOG_DIR}/sync_user_to_sso_$(date +%Y%m%d_%H%M%S).log"
  32. log() {
  33. local level="$1"
  34. local message="$2"
  35. local timestamp
  36. timestamp=$(date '+%Y-%m-%d %H:%M:%S')
  37. echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
  38. }
  39. error_exit() {
  40. log "ERROR" "$1"
  41. [[ -f "$LOCK_FILE" ]] && rm -f "$LOCK_FILE"
  42. exit 1
  43. }
  44. acquire_lock() {
  45. if [[ -f "$LOCK_FILE" ]]; then
  46. local pid
  47. pid=$(cat "$LOCK_FILE" 2>/dev/null || echo "")
  48. if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
  49. error_exit "同步进程已在运行 (PID: $pid)"
  50. fi
  51. rm -f "$LOCK_FILE"
  52. fi
  53. echo $$ > "$LOCK_FILE"
  54. }
  55. release_lock() {
  56. [[ -f "$LOCK_FILE" ]] && rm -f "$LOCK_FILE"
  57. }
  58. trap release_lock EXIT
  59. _db_helper() {
  60. local host="$1" port="$2" user="$3" pass="$4" db="$5" action="$6"
  61. shift 6
  62. python3 "${SCRIPT_DIR}/db_helper.py" \
  63. --host "$host" --port "$port" \
  64. --user "$user" --password "$pass" --database "$db" \
  65. "$action" "$@"
  66. }
  67. source_mysql() {
  68. local sql="$1"
  69. _db_helper "$SOURCE_DB_HOST" "$SOURCE_DB_PORT" "$SOURCE_DB_USER" "$SOURCE_DB_PASS" "$SOURCE_DB_NAME" scalar "$sql" 2>&1
  70. }
  71. target_mysql() {
  72. local sql="$1"
  73. _db_helper "$TARGET_DB_HOST" "$TARGET_DB_PORT" "$TARGET_DB_USER" "$TARGET_DB_PASS" "$TARGET_DB_NAME" scalar "$sql" 2>&1
  74. }
  75. target_mysql_file() {
  76. local file="$1"
  77. _db_helper "$TARGET_DB_HOST" "$TARGET_DB_PORT" "$TARGET_DB_USER" "$TARGET_DB_PASS" "$TARGET_DB_NAME" file "$file" 2>&1
  78. }
  79. write_errlog() {
  80. local table_name="$1"
  81. local action="$2"
  82. local err_msg="$3"
  83. local escaped_msg
  84. escaped_msg=$(echo "$err_msg" | sed "s/'/''/g" | head -c 900)
  85. local sql="INSERT INTO ZFTAL_XTGL_ERRLOG (YWXTMC, YWXTBM, YWXTCZ, SJCCGC, CWZDXX, CWSJ)
  86. VALUES ('${YWXTMC}', '${table_name}', '${action}', 'sync_user_to_sso.sh', '${escaped_msg}', DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'));"
  87. if [[ "$TARGET_DB_TYPE" == "mysql" ]]; then
  88. target_mysql "$sql" >/dev/null 2>&1 || true
  89. fi
  90. }
  91. get_last_sync_time() {
  92. if [[ -f "$STATE_FILE" ]]; then
  93. cat "$STATE_FILE"
  94. else
  95. echo "1970-01-01 00:00:00"
  96. fi
  97. }
  98. save_sync_time() {
  99. date '+%Y-%m-%d %H:%M:%S' > "$STATE_FILE"
  100. }
  101. build_incremental_filter() {
  102. local alias="$1"
  103. local last_sync="$2"
  104. if [[ "$FORCE_FULL" == true ]] || [[ "$SYNC_MODE" == "full" ]]; then
  105. echo ""
  106. else
  107. echo "AND (${alias}.update_time > '${last_sync}' OR ${alias}.create_time > '${last_sync}')"
  108. fi
  109. }
  110. prepare_mysql_sync_sql() {
  111. local last_sync="$1"
  112. local template="${SCRIPT_DIR}/sync_sql/sync_all_mysql.sql"
  113. local output="/tmp/sso_sync_mysql_$$.sql"
  114. if [[ ! -f "$template" ]]; then
  115. error_exit "同步 SQL 模板未找到: $template"
  116. fi
  117. local dept_filter teacher_filter student_filter
  118. dept_filter=$(build_incremental_filter "d" "$last_sync")
  119. teacher_filter=$(build_incremental_filter "u" "$last_sync")
  120. student_filter=$(build_incremental_filter "s" "$last_sync")
  121. sed \
  122. -e "s|/\*INCREMENTAL_FILTER_DEPT\*/|${dept_filter}|g" \
  123. -e "s|/\*INCREMENTAL_FILTER_TEACHER\*/|${teacher_filter}|g" \
  124. -e "s|/\*INCREMENTAL_FILTER_STUDENT\*/|${student_filter}|g" \
  125. "$template" > "$output"
  126. echo "$output"
  127. }
  128. sync_mysql_target() {
  129. local last_sync
  130. last_sync=$(get_last_sync_time)
  131. log "INFO" "开始 MySQL 目标库同步,模式: $([[ "$FORCE_FULL" == true || "$SYNC_MODE" == "full" ]] && echo 全量 || echo 增量),上次同步: $last_sync"
  132. local sql_file
  133. sql_file=$(prepare_mysql_sync_sql "$last_sync")
  134. local result
  135. if result=$(target_mysql_file "$sql_file" 2>&1); then
  136. log "INFO" "MySQL 同步 SQL 执行成功"
  137. rm -f "$sql_file"
  138. save_sync_time
  139. return 0
  140. else
  141. log "ERROR" "MySQL 同步失败: $result"
  142. write_errlog "ALL" "数据同步" "$result"
  143. rm -f "$sql_file"
  144. return 1
  145. fi
  146. }
  147. sync_oracle_target() {
  148. local last_sync
  149. last_sync=$(get_last_sync_time)
  150. local mode_label="增量"
  151. if [[ "$FORCE_FULL" == true ]] || [[ "$SYNC_MODE" == "full" ]]; then
  152. mode_label="全量"
  153. fi
  154. log "INFO" "开始 Oracle 目标库同步,模式: ${mode_label},上次同步: $last_sync"
  155. if ! command -v sqlplus >/dev/null 2>&1; then
  156. error_exit "未找到 sqlplus 命令,无法连接 Oracle 目标库(请安装 Instant Client 并配置 PATH)"
  157. fi
  158. local merge_sql="/tmp/sso_oracle_merge_$$.sql"
  159. local gen_cmd=(
  160. python3 "${SCRIPT_DIR}/oracle_gen_merge.py"
  161. --host "$SOURCE_DB_HOST" --port "$SOURCE_DB_PORT"
  162. --user "$SOURCE_DB_USER" --password "$SOURCE_DB_PASS"
  163. --database "$SOURCE_DB_NAME"
  164. --output "$merge_sql"
  165. --last-sync "$last_sync"
  166. --batch-size "${BATCH_SIZE:-200}"
  167. )
  168. if [[ "$FORCE_FULL" == true ]] || [[ "$SYNC_MODE" == "full" ]]; then
  169. gen_cmd+=(--full)
  170. fi
  171. local gen_out
  172. if ! gen_out=$("${gen_cmd[@]}" 2>&1); then
  173. log "ERROR" "生成 Oracle MERGE SQL 失败: $gen_out"
  174. rm -f "$merge_sql"
  175. return 1
  176. fi
  177. log "INFO" "已生成 MERGE SQL: $gen_out"
  178. local conn="${TARGET_DB_USER}/${TARGET_DB_PASS}@${TARGET_ORACLE_CONNECT:-${TARGET_DB_HOST}:${TARGET_DB_PORT}/${TARGET_DB_NAME}}"
  179. if sqlplus -S "$conn" @"$merge_sql" >> "$LOG_FILE" 2>&1; then
  180. log "INFO" "Oracle 同步完成"
  181. save_sync_time
  182. rm -f "$merge_sql"
  183. return 0
  184. else
  185. log "ERROR" "Oracle 同步失败,详见日志: $LOG_FILE (SQL 文件保留: $merge_sql)"
  186. write_errlog "ALL" "Oracle数据同步" "sqlplus执行失败"
  187. return 1
  188. fi
  189. }
  190. run_sync_cycle() {
  191. log "INFO" "========== 开始同步周期 =========="
  192. # 测试源库连接
  193. if ! source_mysql "SELECT 1;" >/dev/null 2>&1; then
  194. log "ERROR" "源库连接失败: ${SOURCE_DB_USER}@${SOURCE_DB_HOST}:${SOURCE_DB_PORT}/${SOURCE_DB_NAME}"
  195. return 1
  196. fi
  197. if [[ "$TARGET_DB_TYPE" == "mysql" ]]; then
  198. if ! target_mysql "SELECT 1;" >/dev/null 2>&1; then
  199. log "ERROR" "目标库连接失败: ${TARGET_DB_USER}@${TARGET_DB_HOST}:${TARGET_DB_PORT}/${TARGET_DB_NAME}"
  200. return 1
  201. fi
  202. sync_mysql_target
  203. elif [[ "$TARGET_DB_TYPE" == "oracle" ]]; then
  204. sync_oracle_target
  205. else
  206. log "ERROR" "不支持的目标库类型: $TARGET_DB_TYPE"
  207. return 1
  208. fi
  209. log "INFO" "========== 同步周期结束 =========="
  210. }
  211. verify_counts() {
  212. log "INFO" "---------- 数据校验 ----------"
  213. if [[ "$TARGET_DB_TYPE" != "mysql" ]]; then
  214. log "INFO" "Oracle 目标库校验请使用 verify_sync.sh"
  215. return 0
  216. fi
  217. local tables=("GXXX_YXSDWJBSJZL" "GXJG_JZGJCSJZLB" "GXJG_GRTXFSZL" "GXXS_XSJBSJZL" "GXXS_GRTXZL")
  218. for t in "${tables[@]}"; do
  219. local cnt
  220. cnt=$(target_mysql "SELECT COUNT(*) FROM ${t};" 2>/dev/null | tail -1)
  221. log "INFO" "目标表 ${t}: ${cnt} 条"
  222. done
  223. local src_dept src_teacher src_student
  224. src_dept=$(source_mysql "SELECT COUNT(*) FROM sys_dept;" 2>/dev/null | tail -1)
  225. src_teacher=$(source_mysql "SELECT COUNT(*) FROM sys_user WHERE account_type='1';" 2>/dev/null | tail -1)
  226. src_student=$(source_mysql "SELECT COUNT(*) FROM xg_student_msg WHERE deleted=0;" 2>/dev/null | tail -1)
  227. log "INFO" "源表 sys_dept: ${src_dept} 条"
  228. log "INFO" "源表 sys_user(教职工): ${src_teacher} 条"
  229. log "INFO" "源表 xg_student_msg(未删除): ${src_student} 条"
  230. }
  231. # 守护进程时间窗:仅在 [SYNC_WINDOW_START_HOUR, SYNC_WINDOW_END_HOUR) 内执行
  232. # 例如 START=0 END=1 表示每天 00:00~00:59;--once / --full 不受此限制
  233. in_sync_window() {
  234. local enabled="${SYNC_WINDOW_ENABLED:-false}"
  235. if [[ "$enabled" != "true" ]]; then
  236. return 0
  237. fi
  238. local start_h="${SYNC_WINDOW_START_HOUR:-0}"
  239. local end_h="${SYNC_WINDOW_END_HOUR:-1}"
  240. local now_h
  241. now_h=$(date '+%-H' 2>/dev/null || date '+%H' | sed 's/^0//')
  242. [[ -z "$now_h" ]] && now_h=0
  243. if (( now_h >= start_h && now_h < end_h )); then
  244. return 0
  245. fi
  246. return 1
  247. }
  248. main() {
  249. acquire_lock
  250. log "INFO" "统一身份认证用户数据同步启动"
  251. log "INFO" "源库: ${SOURCE_DB_TYPE} ${SOURCE_DB_USER}@${SOURCE_DB_HOST}:${SOURCE_DB_PORT}/${SOURCE_DB_NAME}"
  252. log "INFO" "目标库: ${TARGET_DB_TYPE} ${TARGET_DB_USER}@${TARGET_DB_HOST}:${TARGET_DB_PORT}/${TARGET_DB_NAME}"
  253. log "INFO" "同步模式: ${SYNC_MODE},循环间隔: ${CYCLE_INTERVAL}秒"
  254. if [[ "${SYNC_WINDOW_ENABLED:-false}" == "true" ]]; then
  255. log "INFO" "守护进程时间窗: ${SYNC_WINDOW_START_HOUR:-0}:00 ~ ${SYNC_WINDOW_END_HOUR:-1}:00(仅该时段执行;--once 不受限)"
  256. fi
  257. if [[ "$RUN_ONCE" == true ]]; then
  258. run_sync_cycle && verify_counts
  259. exit $?
  260. fi
  261. while true; do
  262. if in_sync_window; then
  263. run_sync_cycle && verify_counts || log "WARN" "本轮同步失败,将在 ${CYCLE_INTERVAL} 秒后重试"
  264. else
  265. log "INFO" "当前不在同步时间窗内(${SYNC_WINDOW_START_HOUR:-0}:00~${SYNC_WINDOW_END_HOUR:-1}:00),跳过本轮"
  266. fi
  267. log "INFO" "等待 ${CYCLE_INTERVAL} 秒后开始下一轮..."
  268. sleep "$CYCLE_INTERVAL"
  269. done
  270. }
  271. main "$@"