| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313 |
- #!/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 "$@"
|