Files
2026-09-02 09:58:57 +08:00

314 lines
10 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 "$@"