prefix 拼出,不含用户输入, // 而表名又不能用 %s 占位符,所以只能插值; // - 值全部走 $wpdb->prepare(),其中 IN (...) 一类用数组形式传参, // PHPCS 数不出数组长度,才会报占位符数量对不上; // - 译文数据随时被后台改写,缓存反而会读到旧译文,故不接对象缓存。 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared // phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber // phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter /** * Class APGT_Store */ class APGT_Store { /** * 取数据表名。 * * @return string */ public static function table() { return APGT_Install::table_name(); } /** * 写入 / 更新一条译文(按唯一键 upsert)。 * * @param array $data 需含 text_source/text_trans/language_from/language_to, * 可选 source_url/engine/type。 * @return bool */ public static function save( array $data ) { global $wpdb; $text_source = (string) ( $data['text_source'] ?? '' ); if ( '' === trim( $text_source ) ) { return false; } $row = array( 'text_source' => $text_source, 'text_trans' => (string) ( $data['text_trans'] ?? '' ), 'language_from' => (string) ( $data['language_from'] ?? '' ), 'language_to' => (string) ( $data['language_to'] ?? '' ), 'source_url' => isset( $data['source_url'] ) ? (string) $data['source_url'] : null, 'source_md5' => md5( $text_source ), 'engine' => (string) ( $data['engine'] ?? 'ai' ), 'type' => (string) ( $data['type'] ?? 'text' ), 'updated_at' => current_time( 'mysql' ), ); $table = self::table(); // INSERT ... ON DUPLICATE KEY UPDATE:已存在则更新译文/引擎/来源。 $sql = $wpdb->prepare( "INSERT INTO {$table} (text_source, text_trans, language_from, language_to, source_url, source_md5, engine, type, updated_at) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE text_trans = VALUES(text_trans), source_url = VALUES(source_url), engine = VALUES(engine), type = VALUES(type), updated_at = VALUES(updated_at)", // phpcs:ignore WordPress.DB.PreparedSQL $row['text_source'], $row['text_trans'], $row['language_from'], $row['language_to'], $row['source_url'], $row['source_md5'], $row['engine'], $row['type'], $row['updated_at'] ); return false !== $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL } /** * 查询单条原文在指定语言对下的译文记录。 * * @param string $source 原文。 * @param string $from 源语言。 * @param string $to 目标语言。 * @return array|null */ public static function get( $source, $from, $to ) { global $wpdb; $table = self::table(); $sql = $wpdb->prepare( "SELECT * FROM {$table} WHERE language_from = %s AND language_to = %s AND source_md5 = %s LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL $from, $to, md5( (string) $source ) ); $row = $wpdb->get_row( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL return $row ? $row : null; } /** * 批量取译文映射(前台整页替换用)。 * * @param string[] $md5s 原文 md5 列表。 * @param string $from 源语言。 * @param string $to 目标语言。 * @return array md5 => text_trans(仅含有非空译文的记录) */ public static function get_trans_map( array $md5s, $from, $to ) { global $wpdb; $md5s = array_values( array_unique( array_filter( $md5s ) ) ); if ( empty( $md5s ) ) { return array(); } $table = self::table(); $placeholders = implode( ',', array_fill( 0, count( $md5s ), '%s' ) ); $params = array_merge( array( $from, $to ), $md5s ); $sql = $wpdb->prepare( "SELECT source_md5, text_trans FROM {$table} WHERE language_from = %s AND language_to = %s AND source_md5 IN ({$placeholders})", // phpcs:ignore WordPress.DB.PreparedSQL $params ); $rows = $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL $map = array(); foreach ( (array) $rows as $r ) { if ( '' !== (string) $r['text_trans'] ) { $map[ $r['source_md5'] ] = $r['text_trans']; } } return $map; } /** * 判断某原文是否已由 AI/manual 翻译(用于按页面翻译时跳过)。 * * @param string $source 原文。 * @param string $from 源语言。 * @param string $to 目标语言。 * @return bool */ public static function is_translated( $source, $from, $to ) { $row = self::get( $source, $from, $to ); if ( ! $row ) { return false; } return in_array( $row['engine'], array( 'ai', 'manual' ), true ) && '' !== (string) $row['text_trans']; } /** * 把筛选条件编译成 WHERE 子句与参数。 * * @param array $args 见 query()。 * @return array [ 'sql'=>string, 'params'=>array, 'has_filter'=>bool ] */ private static function build_where( array $args ) { global $wpdb; $where = array( '1=1' ); $params = array(); $has_filter = false; $like_cols = array( 'search_source' => 'text_source', 'search_trans' => 'text_trans', 'search_url' => 'source_url', ); foreach ( $like_cols as $key => $col ) { if ( isset( $args[ $key ] ) && '' !== trim( (string) $args[ $key ] ) ) { $where[] = "{$col} LIKE %s"; $params[] = '%' . $wpdb->esc_like( trim( (string) $args[ $key ] ) ) . '%'; $has_filter = true; } } $eq_cols = array( 'language_from' => 'language_from', 'language_to' => 'language_to', 'engine' => 'engine', 'type' => 'type', ); foreach ( $eq_cols as $key => $col ) { if ( isset( $args[ $key ] ) && '' !== trim( (string) $args[ $key ] ) ) { $where[] = "{$col} = %s"; $params[] = trim( (string) $args[ $key ] ); $has_filter = true; } } // 只看空译文(用于挑出漏译的条目)。 if ( ! empty( $args['only_empty'] ) ) { $where[] = "text_trans = ''"; $has_filter = true; } return array( 'sql' => implode( ' AND ', $where ), 'params' => $params, 'has_filter' => $has_filter, ); } /** * 列表查询(译文数据页),带搜索与分页。 * * @param array $args 支持 search_source/search_trans/search_url/language_from/ * language_to/engine/type/only_empty/paged/pagesize。 * @return array [ 'total' => int, 'rows' => array ] */ public static function query( array $args = array() ) { global $wpdb; $table = self::table(); $w = self::build_where( $args ); $count_sql = "SELECT COUNT(*) FROM {$table} WHERE {$w['sql']}"; if ( $w['params'] ) { $count_sql = $wpdb->prepare( $count_sql, $w['params'] ); // phpcs:ignore WordPress.DB.PreparedSQL } $total = (int) $wpdb->get_var( $count_sql ); // phpcs:ignore WordPress.DB.PreparedSQL $pagesize = max( 1, (int) ( $args['pagesize'] ?? 20 ) ); $paged = max( 1, (int) ( $args['paged'] ?? 1 ) ); $offset = ( $paged - 1 ) * $pagesize; $list_sql = $wpdb->prepare( "SELECT * FROM {$table} WHERE {$w['sql']} ORDER BY id DESC LIMIT %d OFFSET %d", // phpcs:ignore WordPress.DB.PreparedSQL array_merge( $w['params'], array( $pagesize, $offset ) ) ); $rows = $wpdb->get_results( $list_sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL return array( 'total' => $total, 'rows' => (array) $rows, ); } /** * 按筛选条件批量删除。 * * 没有任何筛选条件时拒绝执行——否则一次误点就把整张表清空了, * 要清空请走 clear()。 * * @param array $args 同 query() 的筛选条件。 * @return array [ 'ok'=>bool, 'deleted'=>int, 'pages'=>array 受影响的 [路径, 语言] ] */ public static function delete_by_query( array $args ) { global $wpdb; $table = self::table(); $w = self::build_where( $args ); if ( ! $w['has_filter'] ) { return array( 'ok' => false, 'deleted' => 0, 'pages' => array(), ); } // 先记下涉及哪些页面,删完要把它们的翻译进度一并作废。 $pages_sql = "SELECT DISTINCT source_url, language_to FROM {$table} WHERE {$w['sql']}"; if ( $w['params'] ) { $pages_sql = $wpdb->prepare( $pages_sql, $w['params'] ); // phpcs:ignore WordPress.DB.PreparedSQL } $pages = (array) $wpdb->get_results( $pages_sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL $delete_sql = "DELETE FROM {$table} WHERE {$w['sql']}"; if ( $w['params'] ) { $delete_sql = $wpdb->prepare( $delete_sql, $w['params'] ); // phpcs:ignore WordPress.DB.PreparedSQL } $deleted = $wpdb->query( $delete_sql ); // phpcs:ignore WordPress.DB.PreparedSQL return array( 'ok' => true, 'deleted' => (int) $deleted, 'pages' => $pages, ); } /** * 取单条记录。 * * @param int $id 记录 ID。 * @return array|null */ public static function get_by_id( $id ) { global $wpdb; $table = self::table(); $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", (int) $id ), // phpcs:ignore WordPress.DB.PreparedSQL ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL return $row ? $row : null; } /** * 取库中已存在的类型列表(用于筛选下拉)。 * * @return string[] */ public static function distinct_types() { global $wpdb; $table = self::table(); return (array) $wpdb->get_col( "SELECT DISTINCT type FROM {$table} WHERE type <> '' ORDER BY type" ); // phpcs:ignore WordPress.DB.PreparedSQL } /** * 取库中已出现过的语言对(用于筛选下拉)。 * * @return array [ [ 'language_from'=>, 'language_to'=> ], ... ] */ public static function distinct_languages() { global $wpdb; $table = self::table(); return (array) $wpdb->get_results( // phpcs:ignore WordPress.DB.PreparedSQL "SELECT DISTINCT language_from, language_to FROM {$table} ORDER BY language_from, language_to", ARRAY_A ); } /** * 更新指定记录的译文(行内编辑,标记为 manual)。 * * @param int $id 记录 ID。 * @param string $text_trans 新译文。 * @return bool */ public static function update_trans( $id, $text_trans ) { global $wpdb; return false !== $wpdb->update( self::table(), array( 'text_trans' => (string) $text_trans, 'engine' => 'manual', 'updated_at' => current_time( 'mysql' ), ), array( 'id' => (int) $id ), array( '%s', '%s', '%s' ), array( '%d' ) ); } /** * 删除单条记录。 * * @param int $id 记录 ID。 * @return bool */ public static function delete( $id ) { global $wpdb; return false !== $wpdb->delete( self::table(), array( 'id' => (int) $id ), array( '%d' ) ); } /** * 清空全部译文记录。 * * @return void */ public static function clear() { global $wpdb; $table = self::table(); $wpdb->query( "TRUNCATE TABLE {$table}" ); // phpcs:ignore WordPress.DB.PreparedSQL } /** * 取某条站内路径在目标语言下的译文路径。 * * @param string $path 原文路径(以 / 开头)。 * @param string $from 源语言。 * @param string $to 目标语言。 * @return string 没有译文时返回空串。 */ public static function uri_translation( $path, $from, $to ) { global $wpdb; $table = self::table(); $trans = $wpdb->get_var( $wpdb->prepare( "SELECT text_trans FROM {$table} WHERE language_from = %s AND language_to = %s AND source_md5 = %s AND type = 'uri' LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL $from, $to, md5( (string) $path ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL return is_string( $trans ) ? trim( $trans ) : ''; } /** * 由译文路径反查原文路径。 * * 开启 URL 翻译后,访客看到的是 /en/about-us/ 这种译过的地址, * 得先换回原文路径才能交给 WordPress 去找对应的文章。 * * @param string $trans_path 译文路径。 * @param string $from 源语言。 * @param string $to 目标语言。 * @return string 找不到时返回空串。 */ public static function uri_source( $trans_path, $from, $to ) { global $wpdb; $table = self::table(); // 译文路径用目标语言的文字写(日语 /サンプルページ/、俄语 /о-нас/), // 而地址栏送过来的是百分号编码形态,两种写法都得能查到。 $paths = array( (string) $trans_path ); $decoded = rawurldecode( (string) $trans_path ); if ( $decoded !== (string) $trans_path ) { $paths[] = $decoded; } $placeholders = implode( ',', array_fill( 0, count( $paths ), '%s' ) ); $params = array_merge( array( $from, $to ), $paths ); $source = $wpdb->get_var( $wpdb->prepare( "SELECT text_source FROM {$table} WHERE language_from = %s AND language_to = %s AND type = 'uri' AND text_trans IN ({$placeholders}) LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL $params ) ); // phpcs:ignore WordPress.DB.PreparedSQL return is_string( $source ) ? trim( $source ) : ''; } /** * 批量取路径译文(整页回填链接时用,避免逐条查库)。 * * @param string[] $paths 原文路径列表。 * @param string $from 源语言。 * @param string $to 目标语言。 * @return array 原文路径 => 译文路径 */ public static function uri_translation_map( array $paths, $from, $to ) { global $wpdb; $md5s = array(); foreach ( $paths as $path ) { $md5s[ md5( (string) $path ) ] = (string) $path; } if ( empty( $md5s ) ) { return array(); } $table = self::table(); $placeholders = implode( ',', array_fill( 0, count( $md5s ), '%s' ) ); $params = array_merge( array( $from, $to ), array_keys( $md5s ) ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT text_source, text_trans FROM {$table} WHERE language_from = %s AND language_to = %s AND type = 'uri' AND source_md5 IN ({$placeholders})", // phpcs:ignore WordPress.DB.PreparedSQL $params ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL $out = array(); foreach ( (array) $rows as $r ) { if ( '' !== trim( (string) $r['text_trans'] ) ) { $out[ (string) $r['text_source'] ] = trim( (string) $r['text_trans'] ); } } return $out; } /** * 按来源页面统计指定目标语言下的已译条数(页面下拉的「已翻译」标记)。 * * source_url 记录的是该原文最后一次被翻译时所在的页面,同一段文本出现在 * 多个页面时只会记住最后一个,所以这个数字用于「有没有翻译过」的提示, * 精确的覆盖率由页面加载时实时比对得出。 * * @param string $to 目标语言。 * @return array source_url => 已译条数 */ public static function translated_url_counts( $to ) { global $wpdb; $table = self::table(); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT source_url, COUNT(*) AS cnt FROM {$table} WHERE language_to = %s AND text_trans <> '' AND source_url IS NOT NULL AND source_url <> '' GROUP BY source_url", // phpcs:ignore WordPress.DB.PreparedSQL $to ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL $out = array(); foreach ( (array) $rows as $r ) { $out[ (string) $r['source_url'] ] = (int) $r['cnt']; } return $out; } /** * 删除指定语言对下某来源页面的全部译文记录。 * * @param string $url 来源页面 URL。 * @param string $from 源语言。 * @param string $to 目标语言。 * @return int 删除条数。 */ public static function delete_by_url( $url, $from, $to ) { global $wpdb; return (int) $wpdb->delete( self::table(), array( 'source_url' => (string) $url, 'language_from' => (string) $from, 'language_to' => (string) $to, ), array( '%s', '%s', '%s' ) ); } /** * 取库中已存在的引擎名列表(用于筛选下拉)。 * * @return string[] */ public static function distinct_engines() { global $wpdb; $table = self::table(); $rows = $wpdb->get_col( "SELECT DISTINCT engine FROM {$table} WHERE engine <> '' ORDER BY engine" ); // phpcs:ignore WordPress.DB.PreparedSQL return (array) $rows; } }