初始化项目:词曲 & 音眼资产导入工具
- import_hk_songs.py: 主数据导入脚本 - lyric_dedup/: 歌词去重核心模块 - test_dedup.py: 单元测试 & L2 benchmark - serve_l2_dashboard.py + l2_review_dashboard.html: 复核前端 - .gitignore: 排除 .env、数据文件、缓存、输出目录 - README.md: 项目说明文档
0 parents
Showing
16 changed files
with
438 additions
and
0 deletions
.env.example
0 → 100644
| 1 | # 源库 - 音眼正式 | ||
| 2 | SOURCE_DB_HOST= | ||
| 3 | SOURCE_DB_PORT=3306 | ||
| 4 | SOURCE_DB_USER= | ||
| 5 | SOURCE_DB_PASSWORD= | ||
| 6 | SOURCE_DB_NAME= | ||
| 7 | |||
| 8 | # 目标库 - 词曲库 | ||
| 9 | TARGET_DB_HOST= | ||
| 10 | TARGET_DB_PORT=3306 | ||
| 11 | TARGET_DB_USER= | ||
| 12 | TARGET_DB_PASSWORD= | ||
| 13 | TARGET_DB_NAME= | ||
| 14 | TARGET_TABLE_NAME=hk_songs | ||
| 15 | |||
| 16 | # OSS 配置 | ||
| 17 | OSS_ACCESS_KEY_ID= | ||
| 18 | OSS_ACCESS_KEY_SECRET= | ||
| 19 | OSS_ENDPOINT= | ||
| 20 | OSS_BUCKET_NAME= | ||
| 21 | OSS_FILE_BASE_NAME= |
.gitignore
0 → 100644
README.md
0 → 100644
| 1 | # 词曲 & 音眼资产导入工具 | ||
| 2 | |||
| 3 | 本项目用于将音眼(Hikoon)数据库中的歌词和歌曲数据去重、比对后,导入至词曲库的 `hk_songs` 表,并提供 L2 歌词去重测试和人工复核前端。 | ||
| 4 | |||
| 5 | ## 项目结构 | ||
| 6 | |||
| 7 | ``` | ||
| 8 | . | ||
| 9 | ├── import_hk_songs.py # 主脚本:聚合查询、去重、导入 | ||
| 10 | ├── lyric_dedup/ # 歌词去重核心模块 | ||
| 11 | │ ├── normalization.py # 歌词文本规范化 | ||
| 12 | │ ├── checker.py # 去重匹配逻辑 | ||
| 13 | │ ├── eval_dataset.py # 评测数据集构建 | ||
| 14 | │ ├── file_import.py # 文件导入工具 | ||
| 15 | │ └── cli.py # CLI 入口 | ||
| 16 | ├── test_dedup.py # 单元测试 & L2 benchmark | ||
| 17 | ├── serve_l2_dashboard.py # L2 测试复核前端服务 | ||
| 18 | ├── l2_review_dashboard.html # 前端页面 | ||
| 19 | ├── requirements.txt # Python 依赖 | ||
| 20 | ├── 测试流程指南.md # L2 测试流程说明 | ||
| 21 | └── .env # 数据库配置(不纳入版本控制) | ||
| 22 | ``` | ||
| 23 | |||
| 24 | ## 快速开始 | ||
| 25 | |||
| 26 | ### 1. 安装依赖 | ||
| 27 | |||
| 28 | ```bash | ||
| 29 | pip install -r requirements.txt | ||
| 30 | ``` | ||
| 31 | |||
| 32 | ### 2. 配置数据库 | ||
| 33 | |||
| 34 | 复制 `.env.example` 为 `.env` 并填写数据库连接信息: | ||
| 35 | |||
| 36 | ```env | ||
| 37 | YINYAN_DB_HOST=... | ||
| 38 | YINYAN_DB_PORT=3306 | ||
| 39 | YINYAN_DB_USER=... | ||
| 40 | YINYAN_DB_PASSWORD=... | ||
| 41 | YINYAN_DB_NAME=... | ||
| 42 | |||
| 43 | CIKU_DB_HOST=... | ||
| 44 | CIKU_DB_PORT=3306 | ||
| 45 | CIKU_DB_USER=... | ||
| 46 | CIKU_DB_PASSWORD=... | ||
| 47 | CIKU_DB_NAME=... | ||
| 48 | ``` | ||
| 49 | |||
| 50 | ### 3. 运行数据导入 | ||
| 51 | |||
| 52 | ```bash | ||
| 53 | python import_hk_songs.py --help | ||
| 54 | ``` | ||
| 55 | |||
| 56 | ### 4. 运行 L2 歌词去重测试 | ||
| 57 | |||
| 58 | 小样本验证: | ||
| 59 | |||
| 60 | ```bash | ||
| 61 | RUN_L2_BENCHMARK=1 \ | ||
| 62 | L2_BENCHMARK_LIMIT=20 \ | ||
| 63 | L2_BENCHMARK_EXISTING_LIMIT=50 \ | ||
| 64 | L2_BENCHMARK_TOPKS=20,100 \ | ||
| 65 | python -m pytest test_dedup.py::TestL2Benchmark::test_l2_recall_topk_efficiency_and_review_artifacts -q -s | ||
| 66 | ``` | ||
| 67 | |||
| 68 | 详见 [测试流程指南.md](./测试流程指南.md)。 | ||
| 69 | |||
| 70 | ### 5. 启动复核前端 | ||
| 71 | |||
| 72 | ```bash | ||
| 73 | python serve_l2_dashboard.py --host 127.0.0.1 --port 8765 | ||
| 74 | ``` | ||
| 75 | |||
| 76 | 然后访问 http://127.0.0.1:8765 查看测试报告、比对歌词、人工标注。 | ||
| 77 | |||
| 78 | ## 普通单元测试 | ||
| 79 | |||
| 80 | ```bash | ||
| 81 | python -m pytest test_dedup.py -q | ||
| 82 | ``` | ||
| 83 | |||
| 84 | ## 注意事项 | ||
| 85 | |||
| 86 | - `.env` 文件包含数据库凭据,**不要**提交到仓库。 | ||
| 87 | - `output/` 目录存放测试生成的报告和歌词文件,不纳入版本控制。 | ||
| 88 | - CSV / SQL / Excel 等数据文件不纳入版本控制。 |
import_hk_songs.py
0 → 100644
This diff is collapsed.
Click to expand it.
l2_review_dashboard.html
0 → 100644
This diff is collapsed.
Click to expand it.
lyric_dedup/__init__.py
0 → 100644
| 1 | """Lyric duplicate detection utilities.""" | ||
| 2 | |||
| 3 | from lyric_dedup.checker import DuplicateCheckResult | ||
| 4 | from lyric_dedup.checker import DuplicateChecker | ||
| 5 | from lyric_dedup.checker import DuplicateDecision | ||
| 6 | from lyric_dedup.checker import LyricRecord | ||
| 7 | |||
| 8 | __all__ = [ | ||
| 9 | "DuplicateCheckResult", | ||
| 10 | "DuplicateChecker", | ||
| 11 | "DuplicateDecision", | ||
| 12 | "LyricRecord", | ||
| 13 | ] |
lyric_dedup/checker.py
0 → 100644
This diff is collapsed.
Click to expand it.
lyric_dedup/cli.py
0 → 100644
| 1 | """PostgreSQL-backed command line tools for lyric duplicate checking.""" | ||
| 2 | |||
| 3 | from __future__ import annotations | ||
| 4 | |||
| 5 | import argparse | ||
| 6 | import json | ||
| 7 | from pathlib import Path | ||
| 8 | |||
| 9 | from lyric_dedup.eval_dataset import generate_eval_set | ||
| 10 | from lyric_dedup.file_import import record_from_file | ||
| 11 | |||
| 12 | |||
| 13 | def main() -> None: | ||
| 14 | parser = argparse.ArgumentParser(prog="lyric-dedup") | ||
| 15 | subparsers = parser.add_subparsers(dest="command", required=True) | ||
| 16 | |||
| 17 | check = subparsers.add_parser("check-file", help="check one .lrc/.txt file using PostgreSQL recall") | ||
| 18 | check.add_argument("--dsn", default="postgresql:///lyric_dedup") | ||
| 19 | check.add_argument("--file", required=True) | ||
| 20 | check.add_argument("--max-candidates", type=int, default=5) | ||
| 21 | check.add_argument("--recall-limit", type=int, default=100) | ||
| 22 | check.add_argument("--enable-trgm", action="store_true") | ||
| 23 | check.add_argument("--trgm-threshold", type=float, default=0.3) | ||
| 24 | check.add_argument("--statement-timeout-ms", type=int, default=5000) | ||
| 25 | |||
| 26 | generate = subparsers.add_parser("generate-eval-set", help="generate labeled eval samples from a lyric library") | ||
| 27 | generate.add_argument("--library-dir", required=True) | ||
| 28 | generate.add_argument("--lyrics-dir", required=True) | ||
| 29 | generate.add_argument("--csv", required=True) | ||
| 30 | generate.add_argument("--size", type=int, default=100) | ||
| 31 | generate.add_argument("--positive-ratio", type=float, default=0.3) | ||
| 32 | generate.add_argument("--seed", type=int, default=20260602) | ||
| 33 | generate.add_argument( | ||
| 34 | "--profile", | ||
| 35 | choices=("standard", "hard"), | ||
| 36 | default="standard", | ||
| 37 | help="evaluation sample profile: standard production mix or harder business-realistic edge mix", | ||
| 38 | ) | ||
| 39 | |||
| 40 | args = parser.parse_args() | ||
| 41 | if args.command == "check-file": | ||
| 42 | check_file_pg(args) | ||
| 43 | elif args.command == "generate-eval-set": | ||
| 44 | summary = generate_eval_set( | ||
| 45 | library_dir=Path(args.library_dir), | ||
| 46 | output_dir=Path(args.lyrics_dir), | ||
| 47 | csv_path=Path(args.csv), | ||
| 48 | size=args.size, | ||
| 49 | positive_ratio=args.positive_ratio, | ||
| 50 | seed=args.seed, | ||
| 51 | profile=args.profile, | ||
| 52 | ) | ||
| 53 | print(json.dumps(summary, ensure_ascii=False)) | ||
| 54 | |||
| 55 | |||
| 56 | def check_file_pg(args: argparse.Namespace) -> None: | ||
| 57 | from dedup_server.config import ServerConfig | ||
| 58 | from dedup_server.service import DedupService | ||
| 59 | |||
| 60 | record = record_from_file(Path(args.file)) | ||
| 61 | config = ServerConfig( | ||
| 62 | dsn=args.dsn, | ||
| 63 | max_candidates=args.max_candidates, | ||
| 64 | recall_limit=args.recall_limit, | ||
| 65 | enable_trgm=args.enable_trgm, | ||
| 66 | trgm_threshold=args.trgm_threshold, | ||
| 67 | statement_timeout_ms=args.statement_timeout_ms, | ||
| 68 | ) | ||
| 69 | service = DedupService(config=config) | ||
| 70 | result = service.check(record.lyrics, title=record.title, artist=record.artist) | ||
| 71 | print( | ||
| 72 | json.dumps( | ||
| 73 | { | ||
| 74 | "source": args.file, | ||
| 75 | "decision": result.decision, | ||
| 76 | "duplicate": result.duplicate, | ||
| 77 | "confidence": result.confidence, | ||
| 78 | "reason": result.reason, | ||
| 79 | "candidate_count": result.candidate_count, | ||
| 80 | }, | ||
| 81 | ensure_ascii=False, | ||
| 82 | indent=2, | ||
| 83 | ) | ||
| 84 | ) | ||
| 85 | |||
| 86 | |||
| 87 | if __name__ == "__main__": | ||
| 88 | main() |
lyric_dedup/eval_dataset.py
0 → 100644
This diff is collapsed.
Click to expand it.
lyric_dedup/file_import.py
0 → 100644
| 1 | """Import LRC/TXT lyric files into records.""" | ||
| 2 | |||
| 3 | from __future__ import annotations | ||
| 4 | |||
| 5 | import hashlib | ||
| 6 | from pathlib import Path | ||
| 7 | |||
| 8 | from lyric_dedup.checker import LyricRecord | ||
| 9 | |||
| 10 | |||
| 11 | SUPPORTED_SUFFIXES = {".lrc", ".txt"} | ||
| 12 | |||
| 13 | |||
| 14 | def iter_lyric_files(root: str | Path) -> list[Path]: | ||
| 15 | base = Path(root) | ||
| 16 | return sorted( | ||
| 17 | path | ||
| 18 | for path in base.rglob("*") | ||
| 19 | if path.is_file() and path.suffix.lower() in SUPPORTED_SUFFIXES | ||
| 20 | ) | ||
| 21 | |||
| 22 | |||
| 23 | def read_lyric_file(path: str | Path) -> str: | ||
| 24 | file_path = Path(path) | ||
| 25 | data = file_path.read_bytes() | ||
| 26 | for encoding in ("utf-8-sig", "utf-8", "gb18030", "big5"): | ||
| 27 | try: | ||
| 28 | return data.decode(encoding) | ||
| 29 | except UnicodeDecodeError: | ||
| 30 | continue | ||
| 31 | return data.decode("utf-8", errors="replace") | ||
| 32 | |||
| 33 | |||
| 34 | def record_from_file(path: str | Path, *, base_dir: str | Path | None = None) -> LyricRecord: | ||
| 35 | file_path = Path(path) | ||
| 36 | lyrics = read_lyric_file(file_path) | ||
| 37 | title, artist = _metadata_from_name(file_path.stem) | ||
| 38 | record_id = _record_id(file_path, base_dir) | ||
| 39 | return LyricRecord(record_id=record_id, lyrics=lyrics, title=title, artist=artist) | ||
| 40 | |||
| 41 | |||
| 42 | def records_from_dir(root: str | Path) -> list[LyricRecord]: | ||
| 43 | return [record_from_file(path, base_dir=root) for path in iter_lyric_files(root)] | ||
| 44 | |||
| 45 | |||
| 46 | def _record_id(path: Path, base_dir: str | Path | None) -> str: | ||
| 47 | if base_dir is None: | ||
| 48 | source = str(path.resolve()) | ||
| 49 | else: | ||
| 50 | source = str(path.resolve().relative_to(Path(base_dir).resolve())) | ||
| 51 | digest = hashlib.sha1(source.encode("utf-8")).hexdigest()[:12] | ||
| 52 | return f"{digest}:{source}" | ||
| 53 | |||
| 54 | |||
| 55 | def _metadata_from_name(stem: str) -> tuple[str | None, str | None]: | ||
| 56 | cleaned = stem.removesuffix("-歌词").removesuffix("_歌词").removesuffix(" 歌词").strip() | ||
| 57 | if " - " in cleaned: | ||
| 58 | artist, title = cleaned.split(" - ", 1) | ||
| 59 | return title.strip() or None, artist.strip() or None | ||
| 60 | for sep in ("-", "_"): | ||
| 61 | if sep in cleaned: | ||
| 62 | title, artist = cleaned.rsplit(sep, 1) | ||
| 63 | return title.strip() or None, artist.strip() or None | ||
| 64 | return stem.strip() or None, None |
lyric_dedup/normalization.py
0 → 100644
This diff is collapsed.
Click to expand it.
requirements.txt
0 → 100644
serve_l2_dashboard.py
0 → 100644
This diff is collapsed.
Click to expand it.
test_dedup.py
0 → 100644
This diff is collapsed.
Click to expand it.
测试流程指南.md
0 → 100644
| 1 | # L2 歌词去重测试流程指南 | ||
| 2 | |||
| 3 | 本文档说明如何运行 L2 歌词召回测试、查看测试产物,并启动前端页面做人工复核。 | ||
| 4 | |||
| 5 | ## 1. 运行 L2 测试脚本 | ||
| 6 | |||
| 7 | 在项目根目录执行: | ||
| 8 | |||
| 9 | ```bash | ||
| 10 | RUN_L2_BENCHMARK=1 \ | ||
| 11 | L2_BENCHMARK_LIMIT=20000000 \ | ||
| 12 | L2_BENCHMARK_OFFSET=0 \ | ||
| 13 | L2_BENCHMARK_LOAD_EXISTING=1 \ | ||
| 14 | L2_BENCHMARK_EXISTING_LIMIT=50000 \ | ||
| 15 | L2_BENCHMARK_DOWNLOAD_WORKERS=32 \ | ||
| 16 | L2_BENCHMARK_TOPKS=20 \ | ||
| 17 | L2_BENCHMARK_RETRIEVAL_TOP_N=10 \ | ||
| 18 | python -m pytest test_dedup.py::TestL2Benchmark::test_l2_recall_topk_efficiency_and_review_artifacts -q -s | ||
| 19 | ``` | ||
| 20 | |||
| 21 | 这个测试只处理歌词: | ||
| 22 | |||
| 23 | - 从源库读取待导入歌词。 | ||
| 24 | - 可选从目标测试库读取已有 `lyrics_url` 并下载歌词文件。 | ||
| 25 | - 不下载或上传音频、封面、伴奏、曲谱等资源。 | ||
| 26 | - 不写入数据库。 | ||
| 27 | - 对不同 `top_k` 召回配置输出效率指标和人工复核材料。 | ||
| 28 | |||
| 29 | ## 2. 参数说明 | ||
| 30 | |||
| 31 | | 参数 | 默认值 | 说明 | | ||
| 32 | | --------------------------------- | --------------------- | ------------------------------------------------------------------------------- | | ||
| 33 | | `RUN_L2_BENCHMARK` | 未开启 | 必须设为 `1` 才会运行联网 benchmark;否则 pytest 会跳过该测试。 | | ||
| 34 | | `L2_BENCHMARK_LIMIT` | `200` | 从源库查询多少条待评测歌曲。 | | ||
| 35 | | `L2_BENCHMARK_OFFSET` | `0` | 源库查询偏移量,用于分段抽样。 | | ||
| 36 | | `L2_BENCHMARK_LOAD_EXISTING` | `1` | 是否加载目标测试库已有歌词作为历史候选。设为 `0` 时只测试本批新歌之间的召回。 | | ||
| 37 | | `L2_BENCHMARK_EXISTING_LIMIT` | `500` | 从目标测试库加载多少条已有歌词候选;设为 `0` 表示不加 SQL `LIMIT`。 | | ||
| 38 | | `L2_BENCHMARK_TOPKS` | `20,50,100,200,500` | 要对比的 topK 召回候选规模,逗号分隔。 | | ||
| 39 | | `L2_BENCHMARK_RETRIEVAL_TOP_N` | `10` | 每条新歌在结果表中保留前多少个候选。前端当前按 top10 展示。 | | ||
| 40 | | `L2_BENCHMARK_DOWNLOAD_TIMEOUT` | `10` | 下载已有歌词文件的超时时间,单位秒。 | | ||
| 41 | |||
| 42 | ## 3. 输出结果 | ||
| 43 | |||
| 44 | 测试完成后会在 `output/reports/` 下生成: | ||
| 45 | |||
| 46 | ```text | ||
| 47 | l2_topk_benchmark_summary_YYYYMMDD_HHMMSS.csv | ||
| 48 | l2_topk_retrieval_top10_YYYYMMDD_HHMMSS.csv | ||
| 49 | l2_topk_duplicate_hits_YYYYMMDD_HHMMSS.csv | ||
| 50 | l2_topk_benchmark_assets_YYYYMMDD_HHMMSS/lyrics/ | ||
| 51 | ``` | ||
| 52 | |||
| 53 | 各文件含义: | ||
| 54 | |||
| 55 | | 文件 | 说明 | | ||
| 56 | | -------------------------------------- | -------------------------------------------------------------------------------------------------------- | | ||
| 57 | | `l2_topk_benchmark_summary_*.csv` | 每个 topK 的效率汇总,包括耗时、吞吐、平均召回候选数、duplicate/review/new/hit 数量。 | | ||
| 58 | | `l2_topk_retrieval_top10_*.csv` | 每条新歌的 topN 召回候选明细,包含新歌和候选的歌名、歌手、词曲作者、歌词文件路径、相似度指标、判定原因。 | | ||
| 59 | | `l2_topk_duplicate_hits_*.csv` | 只保留命中 `duplicate` 或 `review` 的样本,适合人工重点复核。 | | ||
| 60 | | `l2_topk_benchmark_assets_*/lyrics/` | 新歌和候选歌词正文文件。CSV 中只保留路径,不直接塞歌词全文。 | | ||
| 61 | |||
| 62 | `l2_topk_retrieval_top10_*.csv` 还包含: | ||
| 63 | |||
| 64 | - `l1_metadata_match`:新歌与候选按 L1 元数据规则(歌名、作词人、作曲人)是否命中。 | ||
| 65 | - `l1_l2_conflict`:L1 与 L2 结论是否冲突。比如 L2 判新歌但召回候选中有 L1 命中,或 L2 判重复但命中候选 L1 未命中。 | ||
| 66 | |||
| 67 | ## 4. 启动前端页面 | ||
| 68 | |||
| 69 | 运行: | ||
| 70 | |||
| 71 | ```bash | ||
| 72 | python serve_l2_dashboard.py --host 127.0.0.1 --port 8765 | ||
| 73 | ``` | ||
| 74 | |||
| 75 | 然后打开: | ||
| 76 | |||
| 77 | ```text | ||
| 78 | http://127.0.0.1:8765 | ||
| 79 | ``` | ||
| 80 | |||
| 81 | 前端会自动扫描 `output/reports/` 下成套的 benchmark 结果文件。 | ||
| 82 | |||
| 83 | ## 5. 前端使用方式 | ||
| 84 | |||
| 85 | 页面主要功能: | ||
| 86 | |||
| 87 | - 选择不同测试 run。 | ||
| 88 | - 切换不同 `top_k`。 | ||
| 89 | - 查看效率指标:耗时、吞吐、平均召回候选数、duplicate/review/new/hit 数量。 | ||
| 90 | - 在“召回样本”中查看每条新歌的 topN 候选。 | ||
| 91 | - 在“命中样本”中只查看 `duplicate` / `review` 样本。 | ||
| 92 | - 左右并排查看新歌歌词和候选歌词。 | ||
| 93 | - 高亮 L1/L2 冲突样本。 | ||
| 94 | - 对样本做人工标注:确认重复、确认不重复、待确认。 | ||
| 95 | - 点击“导出标注”导出当前 run、topK 和当前视图下的人工标注 CSV。 | ||
| 96 | - 按歌名、歌手、ID 搜索样本。 | ||
| 97 | |||
| 98 | 如果刚跑完新测试但页面没有更新,点击页面右上角“刷新”。 | ||
| 99 | |||
| 100 | ## 6. 推荐测试方式 | ||
| 101 | |||
| 102 | 先用小样本确认流程: | ||
| 103 | |||
| 104 | ```bash | ||
| 105 | RUN_L2_BENCHMARK=1 \ | ||
| 106 | L2_BENCHMARK_LIMIT=20 \ | ||
| 107 | L2_BENCHMARK_EXISTING_LIMIT=50 \ | ||
| 108 | L2_BENCHMARK_TOPKS=20,100 \ | ||
| 109 | python -m pytest test_dedup.py::TestL2Benchmark::test_l2_recall_topk_efficiency_and_review_artifacts -q -s | ||
| 110 | ``` | ||
| 111 | |||
| 112 | 再扩大样本: | ||
| 113 | |||
| 114 | ```bash | ||
| 115 | RUN_L2_BENCHMARK=1 \ | ||
| 116 | L2_BENCHMARK_LIMIT=1000 \ | ||
| 117 | L2_BENCHMARK_EXISTING_LIMIT=5000 \ | ||
| 118 | L2_BENCHMARK_TOPKS=20,50,100,200,500 \ | ||
| 119 | python -m pytest test_dedup.py::TestL2Benchmark::test_l2_recall_topk_efficiency_and_review_artifacts -q -s | ||
| 120 | ``` | ||
| 121 | |||
| 122 | 如果目标库已有歌词很多,`L2_BENCHMARK_EXISTING_LIMIT=0` 会加载全部候选,运行时间和歌词下载时间会明显增加。 | ||
| 123 | |||
| 124 | ## 7. 普通单元测试 | ||
| 125 | |||
| 126 | 运行全部普通测试: | ||
| 127 | |||
| 128 | ```bash | ||
| 129 | python -m pytest test_dedup.py -q | ||
| 130 | ``` | ||
| 131 | |||
| 132 | 默认情况下,L2 benchmark 会被跳过,不会联网。 |
音眼数据库配置.md
0 → 100644
| 1 | 音眼数据库配置: | ||
| 2 | |||
| 3 | 测试:rm-bp18h64ad9ak4d7h5do.mysql.rds.aliyuncs.com 3306 root Hikoon123! hikoon-data-test | ||
| 4 | |||
| 5 | 正式:username: yinyan360 | ||
| 6 | password: yinyan_hikoon!@#6699 内网: rm-bp10xcwu0930i0h00.mysql.rds.aliyuncs.com 外网:rm-bp10xcwu0930i0h00ro.mysql.rds.aliyuncs.com 端口:3306 | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
-
Please register or sign in to post a comment