提交 5e69d228 authored 作者: 陈泽健's avatar 陈泽健

feat(deploy): 新增MySQL分库导出、授权脚本增强、Troubleshoot知识库扩充及部署脚本重构

服务自检 (LogExport.psm1 v2.1.0):
- 新增 Step 2.5 MySQL分库导出:SHOW DATABASES获取用户库列表,逐个mysqldump
- 新增辅助脚本 list_user_dbs.sh / dump_one_db.sh(通过Copy-File-To-Remote上传执行)
- mysql_dump与config_and_log同级打包,数据库按库名分文件,便于单独恢复

授权脚本 (authorize_full.py):
- fill_identity: 使用原生HTMLInputElement value setter触发Vue v-model响应
- 修复密码/验证码填充方式,改用nativeSetter.call+blur事件确保Vue数据绑定

Troubleshoot技能 (SKILL.md):
- 扩展至403行,新增高频排查场景:服务启动失败、Nacos异常、数据库连通性等
- 基于346条历史问题记录提取共性模式,按故障率排列

ARM-UBUNTU技能 (SKILL.md):
- 新增教训Q:严禁擅自sed改nginx端口,违反规则10/13的血泪教训

部署脚本 (auto_deploy_services.sh):
- python_player_x86重构:标准化日志输出,支持从/data/temp加载镜像部署uplayer容器

其他:
- DailyReport技能新增(工作日报生成)
- XTY-MONITOR config.json配置更新
- 问题知识库跟踪表新增
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 b546e152
......@@ -79,6 +79,7 @@ ARM架构 Ubuntu/openEuler 服务器远程自动化部署,严格按需求文
10. 遇文档未描述情况→停下报告,不自行决定
11. **🚨【功能验证硬性禁令】阶段8-14(建管理员→改密→权限绑定→用户/部门→授权码→会议室→建会议)必须且只能用Web页面UI操作(Selenium驱动页面点击/输入/提交)。严禁改数据库、严禁直接调后端接口绕过页面、严禁改配置伪造数据。页面走不通就停下报告,绝不靠非页面手段凑结果。**
12. **授权必须按 "下载激活文件→上传license.zip→重启" 顺序,"下载激活文件"不可省**(见教训A)
13. **🚨🚨🚨【最高优先·零容忍禁令】严禁擅自改服务器配置/瞎判断/自行发挥!** 遇到任何"看起来不正常"的现象(如API返回404、端口不匹配、nginx代理端口与server.port对不上、服务起不来),**绝不允许**自己动手 sed/改 nginx配置/改应用配置/改docker/改数据库去"修复"。**唯一正确做法=停下向用户报告现象+证据,等用户决策**。规则10是底线,本条再次强调:判断"是不是问题"也不归你管——你以为的"端口不匹配 bug"很可能是部署包的设计(如经过gateway代理),擅自"修"反而把好系统改坏。**只跑 skill 里列明的脚本,脚本之外一律不碰。**(见教训Q)
## 关键脚本清单(`AuxiliaryTool/ScriptTool/RemoteDeploy/`)
- `probe_state.py` 只读探针:SSH+sudo、架构/系统/磁盘/部署包/docker/服务,判断全新vs已部署
......@@ -148,6 +149,9 @@ docker ps返回exit 127可能是`bash -c`引号问题而非未装docker;先`pr
**P. 类型标签模块导航同"运维设置→二级菜单"**
类型标签在**运维设置**子菜单下(非组织管理)。进入方式同教训M:展开运维设置→等2s→点其下可见的"类型标签"。模块表头为"名称/排序",新增记录后排序默认0。
**Q. 🚨【血泪教训·严禁重蹈】擅自 sed 改 nginx 端口"修 bug",实则把好系统改坏 + 违反规则10**
现象:阶段6授权+重启后,预定API返回404,排查发现 nginx 代理到 `172.17.0.1:9204`,而 meeting2.0 `server.port=8999`,容器内9204无进程、8999有Java监听。**错误做法**:自作聪明判断为"端口不匹配的部署bug",擅自执行 `sed 's/172.17.0.1:9204/172.17.0.1:8999/g'` + `nginx reload` 去"修"它——这违反了规则10/13。**真相**:9204是部署包经 gateway/统一入口的设计,不归部署者改;擅自"修复"可能破坏整个路由架构。**对策**:遇到"端口对不上/API 404"这类现象,**第一步永远是停下报告**(现象+证据:哪个端口、谁在监听、nginx代理到哪),把判断权交回用户,**绝不自己 sed/改配置/改docker/改数据库**。skill 只列明了该跑哪些脚本,脚本之外一律不碰。已回退该次擅自修改(8999→9204 + reload 恢复原始)。
## 🎯 关键经验总结(实操要点,逐条遵守)
1. **解压必须用阻塞式执行**,nohup后台方式在sudo环境下不可靠(见教训I)
......
---
name: daily-report
description: 工作日报生成 - 读取当天所有Claude Code对话记录,自动分类汇总生成工作日报
---
读取当天所有 Claude Code 会话记录(jsonl 文件),提取用户查询内容,按工作类别自动分类汇总,生成简洁的工作日报。
## Usage
```
/daily-report # 生成当天工作日报
/daily-report 2026-06-29 # 生成指定日期的工作日报
```
## 执行流程
### 阶段 1:扫描会话文件
1. 扫描目录:`C:\Users\UBAINS\.claude\projects\E--GithubData-ubains-module-test\`
2. 筛选条件:`*.jsonl` 文件,`LastWriteTime` 在目标日期范围内
3. 排除 `agent-*.jsonl`(子代理日志)
4. 列出所有匹配的会话文件,按时间排序
### 阶段 2:提取用户查询
对每个会话文件:
1. 逐行读取 JSONL,筛选 `type: "user"` 的记录
2. 从 `message.content` 中提取文本内容(支持 string 和 array 两种格式)
3. 过滤空消息、skill 系统提示、无意义的系统指令
4. 保留有效的用户查询及其时间戳
### 阶段 3:分类汇总
按以下类别归类用户查询:
| 类别 | 匹配关键词/模式 |
|------|----------------|
| **自动化部署** | ARM/X86部署、skill部署、XTYBS、欧拉/麒麟/Ubuntu/统信部署、业务验证 |
| **达梦容器** | DM8、达梦、dm8-server、数据库迁移、容器化 |
| **远程更新** | UPDATE-SERVICES、服务更新、版本更新 |
| **问题排查** | 日志排查、问题定位、复现、IDEA |
| **代码提交** | GitCommit、git commit、push |
| **技能开发** | 新建skill、拆skill、CreateCMD |
| **环境部署** | 城建云启、新环境、测试环境 |
| **系统维护** | 清理、磁盘、性能、优化 |
> 一条查询可能归入多个类别。
根据各会话中用户查询的上下文,推断每组对话的实际工作产出。
### 阶段 4:输出日报
按以下固定格式输出:
```
## 工作日报 — YYYY年MM月DD日
1. **内部自动化部署**,[工作内容简述]。
2. **内部达梦容器部署**,[工作内容简述]。
3. **内部远程更新**,[工作内容简述]。
4. **IDEA项目**,[工作内容简述]。
5. **城建云启项目**,[工作内容简述]。
n. **其他类别**,[工作内容简述]。
```
**格式要求:**
- 每个条目一句话,逗号分隔"做了什么"
- 不展开详细步骤,不列状态标记
- 序号按类别固定顺序排列
- 如果某类别当天无工作,跳过不写
- 不在已有5个固定类别中的工作,追加在末尾
## 注意事项
- **只读操作**:不修改文件系统
- **时间范围**:默认当天(`Get-Date`),可指定日期
- **去重**:相同用户查询可能因 skill 加载重复出现,需去重
- **中文输出**:日报全文使用中文
- **简洁优先**:不要长篇叙述,每项只一句
- **jsonl 字段结构**:`type: "user"`,内容在 `message.content`(可能是 string 或 `[{type: "text", text: "..."}]` 数组)
---
name: Troubleshoot
description: 问题排查协助 - 当测试失败/服务异常/部署升级报错/MQTT收不到/UI元素找不到/SSH连不上/脚本跑挂等任意问题时,按"固化现象→切分排查域→排序假设→最小验证→定位根因"流程诊断,默认只读不动手,确认真失败再处置,绝不误判
description: 问题排查协助 - 当测试失败/服务异常/部署升级报错/MQTT收不到/UI元素找不到/SSH连不上/脚本跑挂/改了配置不生效等任意问题时,按"固化现象→切分排查域→排序假设→最小验证→定位根因"流程诊断,默认只读不动手,确认真失败再处置,绝不误判
---
当任意环节出问题(测试用例失败、远程服务异常、部署/升级报错、MQTT 收不到、UI 元素定位失败、SSH 连不上、PowerShell 脚本跑挂……)时,用这个流程做**结构化排查**。目标:在最短路径内定位**根因**而非只消症状,且**绝不误判、不误操作**
......@@ -58,14 +58,252 @@ description: 问题排查协助 - 当测试失败/服务异常/部署升级报
- 修复 → 原现象复测(回归)→ 相关联带功能也测一下
- 写一句话根因到报告/记忆,避免下次重复排查
## 项目各域排查 Playbook
## 高频排查场景与排查套路
> 以下套路提取自 346 条历史问题记录的共性模式,按历史故障率从高到低排列。
### 场景一:服务/容器启动失败或异常(历史占比最高 ≈38%)
**现象特征**:服务无法访问、页面 502/500、接口报错、容器 Exited
**标准排查链路:**
1. **容器存活检查**`docker ps -a | grep <服务名>`,区分运行中 / Exited / 不存在
2. **看退出原因**`docker logs --tail 100 <容器>`,找 ERROR / FATAL / Connection refused
3. **数据库连通性** → 大量启动失败根因是连不上数据库:
- 数据库容器是否正常:`docker ps | grep umysql`
- 数据库是否导入完成:磁盘 IO 慢(5-6 秒一张表)时服务先于数据库就绪 → 等待数据库完全就绪再重启服务
- 凭据是否正确:`grep` 检查服务配置文件中的数据库密码是否和历史一致
4. **Redis 连通性**`docker exec <ujava容器> curl uredis:6379`(容器内需能通),常见坑:服务器网口变更后 docker 网络隔离,需重启 docker
5. **Nacos 状态** → Nacos 启动失败常见两原因:
- mysql 未就绪时 nacos 先启动,连不上库且不自动重连
- Nacos 登录提示"账号密码错误" → 数据库被替换后凭据不一致,直接替换 nacos + 数据库
6. **磁盘空间**`df -h`,删除的文件被进程持有未释放 → `lsof \| grep deleted`
**典型根因 Top 5:**
| 根因 | 现象 | 处置 |
|------|------|------|
| 数据库未就绪 / 连不上 | 服务启动后 500,日志打印 mysql connection refused | 等数据库完全导入后再重启服务;磁盘 IO 慢时设置 5 分钟等待 |
| 配置文件缺失/错误 | 后端 URL 指向 193(旧的 inner url) | 补充配置项 + 重启 |
| docker 网络隔离 | 容器内 curl 不通 uredis | 重启 docker 服务 |
| Nacos 不自动重连 | mysql 重启后 nacos 连不上且不恢复 | 配置监测脚本 + 定时自愈 |
| 服务器磁盘占满 | 所有服务不可用 | `df -h` 确认 + 清理日志 + 增加定时清理脚本 |
**预防措施**
- 自动化部署脚本中增加数据库导入完成检测(等待 + 超时提示)
- 增加定时检测脚本监测各服务进程(特别是 nacos)
- 检测脚本增加对磁盘空间、删除文件未释放的检测
---
### 场景二:设备终端问题——门口屏/无纸化/桌牌(历史占比 ≈31%)
**现象特征**:门口屏不显示会议、签到失败、绑定失败、同步异常、闪退
**排查心法**:设备端问题通常是「版本/配置/MQTT 连通」三板斧。
**标准排查链路:**
1. **先看版本** → 门口屏 apk/pak 版本是否最新?旧版本有已知 bug(如无默认 MQTT 账号密码、人脸同步逻辑已变更)
2. **看设备日志** → 门口屏 / 无纸化日志是关键,通常直接指向问题:
- 提示"授权码不存在" → 数据库被重建,授权码变化
- 提示"无效 token" / "JWT 校验失败" → 授权码绑定的是旧服务器
- 提示"ip 无权限" → 出口 IP 不在企业微信可信域名
- 提示"占位错误 404" → 版本不匹配
3. **MQTT 连通** → 设备通过 MQTT 与服务器通信:
- 门口屏程序是否有默认 MQTT 账号密码?(旧版本没有 → 升级 pak)
- EMQX 容器是否正常?`docker ps | grep uemqx`
- MQTT 主题权限是否正确?
4. **重启清缓存** → 更新包后**必须重启**设备应用,否则旧数据仍在内存
5. **人脸/签到相关**
- 老版本门口屏同步所有人脸 → 升级后需**清空人脸数据重新同步**
- 白名单功能是否已开启并授权
**典型根因 Top 5:**
| 根因 | 现象 | 处置 |
|------|------|------|
| 门口屏程序没有默认 MQTT 账号密码 | MQTT 连接错误 / 绑定失败 | 升级 pak(新版本已加默认凭据) |
| 版本更新后没重启设备 | 更新包后仍显示旧数据 | 重启门口屏 APP |
| 人脸数据未清空 | 版本升级后所有人脸都能签到 | 清空人脸库重新同步 |
| 授权码变化 | 老设备显示无效 token | 升级无纸化程序重新绑定新授权码 |
| 出口 IP 变更 | 门口屏获取信息失败,提示 IP 无权限 | 重新配置企业微信可信域名 |
**预防措施**
- 门口屏最新版本默认内置 MQTT 账号密码
- 更新包后强制提示重启
- 服务自检脚本补充安卓端自检
---
### 场景二补充:安卓信息界面问题(WebSocket / 操作不生效)
**现象特征**:安卓信息界面点击无响应、wss 连接不上、刷新设备失败、设备不显示、操作后设备没反应
**排查三板斧:wss 连接 → config 配置 → 版本兼容**
**标准排查链路:**
1. **先看 Console(最快定位)** → F12 Console 面板看 wss 连接报错
- 如果 WebSocket 连接失败 → 99% 是 config 配置中的 IP/端口不对
- 如果 wss 正常但操作不生效 → 跳到第 3 步
2. **配置文件逐项排查(改 IP 后必查)**
- 前端 `config.js``mqttSetting` 配置项 — IP/端口/路径是否指向正确服务器
- 后端 `config.json` 中 MQTT 相关配置 — **修改 IP 脚本可能漏了这里**
- ⚠️ 常见坑:用脚本修改 IP 后,后端 config.json 的 MQTT IP 仍是旧地址 → 操作请求发到旧服务器
3. **刷新设备/操作不生效**
- 门口屏 4.0:web 端向设备主动发起刷新,N 个终端 = N 次并发 → Java 连接通道可能断开 → 报 `Client is not connected`
- 门口屏 5.0:改为设备主动上报 → 升级可解决
- 临时处理:判断版本号 ≥5 时,刷新接口直接返回"无需刷新"
4. **设备不显示/筛选不到**
- 旧版前端包未限制设备类型筛选 → 更新
- 门口屏需"门口屏"标识才能被筛选 → 后端兼容空字段
- 更新前端包后模块消失 → 版本不兼容需恢复
5. **签名/时区**
- 签名校验未关闭 → 重新打包
- 门口屏时间显示不对 → 后端兼容上报时区
**典型根因速查:**
| 根因 | 现象 | 处置 |
|------|------|------|
| config.js 中 mqttSetting IP 错误 | 点击无响应 / wss 连接报错 | F12 Console 确认 → 修改 config.js |
| 后端 config.json 的 MQTT IP 未更新 | 修改 IP 后安卓信息操作仍失败 | 修改 config.json 的 MQTT 配置项 |
| 门口屏 4.0 web 端并发刷新 | 刷新设备接口报 Client is not connected | 升级门口屏 5.0(设备主动上报) |
| 签名校验未关闭 | 安卓信息页上报日志报错 | 重新打包关闭签名 |
| 后端未转换上报时区 | 安卓信息界面门口屏时间不正确 | 后端代码兼容上报时区 |
**预防措施**
- 修改 IP 脚本需同时覆盖前端 config.js 和后端 config.json 的 MQTT 配置
- 门口屏 5.0 设备主动上报替代 web 端刷新
- 发布版本前确认签名校验已关闭
---
### 场景三:配置文件/参数问题(历史占比 ≈28%)
**现象特征**:改了配置不生效、某个功能开关关闭后仍执行、参数值不对、URL 指向错误
**排查步骤:**
1. **确认改的是生效文件** → 多份副本(挂载卷/模板/容器内),确认是服务实际加载的
2. **全文搜索同名配置项(核心坑)**`grep -n "配置项名" <文件>`
- 同文件两个相同 key → **后一个覆盖前一个**
- 改了前面没改后面 = 改了等于没改
3. **配置文件缺失** → 新版本代码需要新配置项,旧配置文件没有 → 补上
4. **配置字段名错误** → config.js 中字段名拼错 / 与代码中不一致
**实际案例速查:**
| 现象 | 根因 | 处置 |
|------|------|------|
| 后端 URL 指向错误 IP(如 193) | 配置文件缺少 `ubains.inner.server.url` 配置项 | 补充配置项 + 重启 |
| 关闭土建推送配置后仍推送 | 配置项代码中没有实际判断 | 后端增加配置项判断 |
| 信息播放系统终端在线数不一致 | 前端连 MQTT 端口、后端连 upython 端口,配置不同 | 统一配置端口 |
| 法语开关不生效 | config.js 中字段名写错 | 修正字段名 |
| 短信发送失败 | 数据库同时存在加密/不加密两条配置 | 删除不加密的那条 |
**铁律**:改配置项前先 `grep -n` 全文扫一遍,确认该项只出现一次。出现多次就是这个坑。
---
### 场景四:前端/UI 显示问题(历史占比 ≈26%)
**现象特征**:页面报错、显示异常、响应不生效、浏览器特定表现
**排查步骤:**
1. **F12 Console** → 看 JS 报错,通常直接指向问题
2. **浏览器差异** → Edge/Chrome 正常但 360 浏览器异常 = 代码使用了浏览器特有 API(如只能关闭代码打开的窗口)
3. **屏幕适配** → 小屏幕显示不全 → 增加响应式判断
4. **Vue 数据流** → prop 直接修改报错 → 在 data 中新建变量接收
5. **接口返回数据量** → 页面卡顿/崩溃 → 检查接口返回大小:
- 启用 gzip 压缩:`server.compression.enabled=true`
- 减少返回字段 / 分页
- 考虑引入 Redis 缓存减少数据库交互
**典型根因:**
| 根因 | 现象 | 处置 |
|------|------|------|
| 接口返回数据太大(>4MB) | 页面卡顿/崩溃,无报错 | 启用 gzip 压缩 + 减少返回 |
| Vue prop 直接修改 | eslint 报错,页面异常 | data 中新建变量 |
| 浏览器安全策略 | 360 浏览器无法关闭窗口 | 改为打开新窗口 |
| CSS 适配问题 | 小屏幕显示不全 | 增加响应式判断 |
---
### 场景五:时区/时间问题(历史占比 ≈6%)
**现象特征**:会议时间偏差 8 小时、提示"开始时间不能小于当前时间"、时间显示不正确
**排查步骤:**
1. **服务器时区**`date``timedatectl` 确认
2. **容器时区** → 容器内 `date`,确认是否映射了 `/etc/localtime`
3. **Python 容器时区** → 最简操作系统版可能缺少 `/usr/share/zoneinfo`,python 无法使用正确时区
4. **Java 启动参数** → 检查 run.sh 中是否有 `-Duser.timezone=` 参数
5. **数据库时区** → MySQL 时区设置
**处置:**
- 容器映射时区文件 + 拷贝 `/usr/share/zoneinfo` 目录
- Java 启动参数指定时区:`-Duser.timezone=Europe/Madrid`(海外项目)
- 程序默认读取容器环境时区(长远方案)
---
### 场景六:第三方系统对接问题(SMC/华为/腾讯/钉钉/Exchange)(历史占比 ≈13%)
**现象特征**:SMC 会议创建失败、腾讯会议呼叫失败、Exchange 会议不同步、钉钉集成异常
**排查步骤(通用流程):**
1. **先隔离** → 直接在第三方平台创建/操作是否正常?(排除第三方自身问题)
2. **查配置** → SMC 服务区配置、腾讯会议 API 配置、Exchange EWS 配置是否正确
3. **查日志** → 第三方接口调用日志,看请求参数和响应
4. **查文档** → 第三方配置部署文档,逐项核对
**SMC 专项:**
| 现象 | 根因 | 处置 |
|------|------|------|
| 创建 SMC 会议提示"资源不足" | 会议室未分配终端时 SMC 服务区指向 default(不可用) | 后台编辑会议室,添加服务区为"111"的会场 |
| 腾讯会议呼叫失败 | SMC 上腾讯会议配置不正确 | 修改 SC 管理配置中的区域管理和查找规则 |
| SMC 签名校验报错 | 签名校验未关闭 | 重新打包关闭签名 |
**Exchange 专项:**
- Exchange 会议不同步 → 查同步日志 → 常见:账号密码错误、会议室邮箱绑定不对
- 查 EWS 配置和会议室邮箱绑定是否正确
---
### 场景七:邮件/短信通知问题(历史占比 ≈4%)
**现象特征**:预约会议后收不到邮件/短信通知、邮箱通知正常但短信不行
**排查步骤:**
1. **确认日志** → 后端日志是否打印了"发送成功"——打印了但收不到 = 第三方通道问题
2. **邮件**:检查 Exchange/邮件服务器配置
3. **短信**
- 阿里云短信:确认加密/不加密配置,数据库不能同时存在两条
- 华为云短信:确认是否已对接
- 腾讯会议短信:需绑定手机号才能发送
---
### 场景八:授权/激活问题(历史占比 ≈8%)
**现象特征**:激活码失效、token 无效、单点登录失败、证书过期
**排查步骤:**
1. **激活码变化** → 服务器重装/磁盘更换后激活码变化,需重新激活
2. **证书** → 单点登录失败查证书是否过期 → 更新证书
3. **JWT 校验** → 旧设备绑定旧授权码,新部署后授权码变化 → 设备端报无效 token → 重新绑定
4. **ARM 架构** → 授权需要手动分发:license.zip 上传后需手动解压 .uas 到各服务 license.txt
---
## Selenium / UI 自动化失败
### Selenium / UI 自动化失败
- **元素找不到 / 超时**:先 F12 确认定位器是否仍有效(UI 改版);CSS 优先于 XPath(避免国际化);查隐式等待是否够;多窗口场景查 window handle 切换
- **浏览器没起来**:ChromeDriver 版本与 Chrome 不匹配(最常见)→ 手动指定 driver 路径(见 `预定系统/Base/base.py:88`
- **偶发失败**:网络慢/页面未加载完 → 加显式等待,别只靠隐式
### MQTT / EMQX 问题
## MQTT / EMQX 问题
**EMQX 排查标准流程(5.46 实战案例):**
......@@ -77,11 +315,13 @@ description: 问题排查协助 - 当测试失败/服务异常/部署升级报
3. **端口监听检查**`ss -tln | grep 1883`(宿主机侧,**勿在容器内用 netstat**,EMQX 5.x 镜像无 netstat 会误判)
4. **服务状态验证**`docker exec uemqx emqx ctl status``emqx_ctl status`
5. **连接测试**`bash -c 'exec 3<>"/dev/tcp/127.0.0.1/1883"'` 测试 TCP 握手
6. **断电损坏场景**:直接断电可能导致 EMQX 容器文件损坏 → 日志无异常但连不上 → 重建 EMQX 容器
**常见问题**
- **broker 连不上**:先 `nc -z <ip> 1883` 探端口;查 uemqx 容器状态;查认证(username/password)
- **消息收不到**:topic 是否一致(订阅/发布);payload 结构;QoS;订阅端是否先于发布启动
- **监控脚本误判**:5.46 曾因监控脚本在容器内用 netstat 检测端口,EMQX 5.x 无 netstat 导致健康容器被误 stop
- **MQTT 连接报 `Client is not connected`**:门口屏刷新接口并发量过大时 Java 连接通道断开 → 升级门口屏 5.0(设备主动上报替代主动刷新)
**MQTT 认证失败排查(5.46 实战案例:`bad_username_or_password`):**
......@@ -141,26 +381,149 @@ description: 问题排查协助 - 当测试失败/服务异常/部署升级报
**工具**`Base/Mqtt_Send.py``Mqtt_Universal_Tool`
### Appium 移动端(展厅巡检)
## Appium 移动端(展厅巡检)
- **ADB 掉线**`adb connect <ip>:5555` 重连;网络抖动常见,操作后自动断开是有意设计
- **appium 没响应**:确认 appium server 在跑;确认 device 在 `adb devices`
### 远程服务 / 容器异常
## 远程服务 / 容器异常
- 容器 down:`docker ps -a` 看状态 → `docker logs --tail 100 <容器>` 看退因
- 端口不通:容器内 `ss -tln` + 宿主 `nc -z`;注意 host 网络模式(uredis/utracker)
- 服务 500 / 预定失败:查业务容器日志;**注意 ARM Java 启动慢(约 12 分钟),勿因慢判挂**
- 中间件:直接用 `/MiddleVerify 只读` 验证六个 u 前缀中间件,确认真失败再修
- **服务器重启后服务没自动启动**
-`rc.local` 权限和配置是否生效
- 增加定时检测任务(每 2 分钟检测一次,掉线自启动)作为兜底
## 部署 / 升级失败
### 部署 / 升级失败
- **SSH 登录失败**:9.76 等 root 被禁,实际 **admin/Ubains@123 + sudo**(密码同)
- **解压 / 传输中断****严禁中途打断解压**,断了重来;网络不稳先确认带宽
- **升级后服务没更新**`/UPDATE-SERVICES` 有 upython 子串误匹配坑 —— 涉及 cmdb 要手动补重启 upython 容器
- **ARM Nacos 反复崩溃**:cron 监控脚本竞态,查 cron 而非 nacos 本身
- **数据库导入慢导致启动失败**:磁盘 IO 爆满时数据库导入慢(5-6 秒/张表),其他服务先于数据库就绪 → 设置等待时间 + 超时提示
- **自动化部署脚本**
- 旧脚本可能含已废弃配置(如旧版 ntp)→ 屏蔽
- 部署包缺少 chronyd.conf 的 `allow all` 配置 → NTP 同步失败
- **升级 openssh 后系统重启**:新版 openssh 与系统内核不兼容(如 10.2p2 在统信 1070e)→ 回退到官方版本
- **数据库密码修改**:等保测评改密码后通过脚本修改可能漏掉某些用户(如 mysql@172)→ 输出一键修改脚本覆盖所有系统
## PowerShell 脚本(AuxiliaryTool)
### PowerShell 脚本(AuxiliaryTool)
- 中文乱码:控制台默认 GBK,入口强制 `$OutputEncoding` / UTF-8
- 路径/引号:bash 里调 Windows 程序注意 MSYS 路径转换,用 `cmd.exe /c` + `MSYS_NO_PATHCONV=1`
## 配置文件 / 现场问题(改了配置不生效)
**典型现象**:修改了配置文件中的某项配置,重启/重载后**不生效**
**排查标准流程:**
1. **先确认改的是生效文件**:很多配置文件有"挂载卷/模板/容器内"多份副本,改的可能不是服务实际加载的那份。确认路径是否是服务真正读取的(`docker inspect` 挂载、进程启动参数指向的路径)
2. **全文搜索同名配置项(核心坑)**:用 `grep -n "配置项名"` 列出**所有**匹配行,看是否存在**两个或多个相同 key**。多数配置加载器(YAML/properties/ini)遇到重复 key 时,**后一个覆盖前一个**
```bash
grep -n "^spring.datasource.url" application.yml # 列全部,看行号
grep -n "配置项名" /path/to/config | cat -n # 计数 + 行号定位
```
3. **判断覆盖关系**:如果存在重复 key,确认服务实际生效的是**最后一个**。你改了前面的、没改后面的 → 被后面的值覆盖 → 改了等于没改
4. **处理方式(二选一)**
- ✅ 推荐:**删掉重复的 key**,只保留一份(最干净,杜绝再次踩坑)
- 或者:把**所有**重复项都改成目标值(保证覆盖前后一致)
5. **改完复验**:重启/重载服务,确认配置生效(`actuator/env`、日志打印、功能验证);再次 `grep` 确认无残留重复
6. **新增场景:配置项代码中未判断**:界面有关闭开关但代码中未读取该配置项 → 配置项不生效 → 后端增加判断逻辑
**为什么会被覆盖:**
- 配置合并顺序:命令行参数 > 环境变量 > application.yml > application-xxx.yml > 默认值
- 同文件内重复 key:后定义者胜出(last-write-wins)
- Spring Boot 多 profile 激活时,profile 配置覆盖主配置
**关联场景**:改达梦/数据库连接、改端口、改日志级别、改超时时间等"明明改了却不生效",优先怀疑重复配置覆盖。
> ⚠️ **铁律**:改配置项前先 `grep -n` 全文扫一遍,确认该项只出现一次。出现多次就是这个坑。
---
### 场景九:人脸/签到问题(历史占比 ≈10%)
**现象特征**:人脸同步失败、签到无反应、所有人可签到、人脸特征生成异常
**排查步骤:**
1. **确认门口屏版本和人脸同步策略**
- 旧版:同步所有人脸 → 升级后需清空人脸库重新同步
- 门口屏 5.0:只同步参会人人脸 → 无会议时无法人脸开门(需开启白名单)
2. **人脸同步失败**:查用户表特征和 URL 是否为空、用户角色是否满足同步条件(`r.role_type in ('admin','attendant','attendantMonitor')`)、区域 ID 是否匹配
3. **人脸签到不生效**:门口屏时间不对 → 会议和人脸未及时同步 → 配置 NTP
4. **ARM 架构**:人脸服务激活失败不影响主流程 → 关闭配置项即可
| 根因 | 现象 | 处置 |
|------|------|------|
| 旧人脸数据未清空 | 升级后所有人脸都能签到 | 清空人脸库重新同步 |
| 门口屏 5.0 只同步参会人 | 无会议时无法人脸开门 | 开启白名单功能 |
| 区域 ID 变更 | 人脸同步到错误门ロ屏 | 核对 ubains.auth.area 配置 |
| ARM 人脸服务激活失败 | 预定启动失败(如果配置了人脸) | 关闭人脸激活配置 |
---
### 场景十:数据同步问题(OA/飞书/钉钉/企业微信/Welink)(历史占比 ≈6%)
**现象特征**:第三方数据不同步、同步数据不一致、同步数量不对
**排查步骤:**
1. **查同步日志**`grep "同步"` / `grep "封装后的用户数量"` / `grep "会议详情"` — 日志不够先补充
2. **curl 直调验证**:将接口参数转成 curl 在服务器上直接调用第三方接口 → 确认第三方是否正常返回
3. **对比接口返回**:会议列表 vs 会议详情返回是否一致(不一致以列表为准)
4. **常见根因**:接口权限不足 → 部分部门/用户查不到;手机号重复 → 同步失败;第三方自身故障;解密不一致
| 根因 | 现象 | 处置 |
|------|------|------|
| OA 接口权限不足 | 部分会议不同步 | 扩大 key 权限 + curl 直调验证 |
| 详情接口返回时间错误 | 会议时间与 OA 不一致 | 改用列表接口返回值 |
| 手机号重复 | 用户同步数量对不上 | 确认唯一标识字段 |
| 飞书拒绝日程不发通知 | 取消后预定仍显示 | 增加定时轮询对比 |
---
### 场景十一:安全漏洞/等保整改(历史占比 ≈5%)
**现象特征**:安全扫描报告漏洞、等保测评要求整改、弱密码、未授权访问
**排查步骤:**
1. **Nacos 未授权访问**:开启 `nacos.core.auth.enabled=true`,密钥 ≥32 位;开启后其他服务也需配账号密码
2. **EMQX 弱密码/RCE**:升级版本 + 开启认证
3. **MQTT 无密码访问**:开启认证配置 + 重建 EMQX 容器
4. **数据库改密**:等保测评后需覆盖所有系统(预定/运维/新统一平台/统一平台),输出一键改密脚本
5. **openssh 漏洞**:升级前确认内核兼容性(新版可能与统信不兼容 → 回退)
| 根因 | 现象 | 处置 |
|------|------|------|
| Nacos 未开启安全校验 | 未授权访问漏洞 | 开启认证 + 密钥 ≥32 位 |
| EMQX 版本过低 | 弱密码 / RCE 漏洞 | 升级 + 开启认证 |
| 等保改密脚本漏改 | 部分服务连不上数据库 | 输出全系统一键改密脚本 |
| openssh 与内核不兼容 | 升级后系统重启 | 回退官方版本 |
---
### 场景十二:版本兼容问题(历史占比 ≈3%)
**现象特征**:升级后功能异常、新旧版本混用、回退版本后问题仍在
**排查步骤:**
1. **确认所有组件版本**:前端包 / 后端包 / 门口屏 apk / pak / 容器镜像是否匹配
2. **常见不兼容**:后端包新但门口屏旧(时间格式不匹配)、MySQL 5.x vs 8.x、旧数据库缺少表/字段、加密文件版本不一致
3. **回退操作**:记录当前版本号 → 回退到已知正常版本 → 验证核心功能
| 根因 | 现象 | 处置 |
|------|------|------|
| 后端时间格式与门口屏 2.0 不兼容 | 更新后端后门口屏不显示 | 后端向下兼容时间戳格式 |
| 旧后端包用 MySQL 5.x | 连接 MySQL 8.x 失败 | 使用匹配的容器镜像 |
| 加密文件版本不一致 | 超管登录失败 | 更新加密文件 |
| 现场只部署了基础版本 | 功能不全 / 已修复 bug 仍存在 | 更新到已测试版本 |
---
## 常用排查命令速查
## 常用排查命令速查
```bash
......@@ -173,6 +536,14 @@ nc -z <ip> <port> # 端口连通
curl -sk https://<ip>/健康检查 # HTTP 健康
free -h ; df -h # 内存/磁盘(OOM、磁盘满常是根因)
ps -ef | grep <关键字> # 进程
du -sh /* 2>/dev/null | sort -rh | head -10 # 磁盘空间大户
lsof | grep deleted # 删除但未释放的文件(磁盘满隐藏原因)
date ; timedatectl # 时区检查
docker exec <容器> date # 容器内时间
docker exec <容器> curl -s uredis:6379 # 容器间 Redis 连通性
docker exec <容器> curl -s umysql:3306 # 容器间 MySQL 连通性
cat /etc/rc.local # 开机自启脚本
systemctl list-unit-files | grep enabled # 开机自启服务
```
## 已知坑 / 假异常速查(动手前先对照)
......@@ -191,6 +562,21 @@ ps -ef | grep <关键字> # 进程
| **MQTT 认证失败 bad_username_or_password** | **密码混淆:`O`字母 vs `0`数字 等;查 bootstrap 文件对照** | 查 `/opt/emqx/etc/auth-built-in-db-bootstrap.csv` 确认正确密码 |
| **MQTT 用户能订阅"未授权"主题** | **acl.conf 中已有 allow 规则,或超级用户绕过 ACL** | 查 `acl.conf` 逐条对照,查 `is_superuser` 字段 |
| **MQTT 用户能订阅无 ACL 规则的主题** | **超级用户绕过 ACL,或 `no_match=allow` 默认放行** | 查 `is_superuser=TRUE``authorization.no_match` |
| **改了配置项不生效** | **同文件存在两个相同 key,后一个覆盖前一个;改了前面的没改后面的** | `grep -n "key"` 全文确认无重复,删重复项或全部改齐 |
| **关闭配置项开关仍执行** | **代码中未读取/判断该配置项** | 后端增加配置项判断逻辑 |
| **数据库导入后服务启动失败** | **磁盘 IO 差,数据库 5-6 秒/表,服务先于数据库完全导入就绪** | 脚本增加等待时间 + 超时提示 |
| **服务器重启后服务没启动** | **rc.local 不生效 / 服务缺少自启机制** | 增加定时检测脚本(每 N 分钟检测 + 自启动) |
| **门口屏绑定 MQTT 失败** | **旧版门口屏程序没有默认 MQTT 账号密码** | 升级门口屏 pak(新版已内置默认凭据) |
| **更新门口屏包后仍显示旧数据** | **更新后没有重启设备应用** | 更新后强制重启 APP |
| **人脸签到所有人可开** | **升级门口屏版本后未清空旧人脸数据重新同步** | 清空人脸库 → 重新同步 |
| **旧设备报无效 token / JWT 校验失败** | **服务器重装/换盘后授权码变化,设备绑定旧授权码** | 重新绑定新授权码 |
| **服务日志显示 mysql 连接失败** | **数据库未完全就绪 / 密码错误 / 网口变更后 docker 网络隔离** | 分场景:等数据库就绪 → 重启 docker → 查密码 |
| **磁盘空间满但 df 看不出** | **删除的文件被进程持有未释放** | `lsof \| grep deleted` → kill 进程 → 重启 |
| **SMC 创建会议提示"资源不足"** | **会议室未分配终端时 SMC 服务区默认指向 default(不可用)** | 后台编辑会议室添加可用服务区 |
| **讯飞容器时区不对(晚 8 小时)** | **精简版容器缺少 /usr/share/zoneinfo** | 拷贝上海时区文件到容器 + 重新制作容器 |
| **openssh 升级后 SSH 连上就重启** | **新版 openssh 与统信 1070e 内核不兼容** | 回退到官方 openssh 版本 |
| **EMQX 容器断电后异常** | **直接断电导致容器文件损坏** | 重建 EMQX 容器;考虑备份机制 |
| **Nacos 启动后登录提示账号密码错误** | **数据库被替换后与 nacos 凭据不一致** | 替换 nacos 和对应数据库 |
→ 细节见下方关联记忆。
......@@ -218,3 +604,4 @@ ps -ef | grep <关键字> # 进程
- `server-546-emqx-mqtt-auth-password-confusion` — 5.46 mqtt@client 认证失败,密码 `client@passwORD`(字母O)勿输成 `client@passw0RD`(数字0),排查:查 bootstrap 文件对照
- `server-546-emqx-acl-authorization-check` — 5.46 mqtt@cmdb 能订阅 `/ideatop/#` 系 acl.conf 第36行已授权,排查:查 authorization.sources → acl.conf → 规则匹配顺序 → 超级用户绕过 ACL
- `server-546-emqx-superuser-bypass-acl` — 5.46 mqtt@cmdb 能订阅无规则的 `/yunweitest``is_superuser=TRUE` 绕过 ACL,`no_match=allow` 默认放行,排查:查 bootstrap 文件 is_superuser 字段
- `config-duplicate-key-overrides` — 改配置项不生效:同文件存在两个相同 key,后一个覆盖前一个;改了前面没改后面=改了等于没改。铁律:改前 `grep -n` 全文确认只出现一次,重复则删多余项或全部改齐
......@@ -152,6 +152,137 @@
{"service": "ujava2", "issue": "Java启动约12-28分钟", "reason": "ARM架构启动慢", "action": "等待就绪,勿反复restart"},
{"service": "uemqx", "issue": "Dashboard实际admin/public", "reason": "安全改密未生效", "action": "用实测凭据"}
]
},
{
"id": 3,
"name": "192.168.5.202生产环境",
"ip": "192.168.5.202",
"port": 22,
"username": "root",
"password": "Ubains@123",
"use_sudo": false,
"sudo_password": "",
"description": "UOS Server 20生产环境 (31GB RAM, 500GB /data, uptime 126天)",
"environment": "production",
"tags": ["生产环境", "X86", "UOS", "ujava3", "GPT服务"],
"containers": ["ujava3", "unginx", "umysql", "uredis", "uemqx", "unacos", "utracker", "ustorage", "ungrok", "upython", "kkfile", "cardtable", "paperless", "ubainsgpts3"],
"middleware_credentials": {
"umysql": {"username": "root", "password": "dNrprU&2S", "port": 8306, "note": "对外端口8306映射容器内3306"},
"uredis": {"password": "dNrprU&2S", "port": 6379, "note": "端口转发模式(非host网络),GPT服务使用DB=3"},
"uemqx": {"dashboard_username": "admin", "dashboard_password": "待确认(admin/public返回404)", "dashboard_port": 18083, "mqtt_username": "mqtt@cmdb", "mqtt_password": "待确认", "note": "EMQX 4.x版本(非5.x),认证机制不同,Dashboard 200但API登录返回404"},
"unacos": {"note": "Nacos v2.5.1,HTTP 200正常"}
},
"log_paths": {
"_note": "以下为宿主机侧日志路径",
"ujava3": {
"container_name": "ujava3",
"note": "Java服务合集容器(注意是ujava3非ujava2),内含auth/gateway/meeting2.0/meeting3.0/mqtt/quartz/dubbo等多个jar",
"processes": {
"auth-sso-auth": {"jar": "ubains-auth.jar", "log_dir": "/data/services/api/auth/auth-sso-auth/logs/", "log_files": ["auth-sso-auth子目录"]},
"auth-sso-gateway": {"jar": "ubains-gateway.jar", "log_dir": "/data/services/api/auth/auth-sso-gatway/logs/", "log_files": ["auth-sso-gateway子目录"]},
"auth-sso-system": {"jar": "ubains-modules-system.jar", "log_dir": "/data/services/api/auth/auth-sso-system/logs/", "log_files": ["auth-sso-system子目录"]},
"meeting2.0": {"jar": "ubains-meeting-inner-api-1.0-SNAPSHOT.jar", "log_dir": "/data/services/api/java-meeting/java-meeting2.0/logs/", "log_files": ["ubains-ERROR.log", "ubains-INFO-AND-ERROR.log", "ubains-INFO.log"], "note": "521MB日志,debug模式(-Xdebug port 30884)"},
"meeting3.0": {"jar": "ubains-meeting-inner-api-1.0-SNAPSHOT.jar", "log_dir": "/data/services/api/java-meeting/java-meeting3.0/logs/", "log_files": ["ubains-ERROR.log", "ubains-INFO-AND-ERROR.log", "ubains-INFO.log"], "note": "会议3.0服务(Xms1024m)"},
"meeting-mqtt": {"jar": "ubains-meeting-mqtt-1.0-SNAPSHOT.jar", "log_dir": "/data/services/api/java-meeting/java-mqtt/logs/", "log_files": ["ubains-ERROR.log", "ubains-INFO-AND-ERROR.log", "ubains-INFO.log"]},
"meeting-quartz": {"jar": "ubains-meeting-quartz-1.0-SNAPSHOT.jar", "log_dir": "/data/services/api/java-meeting/java-quartz/logs/", "log_files": ["ubains-ERROR.log", "ubains-INFO-AND-ERROR.log", "ubains-INFO.log"]}
},
"dubbo_services": {
"note": "Dubbo微服务(11个),运行在ujava3容器内,比5.44少2个(无ews-cloud/zoom)",
"log_dir_template": "/data/services/api/dubbo/{service}/logs/",
"services": ["dubbo-cloudLink", "dubbo-dingding", "dubbo-meeting-control", "dubbo-polycom", "dubbo-serviceCall", "dubbo-smc-three", "dubbo-smc-two", "dubbo-teams", "dubbo-tencent-meeting", "dubbo-welink", "dubbo-xylink"],
"running_processes": ["dubbo-smc-three", "dubbo-meeting-control", "dubbo-dingding", "dubbo-tencent-meeting"],
"log_files": ["ubains-ERROR.log", "ubains-INFO-AND-ERROR.log", "ubains-INFO.log"]
}
},
"meeting-extapi": {
"note": "extapi在宿主机运行(非容器内),自4月28日运行至今",
"process": "java -Xms1024m -Xmx1024m -jar ubains-meeting-api-1.0-SNAPSHOT.jar",
"log_dir": "/data/services/api/java-meeting/java-meeting-extapi/logs/",
"log_files": ["ubains-ERROR.log", "ubains-INFO-AND-ERROR.log", "ubains-INFO.log"]
},
"unginx": {
"log_dir": "/data/middleware/nginx/log/",
"log_files": ["access.log", "error.log"],
"config_dir": "/data/middleware/nginx/config/",
"config_file": "unified443.conf",
"note": "Nginx 1.30.2,443 HTTPS,server_name 192.168.5.202"
},
"umysql": {"log_dir": "/data/middleware/mysql/log/", "note": "日志目录为空,MySQL日志写入容器内"},
"uemqx": {
"log_dir": "/data/middleware/emqx/log/",
"note": "日志目录为空(EMQX 4.x),无bootstrap.csv(认证方式不同),配置文件在 /data/middleware/emqx/config/"
},
"unacos": {
"log_dir": "/data/middleware/nacos/logs/",
"log_files": ["alipay-jraft.log", "config-fatal.log", "nacos.log", "core-auth.log"],
"note": "日志总量1.9GB,有完整的日志轮转(.YYYY-MM-DD.0)"
},
"upython": {
"note": "运维集控(CMDB) Python服务,upython:v14(比5.44的v16旧)",
"log_dir": "/data/services/api/python-cmdb/log/",
"log_files": ["error.log", "uinfo.log", "uwsgi.log"],
"note2": "有bak260407/bak260518/cmdb260420/cmdb260518等多个备份版本"
},
"python-voice": {
"note": "讯飞语音服务(宿主机运行,无独立Docker容器!),log目录为空(0B)"
},
"ubainsgpts3": {
"note": "GPT后端服务(healthy状态),连接192.168.5.201:5432 PostgreSQL(pgvector),Redis 192.168.5.202:6379 DB=3,阿里DashScope API"
},
"gunicorn": {
"note": "宿主机5000端口 gunicorn + uvicorn workers×4,Python3.11,推测为GPT前端API服务"
}
},
"port_map": {
"443": "unginx (HTTPS)",
"80": "nginx (HTTP)",
"1883": "uemqx (MQTT)",
"8883": "uemqx (MQTT SSL)",
"8083-8084": "uemqx (MQTT WS)",
"18083": "uemqx (Dashboard)",
"8848": "unacos (HTTP)",
"9848": "unacos (gRPC)",
"8306": "umysql (映射容器内3306)",
"6379": "uredis (端口转发)",
"22122": "utracker (FastDFS tracker, host网络)",
"23000": "ustorage (FastDFS storage, host网络)",
"8085": "ujava3 (业务端口)",
"8000": "upython (CMDB)",
"8002": "upython (CMDB)",
"8012": "kkfile (文件预览)",
"62121-62122": "paperless",
"30880-30884": "ujava3 (Dubbo+Debug端口, 30884=meeting2.0 debug)",
"8719-8720": "ujava3 (RPC端口)",
"8993-8999": "ujava3 (内部端口)",
"9200-9204": "ujava3 (内部端口)",
"9903-9920": "ujava3 (Dubbo端口)",
"5000": "gunicorn (GPT API服务, 宿主机)",
"27018": "MongoDB (宿主机)",
"8081": "nginx mobile (移动端代理)"
},
"business_checks": {
"enabled": true,
"api_checks": [],
"frontend_urls": [
{"name": "后台管理", "url": "https://192.168.5.202/pc-vue2-backstage/"},
{"name": "会议预定V2", "url": "https://192.168.5.202/pc-vue2-meetngV2/"},
{"name": "会议预定V3", "url": "https://192.168.5.202/pc-vue2-meetngV3/"},
{"name": "运维监控", "url": "https://192.168.5.202/pc-vue2-moniter/"}
],
"special_services": [
{"name": "GPT API", "url": "http://192.168.5.202:5000/", "note": "gunicorn + uvicorn, 返回404正常(根路径无路由)"},
{"name": "GPT后端", "container": "ubainsgpts3", "note": "连接PG向量数据库+Redis,阿里DashScope LLM"}
]
},
"known_issues": [
{"service": "ujava2", "issue": "ujava2容器Exited(137) 7周前,已迁移到ujava3", "action": "ujava2为旧容器,可清理"},
{"service": "ubainsgpts2/ubainsgpts/aimodel4", "issue": "三个旧容器Exited(137)", "action": "旧版本GPT/AI容器,可清理"},
{"service": "uemqx", "issue": "EMQX 4.x版本(非5.x),Dashboard API /api/v5/login返回404", "reason": "4.x使用不同API路径(/api/v4/login)和认证机制", "action": "确认Dashboard凭据和API版本"},
{"service": "message-scheduling", "issue": "jar目录存在但进程未运行", "action": "确认是否需要启动"},
{"service": "python-voice", "issue": "无独立Docker容器,log目录为空", "reason": "可能未部署或使用其他方式运行", "action": "确认讯飞语音服务部署方式"},
{"service": "nacos", "issue": "日志1.9GB较大", "action": "检查日志轮转策略"},
{"service": "meeting2.0", "issue": "开启debug模式(Xdebug port 30884),日志521MB", "action": "生产环境建议关闭debug,清理旧日志"}
]
}
],
"default_server_id": 1,
......
......@@ -44,24 +44,48 @@ def wait_identity(d, timeout=12):
return False
def fill_identity(d):
"""处理'校验身份'对话框:密码+图形验证码+确定。"""
"""处理'校验身份'对话框:密码+图形验证码+确定。使用原生value setter触发Vue响应。"""
if not wait_identity(d,12):
return False
time.sleep(1)
d.execute_script("""
var dl=document.querySelectorAll('.el-dialog');
for(var i=0;i<dl.length;i++){
if(dl[i].offsetParent===null)continue;
if((dl[i].textContent||'').indexOf('校验身份')<0)continue;
var pwd=dl[i].querySelector('input[placeholder*="密码"]');
if(pwd){pwd.focus();pwd.value='Ubains@1357';pwd.dispatchEvent(new Event('input',{bubbles:true}));pwd.dispatchEvent(new Event('change',{bubbles:true}));}
var cap=dl[i].querySelector('input[placeholder*="验证码"]');
if(cap){cap.focus();cap.value='csba';cap.dispatchEvent(new Event('input',{bubbles:true}));cap.dispatchEvent(new Event('change',{bubbles:true}));}
var bs=dl[i].querySelectorAll('button');
for(var j=0;j<bs.length;j++){if(bs[j].textContent.indexOf('确定')>=0){bs[j].click();break;}}
// 使用原生HTMLInputElement value setter触发Vue v-model
var nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
var wrappers = document.querySelectorAll('.el-dialog__wrapper');
for(var i=0;i<wrappers.length;i++){
var w = wrappers[i];
if(w.style.display==='none') continue;
var dialog = w.querySelector('.el-dialog');
if(!dialog || (dialog.textContent||'').indexOf('校验身份')<0) continue;
var pwd = dialog.querySelector('input[placeholder*="密码"]');
if(pwd && !pwd.disabled){
pwd.focus();
nativeSetter.call(pwd, 'Ubains@1357');
pwd.dispatchEvent(new Event('input',{bubbles:true}));
pwd.dispatchEvent(new Event('change',{bubbles:true}));
pwd.dispatchEvent(new Event('blur',{bubbles:true}));
}
var cap = dialog.querySelector('input[placeholder*="验证码"]');
if(cap){
cap.focus();
nativeSetter.call(cap, 'csba');
cap.dispatchEvent(new Event('input',{bubbles:true}));
cap.dispatchEvent(new Event('change',{bubbles:true}));
cap.dispatchEvent(new Event('blur',{bubbles:true}));
}
// 等待Vue更新后点击确定
setTimeout(function(){
var bs = dialog.querySelectorAll('button');
for(var j=0;j<bs.length;j++){
if(bs[j].textContent.indexOf('确定')>=0 && !bs[j].disabled){
bs[j].click();
break;
}
}
}, 300);
}
""")
time.sleep(3)
time.sleep(5)
def login(d):
for attempt in range(3):
......@@ -70,12 +94,28 @@ def login(d):
WebDriverWait(d,30).until(EC.presence_of_element_located((By.TAG_NAME,"button")))
time.sleep(4)
d.execute_script("""
var nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
var ins=document.querySelectorAll('input');
for(var i=0;i<ins.length;i++){
var p=ins[i].placeholder||'';
if(p.indexOf('账号')>=0||p.indexOf('手机')>=0){ins[i].value='superadmin';ins[i].dispatchEvent(new Event('input',{bubbles:true}));}
else if(p.indexOf('密码')>=0){ins[i].value='Ubains@1357';ins[i].dispatchEvent(new Event('input',{bubbles:true}));}
else if(p.indexOf('验证码')>=0){ins[i].value='csba';ins[i].dispatchEvent(new Event('input',{bubbles:true}));}
if(p.indexOf('账号')>=0||p.indexOf('手机')>=0){
ins[i].focus();
nativeSetter.call(ins[i],'superadmin');
ins[i].dispatchEvent(new Event('input',{bubbles:true}));
ins[i].dispatchEvent(new Event('change',{bubbles:true}));
}
else if(p.indexOf('密码')>=0){
ins[i].focus();
nativeSetter.call(ins[i],'Ubains@1357');
ins[i].dispatchEvent(new Event('input',{bubbles:true}));
ins[i].dispatchEvent(new Event('change',{bubbles:true}));
}
else if(p.indexOf('验证码')>=0){
ins[i].focus();
nativeSetter.call(ins[i],'csba');
ins[i].dispatchEvent(new Event('input',{bubbles:true}));
ins[i].dispatchEvent(new Event('change',{bubbles:true}));
}
}
""")
time.sleep(2)
......
#!/bin/bash
# 导出单个 MySQL 数据库
# 环境变量: MYSQL_CONTAINER MYSQL_PORT MYSQL_USER MYSQL_PASS DB_NAME DUMP_OUTPUT
docker exec ${MYSQL_CONTAINER} mysqldump \
-h localhost -P ${MYSQL_PORT} \
-u"${MYSQL_USER}" -p"${MYSQL_PASS}" \
--databases "${DB_NAME}" \
--single-transaction \
--routines \
--triggers \
--events \
--skip-lock-tables \
2>/dev/null > "${DUMP_OUTPUT}"
echo "EXIT:$?"
#!/bin/bash
# 列出 umysql 容器中所有用户数据库(排除系统库)
# 环境变量: MYSQL_CONTAINER MYSQL_PORT MYSQL_USER MYSQL_PASS
DBS=$(docker exec ${MYSQL_CONTAINER} mysql -h localhost -P ${MYSQL_PORT} -u"${MYSQL_USER}" -p"${MYSQL_PASS}" -N -e "SHOW DATABASES" 2>/dev/null)
for db in $DBS; do
case "$db" in
information_schema|performance_schema|mysql|sys) ;;
*) echo -n "$db " ;;
esac
done
echo "EXIT:$?"
......@@ -7,8 +7,13 @@
Supports new unified platform and traditional platform.
.NOTES
Version: 2.0.0
Last Modified: 2026-06-05
Version: 2.1.0
Last Modified: 2026-07-01
Changelog:
- v2.1.0 (2026-07-01): 新增 Step 2.5 MySQL 数据库导出(mysqldump --all-databases),
导出 umysql 容器所有数据库到 config_and_log/mysql_dump/ 目录
- v2.0.0 (2026-06-05): 初始版本,支持新/旧平台配置和日志导出
#>
#region Module Initialization
......@@ -268,9 +273,157 @@ function Export-ConfigAndLogs {
Write-Host ""
Write-Log -Level "INFO" -Message "Scan done: $($exportedServices.Count) ok, $($skippedServices.Count) skipped, $($failedServices.Count) failed"
# Step 3: Remote compress
# Step 2.5: Export MySQL databases via mysqldump
Write-Host ""
Write-Log -Level "INFO" -Message "[Step 2.5] Export MySQL databases from umysql container"
$dbExportSuccess = $false
$dbExportDetail = ""
try {
# MySQL container and credentials
$mysqlContainer = "umysql"
$mysqlUser = "root"
$mysqlPass = "dNrprU&2S"
$mysqlPort = 8306
$dbDumpDir = "/tmp/mysql_dump"
$dbDumpFile = "all_databases_${timestamp}.sql"
# Check if umysql container is running
$checkContainerCmd = "docker ps --format '{{.Names}}' | grep -w '$mysqlContainer' | head -n 1"
$containerCheck = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $checkContainerCmd
if ($containerCheck.ExitCode -ne 0 -or -not $containerCheck.Output) {
Write-Log -Level "WARN" -Message " [SKIP] MySQL export - container '$mysqlContainer' not running"
$dbExportDetail = "Container not running"
}
else {
$actualContainer = (@($containerCheck.Output)[0].ToString().Trim() -replace "`r","")
Write-Log -Level "INFO" -Message " MySQL container found: $actualContainer"
# Create dump directory (each DB gets its own sub-directory)
$mkDumpDir = "mkdir -p '$dbDumpDir'"
[void](Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $mkDumpDir)
# Step 2.5a: Get list of user databases (exclude system DBs)
# 上传 shell 脚本到远程执行,彻底避开 PS 引号转义
Write-Log -Level "INFO" -Message " [CMD] Fetching database list..."
$listScriptLocal = Join-Path $ScriptDir "list_user_dbs.sh"
$listScriptRemote = "/tmp/.list_dbs_${timestamp}.sh"
$uploadOk = Copy-File-To-Remote -LocalPath $listScriptLocal -Server $Server -RemoteDir '/tmp'
if (-not $uploadOk) {
Write-Log -Level "WARN" -Message " [SKIP] Could not upload list_user_dbs.sh"
$dbExportDetail = "Script upload failed"
}
else {
# Move to named script, set env vars, execute
$mvCmd = "mv /tmp/list_user_dbs.sh $listScriptRemote ; chmod +x $listScriptRemote ; MYSQL_CONTAINER='$actualContainer' MYSQL_PORT='$mysqlPort' MYSQL_USER='$mysqlUser' MYSQL_PASS='$mysqlPass' bash $listScriptRemote ; rm -f $listScriptRemote"
$listResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $mvCmd
$listOutput = ($listResult.Output | Out-String).Trim()
if ($listOutput -notmatch 'EXIT:0') {
Write-Log -Level "WARN" -Message " [FAIL] Could not fetch database list"
$dbExportDetail = "SHOW DATABASES failed"
}
else {
# Parse database names
$dbNamesText = ($listOutput -replace 'EXIT:0', '').Trim()
$dbNames = @($dbNamesText -split '\s+' | Where-Object { $_.Trim() -ne '' -and $_.Trim() -ne 'EXIT:' })
if ($dbNames.Count -eq 0) {
Write-Log -Level "WARN" -Message " [SKIP] No user databases found"
$dbExportDetail = "No user databases"
}
else {
Write-Log -Level "INFO" -Message " Databases found ($($dbNames.Count)): $($dbNames -join ', ')"
# Step 2.5b: Export each database to a separate .sql file
$dbExportedCount = 0
$dbFailedCount = 0
$exportedDbFiles = @()
foreach ($dbName in $dbNames) {
$dbSafeName = $dbName.Trim()
$dbSqlFile = "${dbSafeName}_${timestamp}.sql"
$dbSqlPath = "$dbDumpDir/$dbSqlFile"
# 上传 dump_one_db.sh 到远程执行
$dumpScriptLocal = Join-Path $ScriptDir "dump_one_db.sh"
$dumpScriptRemote = "/tmp/.dump_${dbSafeName}_${timestamp}.sh"
$uploadOk2 = Copy-File-To-Remote -LocalPath $dumpScriptLocal -Server $Server -RemoteDir '/tmp'
if (-not $uploadOk2) {
Write-Log -Level "WARN" -Message " [FAIL] $dbSafeName - script upload failed"
$dbFailedCount++
continue
}
$mvDumpCmd = "mv /tmp/dump_one_db.sh $dumpScriptRemote ; chmod +x $dumpScriptRemote ; MYSQL_CONTAINER='$actualContainer' MYSQL_PORT='$mysqlPort' MYSQL_USER='$mysqlUser' MYSQL_PASS='$mysqlPass' DB_NAME='$dbSafeName' DUMP_OUTPUT='$dbSqlPath' bash $dumpScriptRemote ; rm -f $dumpScriptRemote"
Write-Log -Level "INFO" -Message " [CMD] mysqldump --databases $dbSafeName > $dbSqlFile"
$dumpOneResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $mvDumpCmd
$dumpOneOutput = ($dumpOneResult.Output | Out-String).Trim()
if ($dumpOneOutput -match 'EXIT:0') {
# Verify file size
$checkOneCmd = "if [ -f '$dbSqlPath' ]; then wc -c < '$dbSqlPath'; else echo 0; fi"
$checkOne = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $checkOneCmd
$oneSize = ([string]($checkOne.Output -join "")).Trim()
$oneSizeInt = 0
[int]::TryParse($oneSize, [ref]$oneSizeInt) | Out-Null
if ($oneSizeInt -gt 0) {
$oneSizeKB = [math]::Round($oneSizeInt / 1KB, 1)
Write-Log -Level "SUCCESS" -Message " [OK] $dbSafeName -> $dbSqlFile ($oneSizeKB KB)"
$dbExportedCount++
$exportedDbFiles += $dbSqlFile
}
else {
Write-Log -Level "WARN" -Message " [FAIL] $dbSafeName dump is empty"
$dbFailedCount++
}
}
else {
Write-Log -Level "WARN" -Message " [FAIL] $dbSafeName mysqldump error"
$dbFailedCount++
}
}
# Summary
if ($dbExportedCount -gt 0) {
$totalSizeCmd = "du -sh '$dbDumpDir' 2>/dev/null | awk '{print `$1}'"
$totalSizeResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $totalSizeCmd
$totalSize = ([string]($totalSizeResult.Output -join "")).Trim()
if (-not $totalSize) { $totalSize = "N/A" }
Write-Log -Level "SUCCESS" -Message " [OK] DB dump done: $dbExportedCount ok, $dbFailedCount failed, total $totalSize"
$dbExportSuccess = $true
$dbExportDetail = "$dbExportedCount databases ($totalSize)"
# Record as an exported service
$exportedServices += @{
Name = "mysql_dump"
Files = $exportedDbFiles
}
}
elseif ($dbFailedCount -gt 0) {
Write-Log -Level "WARN" -Message " [FAIL] All $dbFailedCount database dumps failed"
$dbExportDetail = "All $dbFailedCount dumps failed"
}
}
}
}
}
}
catch {
Write-Log -Level "WARN" -Message " [FAIL] MySQL export exception: $($_.Exception.Message)"
$dbExportDetail = "Exception: $($_.Exception.Message)"
}
if (-not $dbExportSuccess) {
Write-Log -Level "WARN" -Message " MySQL database export skipped or failed: $dbExportDetail"
}
# Step 3: Remote compress (config_and_log + mysql_dump 同级打包)
Write-Log -Level "INFO" -Message "[Step 3] Compress: $remoteArchive"
$tarCmd = "cd /tmp; tar -czf '${archiveName}' config_and_log 2>/dev/null; if [ `$? -eq 0 ]; then echo TAR_OK; else echo TAR_FAIL; fi"
$tarCmd = "cd /tmp; tar -czf '${archiveName}' config_and_log mysql_dump 2>/dev/null; if [ `$? -eq 0 ]; then echo TAR_OK; else echo TAR_FAIL; fi"
$tarResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $tarCmd
$tarOutput = ($tarResult.Output | Out-String).Trim()
......@@ -352,7 +505,7 @@ function Export-ConfigAndLogs {
# Step 5: Clean up remote temp files
Write-Log -Level "INFO" -Message "[Step 5] Clean up remote temp"
$cleanCmd = "rm -rf $remoteTmpBase /tmp/$archiveName"
$cleanCmd = "rm -rf $remoteTmpBase /tmp/$archiveName /tmp/mysql_dump"
[void](Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $cleanCmd)
Write-Log -Level "SUCCESS" -Message "[Step 5] Remote temp cleaned"
......
......@@ -214,88 +214,131 @@ function python_x86() {
#x86架构python播放器系统安装
function python_player_x86() {
echo -e "\033[36m* 正在检查Python服务是否安装......\033[0m"
# 检查Docker镜像和容器状态
local image_exists=$(docker images | grep -q python && echo "true" || echo "false")
local container_running=$(docker ps | grep -q uplayer && echo "true" || echo "false")
local player_dir="/var/www/player"
local source_player_dir="/home/player_auto_install/player"
local docker_image_path="/home/player_auto_install/server_bag/upython_player.tar.gz"
local start_script="/var/www/player/start.sh"
if [[ "$image_exists" == "true" && "$container_running" == "true" ]]; then
echo -e "\033[32m* 检查到Python已安装!正在安装下一个服务....\033[0m"
log "INFO" "=================================================================="
log "INFO" "开始部署 Python Player 容器 (uplayer) - x86"
log "INFO" "=================================================================="
# ------------------- 定义变量 -------------------
local image_tar="/data/temp/upython.tar"
local image_tag="139.9.60.86:5000/upython:v11"
local container_name="upython_player"
local host_player_dir="/data/services/api/python-player"
local source_player_dir="/data/temp/player"
local container_start_cmd="/root/start.sh"
# ------------------- 检查容器是否已存在 -------------------
if $sudoset docker ps -a --format '{{.Names}}' | grep -wq "^$container_name$"; then
# 容器已存在
if $sudoset docker ps --format '{{.Names}}' | grep -wq "^$container_name$"; then
log "WARN" "✅ 容器 '$container_name' 已在运行。自动退出部署操作!!!"
return 0
else
echo -e "\033[33m* 检查到Python服务未安装,正在安装服务....\033[0m"
# 加载Docker镜像并验证其存在
if [ ! -f "$docker_image_path" ]; then
echo -e "\033[31m* 错误: Docker镜像文件 $docker_image_path 不存在。\033[0m"
log "WARN" "🔄 容器 '$container_name' 存在但未运行,正在启动..."
$sudoset docker start "$container_name"
sleep 3
if $sudoset docker ps --format '{{.Names}}' | grep -wq "^$container_name$"; then
log "INFO" "✅ 容器 '$container_name' 已成功启动"
return 0
else
log "ERROR" "⛔ 容器启动失败,请检查日志: docker logs $container_name"
return 1
fi
if ! docker load -i "$docker_image_path"; then
echo -e "\033[31m* 加载Docker镜像失败。\033[0m"
return 1
fi
fi
# 标记Docker镜像
if ! docker tag e449dd1ea3af 139.9.60.86:5000/upython:v11; then
echo -e "\033[31m* 标记Docker镜像失败。\033[0m"
# ------------------- 检查并准备挂载目录 -------------------
if [[ ! -d "$source_player_dir" ]]; then
log "ERROR" "⛔ 源目录不存在: $source_player_dir"
return 1
fi
# 检查player目录是否存在,如果不存在则复制
if [ ! -d "$source_player_dir" ]; then
echo -e "\033[31m* 错误: 源目录 $source_player_dir 不存在。\033[0m"
if [[ ! -d "$host_player_dir" ]]; then
log "INFO" "📂 目标目录不存在,正在从源目录复制..."
mkdir -p "$host_player_dir"
if ! cp -rp "$source_player_dir"/* "$host_player_dir"/; then
log "ERROR" "⛔ 复制 player 目录失败"
return 1
fi
log "INFO" "🎉 成功复制 player 目录到 $host_player_dir"
if [ ! -d "$player_dir" ]; then
mkdir -p /var/www/player
if ! cp -rp "$source_player_dir" /var/www/; then
echo -e "\033[31m* 复制player目录失败。\033[0m"
return 1
else
echo -e "\033[32m* 成功复制 player 目录。\033[0m"
chmod_files=(
"$player_dir/start.sh"
"$player_dir/manage.py"
"$player_dir/uuwsgi.ini"
"$player_dir/uplayer"
"$player_dir/UbainsDevOps"
# 赋予执行权限
local chmod_files=(
"$host_player_dir/start.sh"
"$host_player_dir/manage.py"
"$host_player_dir/uuwsgi.ini"
"$host_player_dir/uplayer"
"$host_player_dir/UbainsDevOps"
)
chmod 755 -R "${chmod_files[@]}"
fi
else
echo -e "\033[32m* 目录 $player_dir 已存在,跳过复制。\033[0m"
log "INFO" "✅ 目录 $host_player_dir 已存在,跳过复制"
fi
# 确认启动脚本存在
if [ ! -f "$start_script" ]; then
echo -e "\033[31m* 错误: 启动脚本 $start_script 不存在。\033[0m"
if [[ ! -f "$host_player_dir/start.sh" ]]; then
log "ERROR" "⛔ 启动脚本缺失: $host_player_dir/start.sh"
return 1
fi
# ------------------- 加载镜像 -------------------
if $sudoset docker images --format '{{.Repository}}:{{.Tag}}' | grep -Fq "$image_tag"; then
log "INFO" "✅ 镜像 $image_tag 已存在,跳过加载"
else
log "INFO" "📦 正在从离线包加载镜像: $image_tar"
if [[ -f "$image_tar" ]]; then
$sudoset docker load -i "$image_tar"
if [[ $? -ne 0 ]]; then
log "ERROR" "⛔ 镜像加载失败,请检查文件完整性: $image_tar"
return 1
fi
log "INFO" "🎉 镜像加载成功: $image_tag"
# 标记Docker镜像
if ! docker tag 024630e8dca0 139.9.60.86:5000/upython:v11; then
echo -e "\033[31m* 标记Docker镜像失败。\033[0m"
return 1
fi
else
log "ERROR" "⛔ 镜像文件不存在: $image_tar"
return 1
fi
# 启动容器,并指定固定的MAC地址
if ! docker run -itd \
fi
# ------------------- 启动新容器 -------------------
log "INFO" "🚀 正在创建并启动容器 '$container_name'..."
$sudoset docker run -d \
--name "$container_name" \
--restart=always \
--mac-address="02:42:ac:12:00:05" \
-p 18082:8082 -p 11883:1883 -p 8000:8000 -p 9001:9001 -p 8002:8002\
-v /var/www/player:/var/www/html \
--privileged \
-p 18082:8082 \
-p 11883:1883 \
-p 8005:8000 \
-p 9002:9001 \
-p 8006:8002 \
-v "$host_player_dir:/var/www/html" \
-v /etc/localtime:/etc/localtime:ro \
-v /usr/share/zoneinfo:/usr/share/zoneinfo:ro \
--restart=always \
--privileged \
--name=uplayer \
139.9.60.86:5000/upython:v11 \
/root/start.sh; then
echo -e "\033[31m* 启动Python容器失败。\033[0m"
"$image_tag" \
"$container_start_cmd"
if [[ $? -ne 0 ]]; then
log "ERROR" "⛔ 容器创建失败,请检查参数或资源"
return 1
else
echo -e "\033[32m* Python服务安装完成并启动成功!\033[0m"
fi
log "INFO" "✅ 容器 '$container_name' 创建并启动成功"
# ------------------- 状态验证 -------------------
sleep 3
if $sudoset docker ps --format '{{.Names}}' | grep -wq "^$container_name$"; then
log "INFO" "🟢 Python Player 服务运行中 → 访问端口: 18082, 11883, 8005, 9002, 8006"
else
log "ERROR" "⛔ 容器启动后退出,请查看日志: docker logs $container_name"
return 1
fi
return 0
}
# ========================================
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论