refactor: split workspace and delivery layout

This commit is contained in:
yuanzhen869
2026-03-19 13:00:40 +08:00
parent f12b3d5ecd
commit 74e50a2b30
83 changed files with 592 additions and 896 deletions

View File

@@ -1,5 +1,6 @@
.git
.DS_Store
delivery/.env
__pycache__
*.pyc
*.pyo

View File

@@ -1,49 +0,0 @@
name: Daily Upstream Sync
on:
workflow_dispatch:
schedule:
- cron: "0 1 * * *"
permissions:
contents: write
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Capture upstream commit
id: upstream
run: |
echo "commit=$(git ls-remote https://github.com/KHwang9883/MobileModels.git refs/heads/master | awk '{print $1}')" >> "$GITHUB_OUTPUT"
- name: Sync upstream raw data
run: |
python3 tools/sync_upstream_mobilemodels.py --build-index --export-mysql-seed
- name: Commit changes
env:
UPSTREAM_COMMIT: ${{ steps.upstream.outputs.commit }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add brands misc README.md README_en.md CHANGELOG.md CHANGELOG_en.md LICENSE.txt dist/device_index.json dist/mobilemodels_mysql_seed.sql
if git diff --cached --quiet; then
echo "No upstream changes to commit."
exit 0
fi
git commit -m "chore: sync upstream raw data ${UPSTREAM_COMMIT::7}"
- name: Push changes
run: git push

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.DS_Store
.env

162
README.md
View File

@@ -1,157 +1,37 @@
# 手机品牌型号汇总
# MobileModels Workspace
[![issues](https://img.shields.io/github/issues/KHwang9883/MobileModels?color=green)](https://github.com/KHwang9883/MobileModels/issues)
[![prs](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/KHwang9883/MobileModels/pulls)
[![stars](https://img.shields.io/github/stars/KHwang9883/MobileModels.svg?color=yellow)](https://github.com/KHwang9883/MobileModels)
[![forks](https://img.shields.io/github/forks/KHwang9883/MobileModels.svg?color=orange)](https://github.com/KHwang9883/MobileModels)
[![License: CC BY-NC-SA 4.0](https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc-sa/4.0/)
当前仓库按两层结构组织:
汇总各厂商上市的手机型号与对应的传播名。
- `workspace/`
- 工作空间
- 存放上游原始数据、补充资料和历史变更文件
- `delivery/`
- 交付物
- 存放可直接运行的 Docker Compose 项目、Web 页面、MySQL schema 和交付文档
[English](README_en.md)
## 使用方式
## Web UI
项目内置了设备查询与数据管理页面,统一通过 `docker compose` 启动:
进入交付目录启动:
```bash
cd delivery
docker compose up --build -d
```
打开
页面入口
- `http://127.0.0.1:8123/web/device_query.html`
- `http://127.0.0.1:8123/web/brand_management.html`
- `http://127.0.0.1:8123/web/device_query.html?view=docs`
MySQL 也会一并启动:
## 目录说明
- host: `127.0.0.1`
- port: `3306`
- database: `mobilemodels`
- reader user: `mobilemodels_reader`
```text
workspace/ 原始数据与工作空间
delivery/ 可交付运行物
```
如需自定义 MySQL 账号密码,可先复制 `.env.example``.env` 后再启动。
原始数据、索引和 MySQL seed 运行时持久化在 Docker volume 中,不再回写本地工作区。
更多交付说明见:
更多说明参见 [web/README.md](web/README.md)
- ✅ 包含
- ⏹ 仅部分包含
- ❌ 不包含
| 名称 | 品牌 | 汇总范围 | codename | 海外机型 | 备注 |
| :-: | :-: | :-: | :-: | :-: | :-: |
| [360shouji](brands/360shouji.md) | 360 手机 | 全部 360/奇酷手机 | ❌ | ❌ | -- |
| [apple_all](brands/apple_all.md) | Apple | 全部 iPhone/iPad/iPod touch/Apple Watch/Apple TV/Apple Vision | ✅ | ✅ | -- |
| [apple_all_en](brands/apple_all_en.md) | Apple | 全部 iPhone/iPad/iPod touch/Apple Watch/Apple TV/Apple Vision | ✅ | ✅ | 英文版 |
| [apple_cn](brands/apple_cn.md) | Apple | 全部国行 iPhone/iPad/iPod touch/Apple Watch/Apple Vision | ✅ | ❌ | -- |
| [asus_cn](brands/asus_cn.md) | 华硕 (ASUS) | ROG Phone 等 | ✅ | ❌ | -- |
| [asus_en](brands/asus_en.md) | 华硕 (ASUS) | ROG Phone/Zenfone | ✅ | ✅ | 英文版 |
| [blackshark](brands/blackshark.md) | 黑鲨 (Black Shark) | 全部机型 | ✅ | ✅ | -- |
| [blackshark_en](brands/blackshark_en.md) | 黑鲨 (Black Shark) | 全部机型 | ✅ | ✅ | 英文版 |
| [coolpad](brands/coolpad.md) | 酷派 (Coolpad) | 酷派近年智能手机机型 | ❌ | ❌ | -- |
| [google](brands/google.md) | Google | Google Pixel 手机/平板/手表 | ✅ | ✅ | 英文版 |
| [honor_cn](brands/honor_cn.md) | 荣耀 (HONOR) | 荣耀手机/平板/笔记本电脑/智慧屏/穿戴设备,仅包含国行型号 | ⏹ | ❌ | -- |
| [honor_global_en](brands/honor_global_en.md)| 荣耀 (HONOR) | 荣耀手机/平板,仅包含海外型号 | ⏹ | ✅ | 英文版 |
| [huawei_cn](brands/huawei_cn.md) | 华为 (HUAWEI) | 华为 Mate/Pura/nova/G/麦芒/畅享系列、平板电脑、MateBook、智慧屏及穿戴设备仅包含国行型号 | ⏹ | ❌ | [其他早期型号参阅此处](misc/early-huawei-models.md) |
| [huawei_global_en](brands/huawei_global_en.md)| 华为 (HUAWEI) | 华为 Mate/Pura/nova/Y 系列及平板电脑,仅包含海外型号 | ⏹ | ⏹ | 英文版 |
| [lenovo_cn](brands/lenovo_cn.md) | 联想 (Lenovo) | 联想品牌 2017 年起上市的机型、ZUK 全部机型 | ✅ | ❌ | -- |
| [letv](brands/letv.md) | 乐视 (Letv) | 全部手机机型 | ❌ | ❌ | 不包含电视产品 |
| [meizu](brands/meizu.md) | 魅族 (MEIZU) | 全部机型 | ✅ | ✅ | -- |
| [meizu_en](brands/meizu_en.md) | 魅族 (MEIZU) | 全部机型 | ✅ | ✅ | 英文版 |
| [mitv_cn](brands/mitv_cn.md) | 小米 (Xiaomi) | 全部国行小米/Redmi 电视、机顶盒 | ❌ | ❌ | -- |
| [mitv_global_en](brands/mitv_global_en.md) | 小米 (Xiaomi) | 全部小米/Redmi 电视、机顶盒、智能电视棒,仅包含海外型号 | ❌ | ✅ | 英文版 |
| [motorola_cn](brands/motorola_cn.md) | 摩托罗拉 (Motorola) | 2015 年起上市的机型 | ✅ | ❌ | -- |
| [nokia_cn](brands/nokia_cn.md) | 诺基亚 (Nokia) | 2017 年起由 HMD Global 制造的智能手机机型 | ✅ | ❌ | -- |
| [nothing](brands/nothing.md) | Nothing | 全部机型 | ✅ | ✅ | 英文版 |
| [nubia](brands/nubia.md) | 努比亚 (nubia) | 全部机型 | ❌ | ⏹ | -- |
| [oneplus](brands/oneplus.md) | 一加 (OnePlus) | 全部机型 | ✅ | ✅ | -- |
| [oneplus_en](brands/oneplus_en.md) | 一加 (OnePlus) | 全部机型 | ✅ | ✅ | 英文版 |
| [oppo_cn](brands/oppo_cn.md) | OPPO | 2018 年起新型号命名方式的国行机型 | ⏹ | ❌ | -- |
| [oppo_global_en](brands/oppo_global_en.md) | OPPO | 2018 年起上市的海外机型 | ⏹ | ⏹ | 英文版 |
| [realme_cn](brands/realme_cn.md) | 真我 (realme) | 全部国行机型 | ⏹ | ❌ | -- |
| [realme_global_en](brands/realme_global_en.md) | 真我 (realme) | 全部海外机型 | ⏹ | ✅ | 英文版 |
| [samsung_cn](brands/samsung_cn.md) | 三星 (Samsung) | Galaxy S/Note/A/Z/M/C/J/On/Tab/心系天下系列及个别其他机型,仅包含国行型号 | ✅ | ❌ | [其他早期型号参阅此处](misc/early-samsung-models.md) |
| [samsung_global_en](brands/samsung_global_en.md) | 三星 (Samsung) | Galaxy S/Note/A/Z/M/F 系列2019 年起上市的机型 | ✅ | ⏹ | 英文版 |
| [smartisan](brands/smartisan.md) | 坚果 (Smartisan) | 全部机型 | ✅ | ❌ | -- |
| [sony](brands/sony.md) | 索尼 (SONY) | 2015 年起上市的机型 | ✅ | ✅ | 英文版 |
| [sony_cn](brands/sony_cn.md) | 索尼 (SONY) | 2015 年起上市的国行机型 | ✅ | ❌ | -- |
| [vivo_cn](brands/vivo_cn.md) | vivo | 2018 年起新型号命名方式的国行机型 | ✅ | ❌ | -- |
| [vivo_global_en](brands/vivo_global_en.md) | vivo | 2019 年起上市的海外机型 | ⏹ | ⏹ | 英文版 |
| [xiaomi](brands/xiaomi.md) | 小米 (Xiaomi) | 小米/REDMI/POCO 手机 & 平板等 | ✅ | ✅ | -- |
| [xiaomi_cn](brands/xiaomi_cn.md) | 小米 (Xiaomi) | 小米/REDMI 手机 & 平板等 | ✅ | ✅ | 英文版 |
| [xiaomi_en](brands/xiaomi_en.md) | 小米 (Xiaomi) | 小米/REDMI/POCO 手机 & 平板等 | ✅ | ✅ | 英文版 |
| [xiaomi-wear](brands/xiaomi-wear.md) | 小米 (Xiaomi) | 小米/Redmi 手表、手环、TWS 等穿戴设备 | ⏹ | ✅ | TWS 不包含外包型号;暂不含儿童手表型号 |
| [zhixuan](brands/zhixuan.md) | 华为智选 | U-MAGIC 优畅享/电信麦芒/NZONE/Hi nova/雷鸟 FFALCON/TD Tech/WIKO | ⏹ | ❌ | -- |
| [zte_cn](brands/zte_cn.md) | 中兴 (ZTE) | 2017 年起上市的机型 | ❌ | ❌ | -- |
## 更新日志
参见 [CHANGELOG.md](CHANGELOG.md)
## 许可
<a rel="license" href="https://creativecommons.org/licenses/by-nc-sa/4.0/"><img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png" /></a><br />本作品采用 <a rel="license" href="https://creativecommons.org/licenses/by-nc-sa/4.0/">知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议</a> 进行许可。
## 项目历史
### 2024 年 3 月
- 将本项目 csv 及脚本迁移至 [此 repo](https://github.com/KHwang9883/MobileModels-csv),使用 GitHub Actions 自动更新。
### 2022 年 4 月
- 新增 [各大 Android 厂商 BL 解锁/内核开源情况](https://github.com/KHwang9883/bootloader-kernel-source) 汇总(已停更)。
### 2021 年 12 月
- 新增 [各品牌型号命名规则](misc/naming-rules.md) 汇总。
### 2019 年 7 月
- 文档版停止维护。
### 2019 年 4 月
- 文档版迁移至 GitHub 直链下载。
- 新增英文版。
### 2019 年 3 月
- 文档版迁移至微云。
### 2018 年 11 月
- 项目同步至 GitHub。
### 2018 年 7 月
- 由于小米社区帖子失效,「手机品牌型号汇总」项目公开发布,提供文档版百度网盘下载。
### 2016 年 3 月
- 小米手机型号汇总发布至 [小米社区](http://bbs.xiaomi.cn/t-12641411)(帖子已失效)。
### 2016 年 2 月
- 我开始汇总一些国内手机品牌的型号,「手机品牌型号汇总」的雏形诞生。
[![Stargazers over time](https://starchart.cc/KHwang9883/MobileModels.svg)](https://starchart.cc/KHwang9883/MobileModels)
## 参考资料
- [电信设备终端网](http://zd.taf.org.cn)
- [产品认证证书查询](http://webdata.cqccms.com.cn/webdata/query/CCCCerti.do)
- [工业和信息化部政务服务平台](https://ythzxfw.miit.gov.cn/resultQuery)
- [产品库-中国电信天翼终端信息平台](http://surfing.tydevice.com/)
- [Google Play 支持的设备](http://storage.googleapis.com/play_public/supported_devices.html)
- [Wi-Fi Alliance](https://www.wi-fi.org)
- [Bluetooth Launch Studio](https://launchstudio.bluetooth.com/Listings/Search)
- [Xiaomi Firmware Updater](https://xiaomifirmwareupdater.com/)
- [Huawei Open Source Release Center](https://consumer.huawei.com/en/opensource/)
- [ReaMEIZU](https://reameizu.com/)
- [The Apple Wiki](https://theapplewiki.com/)
- [ipsw.me](https://ipsw.me)
- [XDA Developers](https://www.xda-developers.com)
- [Huawei Firmware Database](https://pro-teammt.ru/en/online-firmware-database-ru/)
- [XSMS IMEI 数据库](http://xsms.com.ua/phone/imei/all/1)
- [Android Dumps](https://dumps.tadiphone.dev/dumps)
- [Lenovo Android タブレット一覧](https://idomizu.dev/archives/20150)
以及各品牌官网、论坛、微博等,恕不一一列出
## 联系方式
如有相关问题,请 [提交 Issue](https://github.com/KHwang9883/MobileModels/issues)。如有错误或缺漏,欢迎提交 PR。
其他平台同名(@KHwang9883),但不一定回复本 repo 相关问题。
- [delivery/README.md](delivery/README.md)
- [delivery/docs/README.md](delivery/docs/README.md)

View File

@@ -1,85 +1,30 @@
# Mobile Models
# MobileModels Workspace
[![issues](https://img.shields.io/github/issues/KHwang9883/MobileModels?color=green)](https://github.com/KHwang9883/MobileModels/issues)
[![prs](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/KHwang9883/MobileModels/pulls)
[![stars](https://img.shields.io/github/stars/KHwang9883/MobileModels.svg?color=yellow)](https://github.com/KHwang9883/MobileModels)
[![forks](https://img.shields.io/github/forks/KHwang9883/MobileModels.svg?color=orange)](https://github.com/KHwang9883/MobileModels)
[![License: CC BY-NC-SA 4.0](https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc-sa/4.0/)
This repository is organized into two layers:
Collecting device names, models and internal codenames.
- `workspace/`
- source workspace
- upstream raw data, notes, and historical files
- `delivery/`
- delivery artifact
- a ready-to-run Docker Compose project with web UI, MySQL schema, and docs
[Issue submission](https://github.com/KHwang9883/MobileModels/issues) and [Pull Requests](https://github.com/KHwang9883/MobileModels/pulls) are welcomed if you find mistakes.
## Usage
## Web UI
The project ships with device query and data management pages and now runs through `docker compose`:
Run from the delivery directory:
```bash
cd delivery
docker compose up --build -d
```
Open:
Entry pages:
- `http://127.0.0.1:8123/web/device_query.html`
- `http://127.0.0.1:8123/web/brand_management.html`
- `http://127.0.0.1:8123/web/device_query.html?view=docs`
MySQL is started together with the stack:
More delivery details:
- host: `127.0.0.1`
- port: `3306`
- database: `mobilemodels`
- reader user: `mobilemodels_reader`
If you want custom MySQL credentials, copy `.env.example` to `.env` before startup.
Raw source data, rebuilt indexes, and MySQL seed files are persisted in Docker volumes instead of being written back to the local workspace at runtime.
More details: [web/README.md](web/README.md)
Unlisted brands usually not include international models.
| Name | Brand | Range |
| :-: | :-: | :-: |
| [apple_all_en](brands/apple_all_en.md) | Apple | iPhone, iPad, iPod touch, Apple Watch, Apple TV and Apple Vision |
| [asus_en](brands/asus_en.md) | ASUS | ROG Phone, Zenfone |
| [blackshark_en](brands/blackshark_en.md) | Black Shark | All models |
| [google](brands/google.md) | Google | Google Pixel phones, tablets & watch |
| [honor_global_en](brands/honor_global_en.md) | HONOR | All international models |
| [huawei_global_en](brands/huawei_global_en.md) | HUAWEI | HUAWEI Mate, Pura, nova & Y series, MediaPad & MatePad series |
| [meizu_en](brands/meizu_en.md) | Meizu | All models |
| [mitv_global_en](brands/mitv_global_en.md) | Xiaomi | All international/Indian Xiaomi & Redmi TV models (excluding Chinese models) |
| [nothing](brands/nothing.md) | Nothing | All models |
| [oneplus_en](brands/oneplus_en.md) | OnePlus | All models |
| [oppo_global_en](brands/oppo_global_en.md) | OPPO | International models since 2018 |
| [samsung_global_en](brands/samsung_global_en.md) | Samsung | International models since 2019 |
| [sony](brands/sony.md) | Sony | All models since 2015 |
| [realme_global_en](brands/realme_global_en.md) | realme | All international models |
| [vivo_global_en](brands/vivo_global_en.md) | vivo | International models since 2019 |
| [xiaomi_en](xiaomi_en.md) | Xiaomi | Xiaomi/Redmi/POCO phones & tablets |
## Changelog
[CHANGELOG_en.md](CHANGELOG_en.md)
## References
- [TENAA](http://zd.taf.org.cn)
- [CQCCMS](http://webdata.cqccms.com.cn/webdata/query/CCCCerti.do)
- [MIIT](https://ythzxfw.miit.gov.cn/resultQuery)
- [China Telecom Tianyi Devices](http://surfing.tydevice.com/)
- [Google Play Supported Devices](http://storage.googleapis.com/play_public/supported_devices.html)
- [Wi-Fi Alliance](https://www.wi-fi.org)
- [Bluetooth Launch Studio](https://launchstudio.bluetooth.com/Listings/Search)
- [Xiaomi Firmware Updater](https://xiaomifirmwareupdater.com/)
- [Huawei Open Source Release Center](https://consumer.huawei.com/en/opensource/)
- [ReaMEIZU](https://reameizu.com/)
- [The Apple Wiki](https://theapplewiki.com/)
- [ipsw.me](https://ipsw.me)
- [XDA Developers](https://www.xda-developers.com)
- [Huawei Firmware Database](https://pro-teammt.ru/en/online-firmware-database-ru/)
- [XSMS IMEI Database](http://xsms.com.ua/phone/imei/all/1)
- [Android Dumps](https://dumps.tadiphone.dev/dumps)
- [Lenovo Android タブレット一覧](https://idomizu.dev/archives/20150)
## License
<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/"><img alt="Creative Commons License" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png" /></a><br />This work is licensed under a <a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/">Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License</a>.
- [delivery/README.md](delivery/README.md)
- [delivery/docs/README.md](delivery/docs/README.md)

View File

@@ -9,7 +9,10 @@ RUN apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates default-mysql-client \
&& rm -rf /var/lib/apt/lists/*
COPY . /app
COPY workspace /app/workspace
COPY delivery /app/delivery
WORKDIR /app/delivery
EXPOSE 8123

98
delivery/README.md Normal file
View File

@@ -0,0 +1,98 @@
# 手机品牌型号汇总
[![License: CC BY-NC-SA 4.0](https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc-sa/4.0/)
这是交付目录,提供可直接运行的 Docker Compose 项目。
项目目标:
- 提供可直接交付的设备查询与数据管理系统
- 通过容器内流程同步上游原始 git 数据
- 在容器内重建索引并刷新 MySQL
- 形成可独立部署的交付包
[English](README_en.md)
## 运行方式
在当前目录执行:
```bash
docker compose up --build -d
```
打开:
- `http://127.0.0.1:8123/web/device_query.html`
- `http://127.0.0.1:8123/web/brand_management.html`
MySQL 也会一并启动:
- host: `127.0.0.1`
- port: `3306`
- database: `mobilemodels`
- reader user: `mobilemodels_reader`
如需自定义 MySQL 账号密码,可先复制 `.env.example``.env` 后再启动。
## 结构说明
```text
dist/ 构建产物与 MySQL seed
docs/ 交付文档
sql/ MySQL schema
tools/ 容器内构建与同步脚本
web/ 页面与静态资源
```
上游原始数据不放在本目录,而是从仓库根目录下的 `workspace/` 读取。
容器启动后会自动完成:
- 初始化 `workspace/``dist/` 的运行期数据目录
-`workspace/brands` 构建设备索引
- 导出 MySQL seed
- 加载 MySQL schema 与 seed
- 启动 Web 页面与 API
更多说明参见:
- [docs/README.md](docs/README.md)
- [docs/web-ui.md](docs/web-ui.md)
- [docs/mysql-query-design.md](docs/mysql-query-design.md)
- [docs/device-mapper.md](docs/device-mapper.md)
## 工作空间
原始数据与补充资料位于仓库根目录下的 `workspace/`
- `../workspace/brands`
- `../workspace/misc`
- `../workspace/CHANGELOG.md`
- `../workspace/CHANGELOG_en.md`
### 2016 年 3 月
- 小米手机型号汇总发布至 [小米社区](http://bbs.xiaomi.cn/t-12641411)(帖子已失效)。
### 2016 年 2 月
- 我开始汇总一些国内手机品牌的型号,「手机品牌型号汇总」的雏形诞生。
## 参考资料
- [电信设备终端网](http://zd.taf.org.cn)
- [产品认证证书查询](http://webdata.cqccms.com.cn/webdata/query/CCCCerti.do)
- [工业和信息化部政务服务平台](https://ythzxfw.miit.gov.cn/resultQuery)
- [产品库-中国电信天翼终端信息平台](http://surfing.tydevice.com/)
- [Google Play 支持的设备](http://storage.googleapis.com/play_public/supported_devices.html)
- [Wi-Fi Alliance](https://www.wi-fi.org)
- [Bluetooth Launch Studio](https://launchstudio.bluetooth.com/Listings/Search)
- [Xiaomi Firmware Updater](https://xiaomifirmwareupdater.com/)
- [Huawei Open Source Release Center](https://consumer.huawei.com/en/opensource/)
- [ReaMEIZU](https://reameizu.com/)
- [The Apple Wiki](https://theapplewiki.com/)
- [ipsw.me](https://ipsw.me)
- [XDA Developers](https://www.xda-developers.com)
- [Huawei Firmware Database](https://pro-teammt.ru/en/online-firmware-database-ru/)
- [XSMS IMEI 数据库](http://xsms.com.ua/phone/imei/all/1)
- [Android Dumps](https://dumps.tadiphone.dev/dumps)
- [Lenovo Android タブレット一覧](https://idomizu.dev/archives/20150)
以及各品牌官网、论坛、微博等,恕不一一列出

61
delivery/README_en.md Normal file
View File

@@ -0,0 +1,61 @@
# Mobile Models
[![License: CC BY-NC-SA 4.0](https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc-sa/4.0/)
This is the delivery directory and contains the ready-to-run Docker Compose project.
Project goals:
- provide a ready-to-deliver device query system
- sync upstream raw git data inside containers
- rebuild index and refresh MySQL inside containers
- keep deployment content independent from workspace files
## Run
The project ships with device query and data management pages and now runs through `docker compose`:
```bash
docker compose up --build -d
```
Open:
- `http://127.0.0.1:8123/web/device_query.html`
- `http://127.0.0.1:8123/web/brand_management.html`
MySQL is started together with the stack:
- host: `127.0.0.1`
- port: `3306`
- database: `mobilemodels`
- reader user: `mobilemodels_reader`
If you want custom MySQL credentials, copy `.env.example` to `.env` before startup.
## Structure
```text
dist/ build outputs and MySQL seed
docs/ delivery docs
sql/ MySQL schema
tools/ container-side build and sync scripts
web/ delivered web pages
```
Raw source files are read from the repository root `workspace/` directory.
More details:
- [docs/README.md](docs/README.md)
- [docs/web-ui.md](docs/web-ui.md)
- [docs/mysql-query-design.md](docs/mysql-query-design.md)
- [docs/device-mapper.md](docs/device-mapper.md)
## Workspace
Raw data and historical files live under the repository root `workspace/`:
- `../workspace/brands`
- `../workspace/misc`
- `../workspace/CHANGELOG.md`
- `../workspace/CHANGELOG_en.md`

View File

@@ -23,10 +23,10 @@ services:
mobilemodels:
build:
context: .
dockerfile: Dockerfile
context: ..
dockerfile: delivery/Dockerfile
container_name: mobilemodels-web
working_dir: /app
working_dir: /app/delivery
environment:
MOBILEMODELS_DATA_ROOT: /data
MYSQL_HOST: mysql

18
delivery/docs/README.md Normal file
View File

@@ -0,0 +1,18 @@
# 项目文档
交付版文档统一收敛到本目录,便于部署、培训和对外交付。
## 文档索引
- [部署与使用说明](web-ui.md)
- [MySQL 设计说明](mysql-query-design.md)
- [索引构建与设备映射说明](device-mapper.md)
## 目录说明
- `web-ui.md`
- Docker Compose 启动、页面入口、MySQL 连接和管理能力说明
- `mysql-query-design.md`
- 主表设计、兼容视图、推荐查询方式
- `device-mapper.md`
- `dist/device_index.json` 构建方式与索引字段说明

View File

@@ -0,0 +1,57 @@
# Device Mapper Usage
This tool builds a cross-platform lookup index from `workspace/brands/*.md`.
## 1) Build index
```bash
python3 tools/device_mapper.py build
```
Output file: `dist/device_index.json`
## 2) Query from command line
```bash
python3 tools/device_mapper.py find --name 'iPhone14,5' --brand Apple
python3 tools/device_mapper.py find --name 'M2102J2SC' --brand Xiaomi
python3 tools/device_mapper.py find --name 'L55M5-AD' --brand Xiaomi
```
## 3) JSON structure
- `records`: normalized device records
- `device_name`: standard marketing name
- `brand`: normalized brand
- `manufacturer_brand`: manufacturer-level brand
- `market_brand`: market sub-brand
- `device_type`: `phone | tablet | wear | tv | other`
- `aliases`: all searchable aliases
- `lookup`: normalized alias -> candidate `record.id[]`
- `brand_aliases`: normalized brand aliases to filter by app-provided brand
- `brand_management`: brand governance metadata
## 4) App-side integration
1. Load `dist/device_index.json` into memory.
2. Normalize input `name` and optional `brand`.
3. Use `lookup[normalized_name]` to fetch candidate records.
4. Normalize brand via `brand_management`.
5. Filter records by normalized manufacturer or market brand when needed.
6. Return first candidate or all candidates.
Normalization rule:
- lower-case
- keep only `[0-9a-z\u4e00-\u9fff]`
- remove spaces, hyphens, underscores and punctuation
## 5) Device type mapping
Supported categories:
- `phone`
- `tablet`
- `wear`
- `tv`
- `other`

View File

@@ -0,0 +1,142 @@
# MySQL 设计说明
本文档说明交付版 MobileModels 的 MySQL 数据组织方式、兼容层设计与推荐查询方式。
## 设计目标
- 所有设备标识都能落到 MySQL 查询
- 支持第三方直接查库,保证查询速度
- 保留兼容旧结构的访问方式
- 页面侧和 SQL 接入侧统一使用同一份设备数据
## 主表
主推物理表:
```sql
mobilemodels.mm_device_catalog
```
主表整合了设备型号、品牌、厂商、来源、别名归一化结果和兼容字段,适合作为统一查询入口。
### 关键字段
- `model`
- `record_id`
- `alias_norm`
- `device_name`
- `brand`
- `manufacturer_brand`
- `parent_brand`
- `market_brand`
- `device_type`
- `source_file`
- `section`
- `source_rank`
- `source_weight`
- `code`
- `code_alias`
- `ver_name`
## 推荐查询方式
### 1. 第三方直接查主表
推荐按 `alias_norm` 等值查询:
```sql
SELECT
model,
record_id,
alias_norm,
device_name,
brand,
manufacturer_brand,
parent_brand,
market_brand,
device_type,
source_file,
section,
source_rank,
source_weight,
code,
code_alias,
ver_name
FROM mobilemodels.mm_device_catalog
WHERE alias_norm = ?
ORDER BY source_rank ASC, record_id ASC
LIMIT 20;
```
### 2. 页面 SQL 查询
页面的 `SQL 查询` tab 也是基于这张主表。
查询流程:
1. 接收客户端原始上报值
2. 服务端归一化为 `alias_norm`
3. 按主表等值查询
4. 返回结果列表、执行 SQL 和 JSON 输出
## 兼容视图
为了兼容旧系统,当前仍保留以下视图:
```sql
mobilemodels.mm_device_lookup
mobilemodels.mm_device_record
mobilemodels.models
python_services_test.models
```
其中旧结构 `python_services_test.models` 主要用于兼容既有查询逻辑,不再作为主推接入方式。
## 兼容旧结构查询示例
```sql
SELECT
model,
dtype,
brand,
brand_title,
code,
code_alias,
model_name,
ver_name
FROM python_services_test.models
WHERE model = ?
LIMIT 20;
```
## 归一化规则
`alias_norm` 统一按以下规则生成:
- 全部转小写
- 仅保留 `[0-9a-z中文]`
- 去掉空格、横线、下划线和其他标点
示例:
```text
SM-G9980 -> smg9980
iPhone14,2 -> iphone142
NOH-AL00 -> nohal00
```
## 数据来源
主表和索引数据均由以下流程生成:
1. 同步上游原始 markdown 数据
2. 解析 `workspace/brands/*.md`
3. 构建 `dist/device_index.json`
4. 导出 `dist/mobilemodels_mysql_seed.sql`
5. 加载 MySQL schema 与 seed
## 交付建议
- 第三方新接入优先使用 `mm_device_catalog`
- 页面联调和数据库联调使用同一套原始数据与归一化规则
- 生产环境务必替换默认数据库密码

101
delivery/docs/web-ui.md Normal file
View File

@@ -0,0 +1,101 @@
# Web UI
## 启动方式
`delivery/` 目录执行:
```bash
docker compose up --build -d
```
如需自定义环境变量:
```bash
cp .env.example .env
```
停止服务:
```bash
docker compose down
```
重置 MySQL 和运行期数据:
```bash
docker compose down -v
```
## 页面入口
- `http://127.0.0.1:8123/web/device_query.html`:设备查询
- `http://127.0.0.1:8123/web/brand_management.html`:数据管理
- `http://127.0.0.1:8123/web/device_query.html?view=docs`:相关文档
整个功能栈统一运行在 Docker Compose 中,不再依赖本地 Python 或本地 MySQL。
原始数据工作空间位于仓库根目录下的 `workspace/`,交付物位于 `delivery/`
## 启动后自动完成的动作
-`workspace/brands` 构建设备索引
- 生成 `dist/device_index.json`
- 导出 MySQL seed 文件
- 加载 MySQL schema 与 seed 数据
- 启动 Web 页面与 API 服务
## MySQL 默认连接
- Host: `127.0.0.1`
- Port: `3306`
- Database: `mobilemodels`
- Reader User: `mobilemodels_reader`
如需自定义账号密码,请使用 `.env` 覆盖默认值。
## 设备查询
页面顶部统一提供三个导航入口:
- `设备查询`
- `数据管理`
- `相关文档`
设备查询页顶部包含两个页内 tab
- `SQL 查询`
- `索引查询`
### SQL 查询
- 直接调用 Compose 内 API 查询 MySQL 主表 `mobilemodels.mm_device_catalog`
- 服务端先将输入归一化为 `alias_norm`
- 页面展示实际执行的 SQL、返回结果和 JSON
- 页面同时展示只读连接信息,便于第三方联调
### 索引查询
- 基于 `dist/device_index.json` 内存索引进行快速识别
- 适合前端联调、接口对比和结果核验
### 平台输入建议
- Android / iOS / HarmonyOS直接使用客户端原始上报的 `model_raw`
- 输入框会根据所选平台自动提供示例值
- 未输入时,系统会使用当前平台的默认示例值发起查询
## 数据管理
数据管理页支持:
- 品牌列表管理
- 品牌与厂商关系管理
- 品牌同义词管理
- 数据来源优先级管理
- 原始数据同步
- 索引数据查看与重新加载
## 说明
- 原始数据、索引和 MySQL seed 运行时持久化在 Docker volume 中,不回写本地工作区
- 交付环境建议覆盖默认的 `MYSQL_ROOT_PASSWORD``MYSQL_READER_PASSWORD`

View File

@@ -1,7 +1,7 @@
#!/bin/sh
set -eu
cd /app
cd /app/delivery
sh tools/init_runtime_data.sh

View File

@@ -12,6 +12,8 @@ from datetime import date
from pathlib import Path
from typing import Dict, Iterable, List, Optional, Set
from project_layout import DELIVERY_ROOT, WORKSPACE_ROOT
ENTRY_RE = re.compile(r"^\*\*(.+?)\*\*\s*$")
VARIANT_RE = re.compile(r"^\s*((?:`[^`]+`\s*)+):\s*(.+?)\s*$")
BACKTICK_RE = re.compile(r"`([^`]+)`")
@@ -714,8 +716,8 @@ def main() -> None:
parser.add_argument(
"--repo-root",
type=Path,
default=Path(__file__).resolve().parents[1],
help="Path to MobileModels repository root",
default=WORKSPACE_ROOT,
help="Path to workspace root",
)
subparsers = parser.add_subparsers(dest="command", required=True)
@@ -741,7 +743,7 @@ def main() -> None:
if args.command == "build":
output_path: Path = args.output
if not output_path.is_absolute():
output_path = args.repo_root / output_path
output_path = DELIVERY_ROOT / output_path
export_index(records, output_path)
print(f"Built index: {output_path}")
print(f"Total records: {len(records)}")

View File

@@ -16,9 +16,9 @@ from device_mapper import (
normalize_text,
resolve_parent_brand,
)
from project_layout import DELIVERY_ROOT, WORKSPACE_ROOT
REPO_ROOT = Path(__file__).resolve().parent.parent
LEGACY_CODE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9,._/+\\-]{1,63}$")
@@ -185,8 +185,8 @@ def parse_args() -> argparse.Namespace:
parser.add_argument(
"--repo-root",
type=Path,
default=REPO_ROOT,
help="Path to MobileModels repository root",
default=WORKSPACE_ROOT,
help="Path to workspace root",
)
parser.add_argument(
"--output",
@@ -200,7 +200,7 @@ def parse_args() -> argparse.Namespace:
def main() -> int:
args = parse_args()
repo_root = args.repo_root.resolve()
output_path = args.output if args.output.is_absolute() else repo_root / args.output
output_path = args.output if args.output.is_absolute() else DELIVERY_ROOT / args.output
records = build_records(repo_root)
device_record_count = len(records)

View File

@@ -60,14 +60,8 @@ init_path() {
}
for rel_path in \
brands \
misc \
dist \
README.md \
README_en.md \
CHANGELOG.md \
CHANGELOG_en.md \
LICENSE.txt
workspace \
delivery/dist
do
init_path "$rel_path"
done

View File

@@ -10,8 +10,7 @@ import sys
import time
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
from project_layout import DELIVERY_ROOT
def mysql_env(password: str) -> dict[str, str]:
@@ -131,8 +130,8 @@ def parse_args() -> argparse.Namespace:
def main() -> int:
args = parse_args()
schema_path = args.schema if args.schema.is_absolute() else REPO_ROOT / args.schema
seed_path = args.seed if args.seed.is_absolute() else REPO_ROOT / args.seed
schema_path = args.schema if args.schema.is_absolute() else DELIVERY_ROOT / args.schema
seed_path = args.seed if args.seed.is_absolute() else DELIVERY_ROOT / args.seed
wait_for_mysql(args.user, args.password, args.host, args.port, args.wait_timeout)

View File

@@ -0,0 +1,11 @@
#!/usr/bin/env python3
"""Shared path helpers for the workspace/delivery project layout."""
from __future__ import annotations
from pathlib import Path
DELIVERY_ROOT = Path(__file__).resolve().parent.parent
PROJECT_ROOT = DELIVERY_ROOT.parent
WORKSPACE_ROOT = PROJECT_ROOT / "workspace"

View File

@@ -11,15 +11,13 @@ import sys
import tempfile
from pathlib import Path
from project_layout import DELIVERY_ROOT, PROJECT_ROOT, WORKSPACE_ROOT
REPO_ROOT = Path(__file__).resolve().parent.parent
DEFAULT_REPO_URL = "https://github.com/KHwang9883/MobileModels.git"
DEFAULT_BRANCH = "master"
SYNC_PATHS = [
"brands",
"misc",
"README.md",
"README_en.md",
"CHANGELOG.md",
"CHANGELOG_en.md",
"LICENSE.txt",
@@ -27,7 +25,7 @@ SYNC_PATHS = [
def run(cmd: list[str], cwd: Path | None = None) -> None:
subprocess.run(cmd, cwd=cwd or REPO_ROOT, check=True)
subprocess.run(cmd, cwd=cwd or PROJECT_ROOT, check=True)
def remove_path(path: Path) -> None:
@@ -59,7 +57,7 @@ def sync_path(src: Path, dst: Path) -> None:
def sync_selected_paths(upstream_root: Path) -> None:
for relative_path in SYNC_PATHS:
src = upstream_root / relative_path
dst = REPO_ROOT / relative_path
dst = WORKSPACE_ROOT / relative_path
if not src.exists():
raise FileNotFoundError(f"Missing upstream path: {relative_path}")
sync_path(src, dst)
@@ -69,7 +67,9 @@ def build_index(output_path: str) -> None:
run(
[
sys.executable,
str(REPO_ROOT / "tools/device_mapper.py"),
str(DELIVERY_ROOT / "tools/device_mapper.py"),
"--repo-root",
str(WORKSPACE_ROOT),
"build",
"--output",
output_path,
@@ -81,9 +81,11 @@ def export_mysql_seed(output_path: str) -> None:
run(
[
sys.executable,
str(REPO_ROOT / "tools/export_mysql_seed.py"),
str(DELIVERY_ROOT / "tools/export_mysql_seed.py"),
"--output",
output_path,
"--repo-root",
str(WORKSPACE_ROOT),
]
)
@@ -92,7 +94,7 @@ def load_mysql_seed(seed_path: str) -> None:
run(
[
sys.executable,
str(REPO_ROOT / "tools/load_mysql_seed.py"),
str(DELIVERY_ROOT / "tools/load_mysql_seed.py"),
"--seed",
seed_path,
]

View File

@@ -14,13 +14,14 @@ from http import HTTPStatus
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from sync_upstream_mobilemodels import DEFAULT_BRANCH, DEFAULT_REPO_URL, REPO_ROOT
from project_layout import DELIVERY_ROOT, PROJECT_ROOT, WORKSPACE_ROOT
from sync_upstream_mobilemodels import DEFAULT_BRANCH, DEFAULT_REPO_URL
SYNC_SCRIPT = REPO_ROOT / "tools/sync_upstream_mobilemodels.py"
INDEX_PATH = REPO_ROOT / "dist/device_index.json"
MYSQL_SEED_PATH = REPO_ROOT / "dist/mobilemodels_mysql_seed.sql"
MYSQL_LOADER = REPO_ROOT / "tools/load_mysql_seed.py"
SYNC_SCRIPT = DELIVERY_ROOT / "tools/sync_upstream_mobilemodels.py"
INDEX_PATH = DELIVERY_ROOT / "dist/device_index.json"
MYSQL_SEED_PATH = DELIVERY_ROOT / "dist/mobilemodels_mysql_seed.sql"
MYSQL_LOADER = DELIVERY_ROOT / "tools/load_mysql_seed.py"
DATA_ROOT = Path(os.environ.get("MOBILEMODELS_DATA_ROOT", "/data"))
SYNC_METADATA_PATH = DATA_ROOT / "state/sync_status.json"
SYNC_LOCK = threading.Lock()
@@ -30,7 +31,7 @@ NORMALIZE_RE = re.compile(r"[^0-9a-z\u4e00-\u9fff]+")
def run_command(args: list[str]) -> subprocess.CompletedProcess[str]:
return subprocess.run(
args,
cwd=REPO_ROOT,
cwd=PROJECT_ROOT,
text=True,
capture_output=True,
check=False,
@@ -192,15 +193,17 @@ def get_status_payload() -> dict[str, object]:
return {
"supports_upstream_sync": True,
"storage_mode": "docker_volume",
"repo_root": str(REPO_ROOT),
"project_root": str(PROJECT_ROOT),
"workspace_root": str(WORKSPACE_ROOT),
"delivery_root": str(DELIVERY_ROOT),
"data_root": str(DATA_ROOT),
"upstream_repo_url": DEFAULT_REPO_URL,
"upstream_branch": DEFAULT_BRANCH,
"last_sync_time": sync_metadata.get("last_sync_time"),
"last_upstream_commit": sync_metadata.get("last_upstream_commit"),
"index_file": str(INDEX_PATH.relative_to(REPO_ROOT)),
"index_file": str(INDEX_PATH.relative_to(DELIVERY_ROOT)),
"index_mtime": index_mtime,
"mysql_seed_file": str(MYSQL_SEED_PATH.relative_to(REPO_ROOT)),
"mysql_seed_file": str(MYSQL_SEED_PATH.relative_to(DELIVERY_ROOT)),
"mysql_seed_mtime": mysql_seed_mtime,
"mysql_host": mysql_host,
"mysql_port": mysql_port,
@@ -240,18 +243,20 @@ def run_upstream_sync() -> dict[str, object]:
payload = {
"storage_mode": "docker_volume",
"repo_root": str(REPO_ROOT),
"project_root": str(PROJECT_ROOT),
"workspace_root": str(WORKSPACE_ROOT),
"delivery_root": str(DELIVERY_ROOT),
"data_root": str(DATA_ROOT),
"upstream_repo_url": DEFAULT_REPO_URL,
"upstream_branch": DEFAULT_BRANCH,
"upstream_commit": upstream_commit,
"last_sync_time": datetime.now().isoformat(timespec="seconds"),
"last_upstream_commit": upstream_commit,
"index_file": str(INDEX_PATH.relative_to(REPO_ROOT)),
"index_file": str(INDEX_PATH.relative_to(DELIVERY_ROOT)),
"index_mtime": datetime.fromtimestamp(INDEX_PATH.stat().st_mtime).isoformat(timespec="seconds")
if INDEX_PATH.exists()
else None,
"mysql_seed_file": str(MYSQL_SEED_PATH.relative_to(REPO_ROOT)),
"mysql_seed_file": str(MYSQL_SEED_PATH.relative_to(DELIVERY_ROOT)),
"mysql_seed_mtime": datetime.fromtimestamp(MYSQL_SEED_PATH.stat().st_mtime).isoformat(timespec="seconds")
if MYSQL_SEED_PATH.exists()
else None,
@@ -270,7 +275,7 @@ def run_upstream_sync() -> dict[str, object]:
class MobileModelsHandler(SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=str(REPO_ROOT), **kwargs)
super().__init__(*args, directory=str(DELIVERY_ROOT), **kwargs)
def guess_type(self, path: str) -> str:
content_type = super().guess_type(path)

View File

@@ -518,7 +518,9 @@
const lines = [];
if (fallbackTitle) lines.push(fallbackTitle);
if (data.data_root) lines.push(`数据目录: ${data.data_root}`);
if (data.repo_root) lines.push(`应用目录: ${data.repo_root}`);
if (data.project_root) lines.push(`项目目录: ${data.project_root}`);
if (data.workspace_root) lines.push(`工作空间目录: ${data.workspace_root}`);
if (data.delivery_root) lines.push(`交付目录: ${data.delivery_root}`);
if (data.storage_mode) lines.push(`存储模式: ${data.storage_mode}`);
if (data.upstream_repo_url) lines.push(`上游仓库: ${data.upstream_repo_url}`);
if (data.upstream_branch) lines.push(`上游分支: ${data.upstream_branch}`);

View File

@@ -694,15 +694,15 @@
<h1 class="title">相关文档</h1>
<p class="sub">这里统一整理页面调试、MySQL 接入和兼容查询相关说明,直接在当前页面查看,不再跳转到单独页面。</p>
<div class="doc-link-list">
<button type="button" class="doc-link active" data-doc-path="/misc/mysql-query-design.md" data-doc-title="MySQL 设计说明">MySQL 设计说明</button>
<button type="button" class="doc-link" data-doc-path="/web/README.md" data-doc-title="Web 使用说明">Web 使用说明</button>
<button type="button" class="doc-link active" data-doc-path="/docs/mysql-query-design.md" data-doc-title="MySQL 设计说明">MySQL 设计说明</button>
<button type="button" class="doc-link" data-doc-path="/docs/web-ui.md" data-doc-title="Web 使用说明">Web 使用说明</button>
<button type="button" class="doc-link" data-doc-path="/README.md" data-doc-title="项目 README">项目 README</button>
</div>
</article>
<article class="card">
<h2 class="title" id="docPanelTitle">MySQL 设计说明</h2>
<p class="sub" id="docPanelPath">/misc/mysql-query-design.md</p>
<p class="sub" id="docPanelPath">/docs/mysql-query-design.md</p>
<pre id="docPanelContent">加载中...</pre>
</article>
@@ -1880,7 +1880,7 @@ LIMIT 20;</pre>
syncReportPlatformUI();
syncSqlPlatformUI();
loadReadonlyInfo();
loadDocInPanel("/misc/mysql-query-design.md", "MySQL 设计说明");
loadDocInPanel("/docs/mysql-query-design.md", "MySQL 设计说明");
loadIndexFromPath();
</script>
</body>

View File

@@ -126,8 +126,8 @@
<h1 class="title" id="docTitle">文档查看</h1>
<p class="sub" id="docPath">正在加载文档...</p>
<div class="btns">
<a class="btn" href="/web/doc_viewer.html?path=/misc/mysql-query-design.md&title=MySQL%20%E8%AE%BE%E8%AE%A1%E8%AF%B4%E6%98%8E">MySQL 设计说明</a>
<a class="btn" href="/web/doc_viewer.html?path=/web/README.md&title=Web%20%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E">Web 使用说明</a>
<a class="btn" href="/web/doc_viewer.html?path=/docs/mysql-query-design.md&title=MySQL%20%E8%AE%BE%E8%AE%A1%E8%AF%B4%E6%98%8E">MySQL 设计说明</a>
<a class="btn" href="/web/doc_viewer.html?path=/docs/web-ui.md&title=Web%20%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E">Web 使用说明</a>
<a class="btn" href="/web/doc_viewer.html?path=/README.md&title=%E9%A1%B9%E7%9B%AE%20README">项目 README</a>
</div>
</section>
@@ -139,14 +139,14 @@
<script>
const ALLOWED_DOCS = new Map([
["/misc/mysql-query-design.md", "MySQL 设计说明"],
["/web/README.md", "Web 使用说明"],
["/docs/mysql-query-design.md", "MySQL 设计说明"],
["/docs/web-ui.md", "Web 使用说明"],
["/README.md", "项目 README"],
]);
async function main() {
const params = new URLSearchParams(window.location.search);
const path = params.get("path") || "/misc/mysql-query-design.md";
const path = params.get("path") || "/docs/mysql-query-design.md";
const title = params.get("title") || ALLOWED_DOCS.get(path) || "文档查看";
const docTitleEl = document.getElementById("docTitle");
const docPathEl = document.getElementById("docPath");

View File

@@ -1,372 +0,0 @@
# MySQL Query Design
## Goal
让三方直接查 MySQL同时保持查询简单、稳定、足够快。
当前数据规模:
- `device_record`: 6219
- `lookup key`: 23463
这个量级不需要分库分表,也不需要 ES。MySQL 8 + 合适索引就足够。
## Overall Design
建议把 MySQL 拆成两层角色:
1. 离线构建层
- 继续以 `brands/*.md` 为原始数据。
- 通过 `tools/device_mapper.py` 生成标准化记录。
- 通过 `tools/export_mysql_seed.py` 导出 MySQL seed SQL。
2. 查询服务层
- 三方只读查询 MySQL。
- 只开放读账号,只允许 `SELECT`
- 最好挂在只读实例或只读副本上,不直接连主库。
## Tables
### `mm_device_record`
兼容视图,一条设备一行,来自 `mm_device_catalog` 聚合。
主要字段:
- `record_id`
- `device_name`
- `brand`
- `manufacturer_brand`
- `parent_brand`
- `market_brand`
- `device_type`
- `source_file`
- `section`
- `source_rank`
- `source_weight`
- `aliases_json`
用途:
- 回源查看聚合后的设备信息
- 兼容历史排查 SQL
- 不再作为独立物理表维护
### `mm_device_catalog`
统一设备查询主表,一条设备 alias 一行。
主要字段:
- `model`
- `alias_norm`
- `record_id`
- `device_name`
- `brand`
- `manufacturer_brand`
- `parent_brand`
- `market_brand`
- `device_type`
- `source_rank`
- `source_weight`
- `code`
- `code_alias`
- `ver_name`
这是当前唯一的设备物理表。
原因:
- 只需要等值查 `alias_norm`
- 同时兼容 `mm_device_lookup``models`
- 最适合高频只读场景
### `mm_device_lookup`
兼容视图,来自 `mm_device_catalog`
### `mm_brand_lookup`
品牌归一化表,用于把三方传来的品牌值归到:
- `manufacturer_brand`
- `parent_brand`
- `market_brand`
比如:
- `荣耀` -> `HONOR`
- `redmi` -> `market_brand=Redmi`
- `xiaomi` -> `manufacturer_brand=Xiaomi`
### `models` / `python_services_test.models`
为了兼容旧查询链路,保留一层旧结构适配:
- `mobilemodels.models`(兼容视图)
- `python_services_test.models`(兼容视图)
字段映射说明:
- `model`
- 当前行可直接查询的原始设备标识
- 一条设备的多个 alias 会展开成多行
- `dtype`
- 对应当前的 `device_type`
- `brand`
- 优先使用 `market_brand`
- `brand_title`
- 使用 `manufacturer_brand`
- `code`
- 当前设备识别出的主型号编码
- `code_alias`
- 其他型号编码,使用 ` | ` 拼接
- `model_name`
- 对应当前的 `device_name`
- `ver_name`
- 当前设备的人类可读别名,使用 ` | ` 拼接
这层主要用于兼容历史 SQL 和第三方直接查表,不建议作为后续新能力的主数据模型。
## Query Contract
三方不要直接查原始字符串,统一查归一化后的 `alias_norm`
归一化规则与当前项目一致:
- 全部转小写
- 只保留 `[0-9a-z\u4e00-\u9fff]`
- 去掉空格、横线、下划线和其他标点
例如:
- `SM-G9980` -> `smg9980`
- `iPhone14,2` -> `iphone142`
- `NOH-AL00` -> `nohal00`
## Recommended SQL
推荐分三层:
1. 新接入
- 直接查 `mobilemodels.mm_device_catalog`
2. 兼容现有查询
- 继续查 `mobilemodels.mm_device_lookup`
3. 兼容历史旧结构
- 继续查 `python_services_test.models`
### 1. 主推查法:按设备标识查主表
```sql
SELECT
model,
record_id,
alias_norm,
device_name,
brand,
manufacturer_brand,
parent_brand,
market_brand,
device_type,
source_file,
section,
source_rank,
source_weight,
code,
code_alias,
ver_name
FROM mobilemodels.mm_device_catalog
WHERE alias_norm = ?
ORDER BY source_rank ASC, record_id ASC
LIMIT 20;
```
说明:
- `alias_norm` 是主查询键
- `model` 是当前命中的原始设备标识
- `code / code_alias / ver_name` 用于兼容历史字段和辅助展示
### 1.1 兼容查法:沿用 `mm_device_lookup`
如果接入方已经依赖 `mm_device_lookup`,可以不改 SQL
```sql
SELECT
alias_norm,
record_id,
device_name,
brand,
manufacturer_brand,
parent_brand,
market_brand,
device_type,
source_file,
section,
source_rank,
source_weight
FROM mobilemodels.mm_device_lookup
WHERE alias_norm = ?
ORDER BY source_rank ASC, record_id ASC
LIMIT 20;
```
### 1.2 兼容旧结构查法:沿用 `python_services_test.models`
如果接入方仍然沿用旧表结构,可以继续查:
```sql
SELECT
model,
dtype,
brand,
brand_title,
code,
code_alias,
model_name,
ver_name
FROM python_services_test.models
WHERE model = ?
LIMIT 20;
```
### 2. 主推查法:带品牌约束查
先把品牌查成归一化结果,再过滤:
```sql
SELECT
l.model,
l.record_id,
l.alias_norm,
l.device_name,
l.brand,
l.manufacturer_brand,
l.parent_brand,
l.market_brand,
l.device_type,
l.source_file,
l.section,
l.source_rank,
l.source_weight,
l.code,
l.code_alias,
l.ver_name
FROM mobilemodels.mm_device_catalog AS l
LEFT JOIN mobilemodels.mm_brand_lookup AS b
ON b.alias_norm = ?
WHERE l.alias_norm = ?
AND (
b.alias_norm IS NULL
OR (b.market_brand IS NOT NULL AND l.market_brand = b.market_brand)
OR (b.manufacturer_brand IS NOT NULL AND l.manufacturer_brand = b.manufacturer_brand)
OR (b.parent_brand IS NOT NULL AND l.parent_brand = b.parent_brand)
)
ORDER BY l.source_rank ASC, l.record_id ASC
LIMIT 20;
```
### 3. 兼容查法:按设备记录聚合查看
如果历史排查逻辑需要“一台设备一行”,可以继续查:
```sql
SELECT
record_id,
device_name,
brand,
manufacturer_brand,
parent_brand,
market_brand,
device_type,
source_file,
section,
source_rank,
source_weight,
aliases_json
FROM mobilemodels.mm_device_record
WHERE record_id = ?
LIMIT 1;
```
## Migration Advice
建议迁移顺序:
1. 新增接入直接使用 `mobilemodels.mm_device_catalog`
2. 旧查询先保持 `mm_device_lookup` / `python_services_test.models` 不变
3. 待业务侧完成字段适配后,再逐步切到主表
这样可以做到:
- 数据底层只维护一张设备实体表
- 现有查询链路不需要同时改造
- 新能力统一围绕主表扩展
## Performance Strategy
要点只有四个:
1. 只允许等值查询 `alias_norm`
- 禁止 `%xxx%` 这类模糊查
2. 查询主表用 `mm_device_catalog`
- 兼容链路走 `mm_device_lookup` / `models` 视图
3. 给三方只读账号
- 只开放 `SELECT`
4. 最好放只读实例
- 不让三方流量影响同步和管理操作
在当前数据规模下,`alias_norm` 命中是很轻的查询。
## Update Flow
建议每日全量刷新一次,不做增量。
原因:
- 数据量小
- 全量替换更稳定
- 不容易出现脏数据和漏删
建议流程:
1. 拉取上游原始数据
2. 生成最新 `device_index.json`
3. 导出 MySQL seed SQL
4. 在 MySQL 中执行:
- `DELETE FROM mm_device_catalog`
- `DELETE FROM mm_brand_lookup`
- 批量插入新数据
5. 完成后切换读流量
## Security
如果一定让三方直连 MySQL至少要做这些限制
- 单独只读账号
- IP 白名单
- 只授权 `mobilemodels` 和兼容需要的 `python_services_test` schema
- 不开放 DDL / DML
- 连接数限制
- 查询超时限制
更稳妥的方式仍然是:
- 三方查你们自己的只读网关
- 网关内部查 MySQL
但如果现阶段必须直连库,上面的三张表已经足够支撑。
## Files
- Schema: `sql/mobilemodels_mysql_schema.sql`
- Seed exporter: `tools/export_mysql_seed.py`
生成 seed
```bash
python3 tools/export_mysql_seed.py
```
默认输出:
- `dist/mobilemodels_mysql_seed.sql`

View File

@@ -1,66 +0,0 @@
# Device Mapper Usage
This tool builds a cross-platform lookup index from `brands/*.md`.
## 1) Build index
```bash
python3 tools/device_mapper.py build
```
Output file: `dist/device_index.json`
## 2) Query from command line
```bash
python3 tools/device_mapper.py find --name 'iPhone14,5' --brand Apple
python3 tools/device_mapper.py find --name 'M2102J2SC' --brand Xiaomi
python3 tools/device_mapper.py find --name 'L55M5-AD' --brand Xiaomi
```
## 3) JSON structure
- `records`: normalized device records
- `device_name`: standard marketing name
- `brand`: normalized brand
- `manufacturer_brand`: manufacturer-level brand (e.g. `Xiaomi`)
- `market_brand`: market sub-brand (e.g. `Xiaomi` / `Redmi` / `POCO`)
- `device_type`: `phone | tablet | wear | tv | other`
- `aliases`: all known searchable aliases
- `lookup`: normalized alias -> candidate `record.id[]`
- `brand_aliases`: normalized brand aliases to filter by app-provided brand
- `brand_management`: brand governance metadata
- `manufacturer_aliases`: canonical brand aliases (`华为/huawei/HUAWEI` -> `HUAWEI`)
- `manufacturer_to_parent`: child -> parent brand (e.g. `OnePlus -> OPPO`)
- `parent_to_children`: parent -> child brands
- `market_brand_aliases`: sub-brand aliases (`redmi` -> `Redmi`)
- `stats`: includes parent/manufacturer/market-level counts and Xiaomi combined + separated counts
## 4) App-side integration (iOS / Android / Harmony)
1. Load `dist/device_index.json` into memory.
2. Normalize input `name` and optional `brand`.
3. Use `lookup[normalized_name]` to fetch candidate records.
4. If `brand` exists, normalize it by `brand_management`:
- manufacturer aliases (e.g. `华为/huawei/HUAWEI` -> `HUAWEI`)
- market sub-brand aliases (e.g. `redmi` -> `Redmi`, `xiaomi` -> manufacturer-level `Xiaomi` combined)
5. Filter records by normalized manufacturer/market brand as needed.
6. Return first candidate (or show all candidates when ambiguous).
Normalization rule should match this script:
- lower-case
- keep only `[0-9a-z\u4e00-\u9fff]`
- remove all spaces, hyphens, underscores and punctuation
## 5) Device type mapping
Supported categories:
- `phone`
- `tablet`
- `wear`
- `tv`
- `other`
Type inference uses keyword + source-file default fallback to reduce misses.

View File

@@ -1,140 +0,0 @@
# Web UI
## Start
From repository root:
```bash
docker compose up --build -d
```
Optional environment setup:
```bash
cp .env.example .env
```
Stop:
```bash
docker compose down
```
Reset both MySQL and managed raw/index data:
```bash
docker compose down -v
```
Open:
- http://127.0.0.1:8123/web/device_query.html (设备查询)
- http://127.0.0.1:8123/web/brand_management.html (数据管理)
整个功能栈统一运行在 Docker Compose 中,不再依赖本地 Python 或本地 MySQL 直接启动。
容器启动时会自动完成:
- 构建 `dist/device_index.json`
- 导出 MySQL seed 文件
- 加载 MySQL schema 与 seed 数据
- 启动 Web 页面与 API 服务
容器内还会同时启动 MySQL
- `127.0.0.1:3306`
- database: `mobilemodels`
- reader user: `mobilemodels_reader`
页面顶部有导航条Gitea 风格):
- `设备查询`
- `数据管理`
设备查询页顶部额外提供三个页内 tab
- `索引查询`
- `SQL 查询`
- `相关文档`
## 设备查询
### 客户端上报模式
字段按你给的上报方式输入:
- Android
- `platform=android`
- `model_raw=Build.MODEL`(例如 `SM-G9980`
- iOS
- `platform=ios`
- `model_raw=utsname.machine`(例如 `iPhone14,2`
- HarmonyOS
- `platform=harmony`
- `model_raw`(例如 `NOH-AL00`
说明:
- Android / iOS / HarmonyOS 都直接使用客户端原始上报的 `model_raw`
- 页面会输出完整候选列表,并附带 `report_payload` 方便核对上报值。
### SQL 查询模式
- 直接调用 Compose 内的 API 查询 MySQL 主表 `mobilemodels.mm_device_catalog`
- 服务端会先把输入归一化成 `alias_norm`
- 页面会显示实际执行的 SQL、结果列表和返回 JSON
### 相关文档
- 页面内统一展示主推 SQL、兼容 SQL、归一化规则和文档入口
- 可直接跳转查看 `misc/mysql-query-design.md``web/README.md``README.md`
## Output
设备查询页会返回:
- best candidate
- complete ranked candidates
- normalized brand + parent manufacturer
- source rank/weight (from source order config)
- matched inputs
- matched normalized keys
- source markdown file and section
- full JSON output for direct integration or debugging
## Data management
数据管理页支持:
- Independent maintenance:
- Brand list is maintained independently in the UI and initialized from index data on first load.
- Changes are stored in browser `localStorage`.
- Alias normalization (brand only):
- `华为 / huawei / HUAWEI` are normalized to brand `HUAWEI`.
- Brand-manufacturer relations:
- One brand has exactly one parent manufacturer.
- One manufacturer can contain multiple brands.
- Examples:
- `Xiaomi` manufacturer includes brands `Xiaomi`, `Redmi`, `POCO`.
- `vivo` manufacturer includes brands `vivo`, `iQOO`.
- `OPPO` manufacturer includes brands `OPPO`, `OnePlus`, `realme`.
- Click `品牌数 / 厂商数` to open full list popup.
- `编辑品牌列表`: popup edit.
- `编辑品牌-厂商关系`: popup edit.
- `编辑品牌同义词`: popup edit.
- 数据来源管理(新增):
- Supports drag-and-drop source ordering.
- Ranking weight is higher for sources at the top.
- Initial order puts `*_cn.md` before non-`cn` sources.
- 原始数据同步(新增):
- Available in the third left tab.
- Calls the Compose service API to sync upstream `KHwang9883/MobileModels` raw markdown, rebuild `dist/device_index.json`, export MySQL seed, and reload MySQL.
- Requires `docker compose up --build -d`.
- 索引数据(新增):
- Available in the fourth left tab.
- Includes index reload and index load status.
## Notes
- Managed raw data, rebuilt index, and MySQL seed files are persisted in Docker volumes, not written back to the local workspace during runtime.
- For production, override `MYSQL_ROOT_PASSWORD` and `MYSQL_READER_PASSWORD` with your own values.

View File

@@ -5,7 +5,7 @@
> Providing your device's codename is welcome. You can find it in version of system firmware, starting with `PD`.
>
> Please report any errors by [opening an issue](https://github.com/KHwang9883/MobileModels/issues).
> Please report any errors through your project delivery channel.
## vivo X series
@@ -1107,4 +1107,4 @@
**JOVI Y39 5G (`PD2444F`):**
`V2444`: JOVI Y39 5G
`V2444`: JOVI Y39 5G