#!/bin/bash # =========================================== # 统一身份认证用户数据同步脚本 # 参考:学生换班级 process_student_class_change_fixed.sh # 功能:定时将校园系统用户/组织数据同步到认证中间库 # 用法: # ./sync_user_to_sso.sh # 守护进程,按 CYCLE_INTERVAL 循环 # ./sync_user_to_sso.sh --once # 仅执行一次同步 # ./sync_user_to_sso.sh --full # 强制全量同步(一次) # =========================================== set -euo pipefail export LANG=C export LC_ALL=C SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CONFIG_FILE="${SCRIPT_DIR}/data.conf" RUN_ONCE=false FORCE_FULL=false for arg in "$@"; do case "$arg" in --once) RUN_ONCE=true ;; --full) FORCE_FULL=true ;; --config=*) CONFIG_FILE="${arg#*=}" ;; esac done if [[ ! -f "$CONFIG_FILE" ]]; then echo "错误:配置文件 $CONFIG_FILE 未找到" exit 1 fi source "$CONFIG_FILE" mkdir -p "$LOG_DIR" LOG_FILE="${LOG_DIR}/sync_user_to_sso_$(date +%Y%m%d_%H%M%S).log" log() { local level="$1" local message="$2" local timestamp timestamp=$(date '+%Y-%m-%d %H:%M:%S') echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE" } error_exit() { log "ERROR" "$1" [[ -f "$LOCK_FILE" ]] && rm -f "$LOCK_FILE" exit 1 } acquire_lock() { if [[ -f "$LOCK_FILE" ]]; then local pid pid=$(cat "$LOCK_FILE" 2>/dev/null || echo "") if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then error_exit "同步进程已在运行 (PID: $pid)" fi rm -f "$LOCK_FILE" fi echo $$ > "$LOCK_FILE" } release_lock() { [[ -f "$LOCK_FILE" ]] && rm -f "$LOCK_FILE" } trap release_lock EXIT _db_helper() { local host="$1" port="$2" user="$3" pass="$4" db="$5" action="$6" shift 6 python3 "${SCRIPT_DIR}/db_helper.py" \ --host "$host" --port "$port" \ --user "$user" --password "$pass" --database "$db" \ "$action" "$@" } source_mysql() { local sql="$1" _db_helper "$SOURCE_DB_HOST" "$SOURCE_DB_PORT" "$SOURCE_DB_USER" "$SOURCE_DB_PASS" "$SOURCE_DB_NAME" scalar "$sql" 2>&1 } target_mysql() { local sql="$1" _db_helper "$TARGET_DB_HOST" "$TARGET_DB_PORT" "$TARGET_DB_USER" "$TARGET_DB_PASS" "$TARGET_DB_NAME" scalar "$sql" 2>&1 } target_mysql_file() { local file="$1" _db_helper "$TARGET_DB_HOST" "$TARGET_DB_PORT" "$TARGET_DB_USER" "$TARGET_DB_PASS" "$TARGET_DB_NAME" file "$file" 2>&1 } write_errlog() { local table_name="$1" local action="$2" local err_msg="$3" local escaped_msg escaped_msg=$(echo "$err_msg" | sed "s/'/''/g" | head -c 900) local sql="INSERT INTO ZFTAL_XTGL_ERRLOG (YWXTMC, YWXTBM, YWXTCZ, SJCCGC, CWZDXX, CWSJ) VALUES ('${YWXTMC}', '${table_name}', '${action}', 'sync_user_to_sso.sh', '${escaped_msg}', DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'));" if [[ "$TARGET_DB_TYPE" == "mysql" ]]; then target_mysql "$sql" >/dev/null 2>&1 || true fi } get_last_sync_time() { if [[ -f "$STATE_FILE" ]]; then cat "$STATE_FILE" else echo "1970-01-01 00:00:00" fi } save_sync_time() { date '+%Y-%m-%d %H:%M:%S' > "$STATE_FILE" } build_incremental_filter() { local alias="$1" local last_sync="$2" if [[ "$FORCE_FULL" == true ]] || [[ "$SYNC_MODE" == "full" ]]; then echo "" else echo "AND (${alias}.update_time > '${last_sync}' OR ${alias}.create_time > '${last_sync}')" fi } prepare_mysql_sync_sql() { local last_sync="$1" local template="${SCRIPT_DIR}/sync_sql/sync_all_mysql.sql" local output="/tmp/sso_sync_mysql_$$.sql" if [[ ! -f "$template" ]]; then error_exit "同步 SQL 模板未找到: $template" fi local dept_filter teacher_filter student_filter dept_filter=$(build_incremental_filter "d" "$last_sync") teacher_filter=$(build_incremental_filter "u" "$last_sync") student_filter=$(build_incremental_filter "s" "$last_sync") sed \ -e "s|/\*INCREMENTAL_FILTER_DEPT\*/|${dept_filter}|g" \ -e "s|/\*INCREMENTAL_FILTER_TEACHER\*/|${teacher_filter}|g" \ -e "s|/\*INCREMENTAL_FILTER_STUDENT\*/|${student_filter}|g" \ "$template" > "$output" echo "$output" } sync_mysql_target() { local last_sync last_sync=$(get_last_sync_time) log "INFO" "开始 MySQL 目标库同步,模式: $([[ "$FORCE_FULL" == true || "$SYNC_MODE" == "full" ]] && echo 全量 || echo 增量),上次同步: $last_sync" local sql_file sql_file=$(prepare_mysql_sync_sql "$last_sync") local result if result=$(target_mysql_file "$sql_file" 2>&1); then log "INFO" "MySQL 同步 SQL 执行成功" rm -f "$sql_file" save_sync_time return 0 else log "ERROR" "MySQL 同步失败: $result" write_errlog "ALL" "数据同步" "$result" rm -f "$sql_file" return 1 fi } sync_oracle_target() { local last_sync last_sync=$(get_last_sync_time) local mode_label="增量" if [[ "$FORCE_FULL" == true ]] || [[ "$SYNC_MODE" == "full" ]]; then mode_label="全量" fi log "INFO" "开始 Oracle 目标库同步,模式: ${mode_label},上次同步: $last_sync" if ! command -v sqlplus >/dev/null 2>&1; then error_exit "未找到 sqlplus 命令,无法连接 Oracle 目标库(请安装 Instant Client 并配置 PATH)" fi local merge_sql="/tmp/sso_oracle_merge_$$.sql" local gen_cmd=( python3 "${SCRIPT_DIR}/oracle_gen_merge.py" --host "$SOURCE_DB_HOST" --port "$SOURCE_DB_PORT" --user "$SOURCE_DB_USER" --password "$SOURCE_DB_PASS" --database "$SOURCE_DB_NAME" --output "$merge_sql" --last-sync "$last_sync" --batch-size "${BATCH_SIZE:-200}" ) if [[ "$FORCE_FULL" == true ]] || [[ "$SYNC_MODE" == "full" ]]; then gen_cmd+=(--full) fi local gen_out if ! gen_out=$("${gen_cmd[@]}" 2>&1); then log "ERROR" "生成 Oracle MERGE SQL 失败: $gen_out" rm -f "$merge_sql" return 1 fi log "INFO" "已生成 MERGE SQL: $gen_out" local conn="${TARGET_DB_USER}/${TARGET_DB_PASS}@${TARGET_ORACLE_CONNECT:-${TARGET_DB_HOST}:${TARGET_DB_PORT}/${TARGET_DB_NAME}}" if sqlplus -S "$conn" @"$merge_sql" >> "$LOG_FILE" 2>&1; then log "INFO" "Oracle 同步完成" save_sync_time rm -f "$merge_sql" return 0 else log "ERROR" "Oracle 同步失败,详见日志: $LOG_FILE (SQL 文件保留: $merge_sql)" write_errlog "ALL" "Oracle数据同步" "sqlplus执行失败" return 1 fi } run_sync_cycle() { log "INFO" "========== 开始同步周期 ==========" # 测试源库连接 if ! source_mysql "SELECT 1;" >/dev/null 2>&1; then log "ERROR" "源库连接失败: ${SOURCE_DB_USER}@${SOURCE_DB_HOST}:${SOURCE_DB_PORT}/${SOURCE_DB_NAME}" return 1 fi if [[ "$TARGET_DB_TYPE" == "mysql" ]]; then if ! target_mysql "SELECT 1;" >/dev/null 2>&1; then log "ERROR" "目标库连接失败: ${TARGET_DB_USER}@${TARGET_DB_HOST}:${TARGET_DB_PORT}/${TARGET_DB_NAME}" return 1 fi sync_mysql_target elif [[ "$TARGET_DB_TYPE" == "oracle" ]]; then sync_oracle_target else log "ERROR" "不支持的目标库类型: $TARGET_DB_TYPE" return 1 fi log "INFO" "========== 同步周期结束 ==========" } verify_counts() { log "INFO" "---------- 数据校验 ----------" if [[ "$TARGET_DB_TYPE" != "mysql" ]]; then log "INFO" "Oracle 目标库校验请使用 verify_sync.sh" return 0 fi local tables=("GXXX_YXSDWJBSJZL" "GXJG_JZGJCSJZLB" "GXJG_GRTXFSZL" "GXXS_XSJBSJZL" "GXXS_GRTXZL") for t in "${tables[@]}"; do local cnt cnt=$(target_mysql "SELECT COUNT(*) FROM ${t};" 2>/dev/null | tail -1) log "INFO" "目标表 ${t}: ${cnt} 条" done local src_dept src_teacher src_student src_dept=$(source_mysql "SELECT COUNT(*) FROM sys_dept;" 2>/dev/null | tail -1) src_teacher=$(source_mysql "SELECT COUNT(*) FROM sys_user WHERE account_type='1';" 2>/dev/null | tail -1) src_student=$(source_mysql "SELECT COUNT(*) FROM xg_student_msg WHERE deleted=0;" 2>/dev/null | tail -1) log "INFO" "源表 sys_dept: ${src_dept} 条" log "INFO" "源表 sys_user(教职工): ${src_teacher} 条" log "INFO" "源表 xg_student_msg(未删除): ${src_student} 条" } # 守护进程时间窗:仅在 [SYNC_WINDOW_START_HOUR, SYNC_WINDOW_END_HOUR) 内执行 # 例如 START=0 END=1 表示每天 00:00~00:59;--once / --full 不受此限制 in_sync_window() { local enabled="${SYNC_WINDOW_ENABLED:-false}" if [[ "$enabled" != "true" ]]; then return 0 fi local start_h="${SYNC_WINDOW_START_HOUR:-0}" local end_h="${SYNC_WINDOW_END_HOUR:-1}" local now_h now_h=$(date '+%-H' 2>/dev/null || date '+%H' | sed 's/^0//') [[ -z "$now_h" ]] && now_h=0 if (( now_h >= start_h && now_h < end_h )); then return 0 fi return 1 } main() { acquire_lock log "INFO" "统一身份认证用户数据同步启动" log "INFO" "源库: ${SOURCE_DB_TYPE} ${SOURCE_DB_USER}@${SOURCE_DB_HOST}:${SOURCE_DB_PORT}/${SOURCE_DB_NAME}" log "INFO" "目标库: ${TARGET_DB_TYPE} ${TARGET_DB_USER}@${TARGET_DB_HOST}:${TARGET_DB_PORT}/${TARGET_DB_NAME}" log "INFO" "同步模式: ${SYNC_MODE},循环间隔: ${CYCLE_INTERVAL}秒" if [[ "${SYNC_WINDOW_ENABLED:-false}" == "true" ]]; then log "INFO" "守护进程时间窗: ${SYNC_WINDOW_START_HOUR:-0}:00 ~ ${SYNC_WINDOW_END_HOUR:-1}:00(仅该时段执行;--once 不受限)" fi if [[ "$RUN_ONCE" == true ]]; then run_sync_cycle && verify_counts exit $? fi while true; do if in_sync_window; then run_sync_cycle && verify_counts || log "WARN" "本轮同步失败,将在 ${CYCLE_INTERVAL} 秒后重试" else log "INFO" "当前不在同步时间窗内(${SYNC_WINDOW_START_HOUR:-0}:00~${SYNC_WINDOW_END_HOUR:-1}:00),跳过本轮" fi log "INFO" "等待 ${CYCLE_INTERVAL} 秒后开始下一轮..." sleep "$CYCLE_INTERVAL" done } main "$@"