314 lines
10 KiB
Bash
Executable File
314 lines
10 KiB
Bash
Executable File
#!/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 "$@"
|