Compare commits
2 Commits
5ace2cc5ac
...
48953c0dbd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48953c0dbd | ||
|
|
bfbd649f9f |
@ -414,3 +414,12 @@ To https://git.valavala.com/ai_member_only/ai_member_xiaoban
|
|||||||
906af79..e279bc7 master -> master
|
906af79..e279bc7 master -> master
|
||||||
[2026-05-24 08:10:02] 工作区备份成功:自动备份 2026-05-24 08:10:01
|
[2026-05-24 08:10:02] 工作区备份成功:自动备份 2026-05-24 08:10:01
|
||||||
[2026-05-25 08:10:01] 开始备份工作区...
|
[2026-05-25 08:10:01] 开始备份工作区...
|
||||||
|
[master 5ace2cc] 自动备份 2026-05-25 08:10:01
|
||||||
|
2 files changed, 1 insertion(+), 11 deletions(-)
|
||||||
|
delete mode 100644 tmp_daily_summary.md
|
||||||
|
remote: . Processing 1 references
|
||||||
|
remote: Processed 1 references in total
|
||||||
|
To https://git.valavala.com/ai_member_only/ai_member_xiaoban
|
||||||
|
8f74b8e..5ace2cc master -> master
|
||||||
|
[2026-05-25 08:10:01] 工作区备份成功:自动备份 2026-05-25 08:10:01
|
||||||
|
[2026-05-26 08:10:01] 开始备份工作区...
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
{"type":"memory.recall.recorded","timestamp":"2026-05-24T02:48:04.923Z","query":"PostgreSQL online database password PG_DB_PASSWORD","resultCount":4,"results":[{"path":"memory/2026-05-24.md","startLine":1,"endLine":30,"score":1},{"path":"memory/2026-05-24.md","startLine":23,"endLine":52,"score":1},{"path":"memory/2026-03-01.md","startLine":1,"endLine":11,"score":1},{"path":"memory/2026-05-24.md","startLine":46,"endLine":61,"score":1}]}
|
{"type":"memory.recall.recorded","timestamp":"2026-05-24T02:48:04.923Z","query":"PostgreSQL online database password PG_DB_PASSWORD","resultCount":4,"results":[{"path":"memory/2026-05-24.md","startLine":1,"endLine":30,"score":1},{"path":"memory/2026-05-24.md","startLine":23,"endLine":52,"score":1},{"path":"memory/2026-03-01.md","startLine":1,"endLine":11,"score":1},{"path":"memory/2026-05-24.md","startLine":46,"endLine":61,"score":1}]}
|
||||||
|
{"type":"memory.recall.recorded","timestamp":"2026-05-25T05:47:41.388Z","query":"账号信息查询 account_id 14157 数据库","resultCount":4,"results":[{"path":"memory/2026-05-24.md","startLine":46,"endLine":71,"score":1},{"path":"memory/2026-05-24.md","startLine":85,"endLine":110,"score":1},{"path":"memory/2026-05-24.md","startLine":23,"endLine":52,"score":1},{"path":"memory/2026-05-24.md","startLine":1,"endLine":30,"score":1}]}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"updatedAt": "2026-05-24T02:48:04.923Z",
|
"updatedAt": "2026-05-25T05:47:41.388Z",
|
||||||
"entries": {
|
"entries": {
|
||||||
"memory:memory/2026-05-24.md:1:30": {
|
"memory:memory/2026-05-24.md:1:30": {
|
||||||
"key": "memory:memory/2026-05-24.md:1:30",
|
"key": "memory:memory/2026-05-24.md:1:30",
|
||||||
@ -9,18 +9,20 @@
|
|||||||
"endLine": 30,
|
"endLine": 30,
|
||||||
"source": "memory",
|
"source": "memory",
|
||||||
"snippet": "# 2026-05-24 工作日志 ## 新建技能: studytime-analysis [刘庆逊提出] 创建学习时间分析技能,分析角色完课记录的规律。 ### 技能结构 - `skills/studytime-analysis/SKILL.md` — 技能定义 - `skills/studytime-analysis/scripts/studytime_analysis.py` — Python 分析脚本 ### 分析维度 1. **一周时间分布**(排除寒暑假1-2月、7-8月):周一~周日各天完课数、时段分布(上午/中午/下午/晚上)、周末是否上课 2. **跨周学习趋势**(包含寒暑假全部数据):总周数、周均完课数、连续性、中断周检测、前后半段趋势对比、突增/骤降检测 3. **完课记录明细表**(全部数据):日期/时间/星期/时段/级别/课程ID ### 数据源 - PostgreSQL Online(vala 库) - 核心表:`user_chapter_play_record_0~7`(8张分表,无 `bi_` 前缀) - 筛选:`play_status = 1` - 注意:表在 PostgreSQL 而非 MySQL,表名无 `bi_` 前缀 ### 寒暑假规则 - 一周分布分析时排除 1-2 月(寒假)和 7-8 月(暑假)—— 因为寒暑假作息与平时差异大,混在一起会干扰时段分析 - 跨周趋势和明细表包含全部数据(含寒暑假) - 报告中区分标注数据范围 ### 触发方式 用户说「学习时间分析 [角色ID]」即可触发 ### 已测试角色",
|
"snippet": "# 2026-05-24 工作日志 ## 新建技能: studytime-analysis [刘庆逊提出] 创建学习时间分析技能,分析角色完课记录的规律。 ### 技能结构 - `skills/studytime-analysis/SKILL.md` — 技能定义 - `skills/studytime-analysis/scripts/studytime_analysis.py` — Python 分析脚本 ### 分析维度 1. **一周时间分布**(排除寒暑假1-2月、7-8月):周一~周日各天完课数、时段分布(上午/中午/下午/晚上)、周末是否上课 2. **跨周学习趋势**(包含寒暑假全部数据):总周数、周均完课数、连续性、中断周检测、前后半段趋势对比、突增/骤降检测 3. **完课记录明细表**(全部数据):日期/时间/星期/时段/级别/课程ID ### 数据源 - PostgreSQL Online(vala 库) - 核心表:`user_chapter_play_record_0~7`(8张分表,无 `bi_` 前缀) - 筛选:`play_status = 1` - 注意:表在 PostgreSQL 而非 MySQL,表名无 `bi_` 前缀 ### 寒暑假规则 - 一周分布分析时排除 1-2 月(寒假)和 7-8 月(暑假)—— 因为寒暑假作息与平时差异大,混在一起会干扰时段分析 - 跨周趋势和明细表包含全部数据(含寒暑假) - 报告中区分标注数据范围 ### 触发方式 用户说「学习时间分析 [角色ID]」即可触发 ### 已测试角色",
|
||||||
"recallCount": 1,
|
"recallCount": 2,
|
||||||
"dailyCount": 0,
|
"dailyCount": 0,
|
||||||
"groundedCount": 0,
|
"groundedCount": 0,
|
||||||
"totalScore": 1,
|
"totalScore": 2,
|
||||||
"maxScore": 1,
|
"maxScore": 1,
|
||||||
"firstRecalledAt": "2026-05-24T02:48:04.923Z",
|
"firstRecalledAt": "2026-05-24T02:48:04.923Z",
|
||||||
"lastRecalledAt": "2026-05-24T02:48:04.923Z",
|
"lastRecalledAt": "2026-05-25T05:47:41.388Z",
|
||||||
"queryHashes": [
|
"queryHashes": [
|
||||||
"c2d15f7574fb"
|
"c2d15f7574fb",
|
||||||
|
"9aff8ec9594a"
|
||||||
],
|
],
|
||||||
"recallDays": [
|
"recallDays": [
|
||||||
"2026-05-24"
|
"2026-05-24",
|
||||||
|
"2026-05-25"
|
||||||
],
|
],
|
||||||
"conceptTags": [
|
"conceptTags": [
|
||||||
"studytime-analysis",
|
"studytime-analysis",
|
||||||
@ -40,18 +42,20 @@
|
|||||||
"endLine": 52,
|
"endLine": 52,
|
||||||
"source": "memory",
|
"source": "memory",
|
||||||
"snippet": "- 一周分布分析时排除 1-2 月(寒假)和 7-8 月(暑假)—— 因为寒暑假作息与平时差异大,混在一起会干扰时段分析 - 跨周趋势和明细表包含全部数据(含寒暑假) - 报告中区分标注数据范围 ### 触发方式 用户说「学习时间分析 [角色ID]」即可触发 ### 已测试角色 - 2343、2344:无完课记录(play_status=2,未完成) - 2840:276条记录,秋季集中型用户 - 25976:265条,246条在W16周一天完成(A2批量),疑似系统批量标记 - 2895:188条,长期稳定学习型用户,36周几乎不间断,非寒暑假晚上为主,寒暑假上午为主 ### 技术要点 - psycopg2 的 `%(param_name)s` 命名参数必须正确匹配,UNION ALL 多个子查询需要不同参数名 - PostgreSQL 返回的 `updated_at` 是 tz-aware datetime - `datetime.fromisocalendar(year, week, 1)` 获取某周周一的日期 ### 同步 - 已推送到 SkillHub(`studytime-analysis.xiaoban`) - 已 commit 到 Git 远程仓库 - 已通知 Cris(李若松) ### 增强: 报告开头加入角色基本信息 (2026-05-24) [刘庆逊提出] 在 studytime-analysis 输出中加入角色基本信息,包括: - 角色ID、账号ID、角色名字、性别、年龄、账号手机号后4位 **数据源(新增)**: - MySQL Onli",
|
"snippet": "- 一周分布分析时排除 1-2 月(寒假)和 7-8 月(暑假)—— 因为寒暑假作息与平时差异大,混在一起会干扰时段分析 - 跨周趋势和明细表包含全部数据(含寒暑假) - 报告中区分标注数据范围 ### 触发方式 用户说「学习时间分析 [角色ID]」即可触发 ### 已测试角色 - 2343、2344:无完课记录(play_status=2,未完成) - 2840:276条记录,秋季集中型用户 - 25976:265条,246条在W16周一天完成(A2批量),疑似系统批量标记 - 2895:188条,长期稳定学习型用户,36周几乎不间断,非寒暑假晚上为主,寒暑假上午为主 ### 技术要点 - psycopg2 的 `%(param_name)s` 命名参数必须正确匹配,UNION ALL 多个子查询需要不同参数名 - PostgreSQL 返回的 `updated_at` 是 tz-aware datetime - `datetime.fromisocalendar(year, week, 1)` 获取某周周一的日期 ### 同步 - 已推送到 SkillHub(`studytime-analysis.xiaoban`) - 已 commit 到 Git 远程仓库 - 已通知 Cris(李若松) ### 增强: 报告开头加入角色基本信息 (2026-05-24) [刘庆逊提出] 在 studytime-analysis 输出中加入角色基本信息,包括: - 角色ID、账号ID、角色名字、性别、年龄、账号手机号后4位 **数据源(新增)**: - MySQL Onli",
|
||||||
"recallCount": 1,
|
"recallCount": 2,
|
||||||
"dailyCount": 0,
|
"dailyCount": 0,
|
||||||
"groundedCount": 0,
|
"groundedCount": 0,
|
||||||
"totalScore": 1,
|
"totalScore": 2,
|
||||||
"maxScore": 1,
|
"maxScore": 1,
|
||||||
"firstRecalledAt": "2026-05-24T02:48:04.923Z",
|
"firstRecalledAt": "2026-05-24T02:48:04.923Z",
|
||||||
"lastRecalledAt": "2026-05-24T02:48:04.923Z",
|
"lastRecalledAt": "2026-05-25T05:47:41.388Z",
|
||||||
"queryHashes": [
|
"queryHashes": [
|
||||||
"c2d15f7574fb"
|
"c2d15f7574fb",
|
||||||
|
"9aff8ec9594a"
|
||||||
],
|
],
|
||||||
"recallDays": [
|
"recallDays": [
|
||||||
"2026-05-24"
|
"2026-05-24",
|
||||||
|
"2026-05-25"
|
||||||
],
|
],
|
||||||
"conceptTags": [
|
"conceptTags": [
|
||||||
"1-2",
|
"1-2",
|
||||||
@ -125,6 +129,68 @@
|
|||||||
"vala-app-account",
|
"vala-app-account",
|
||||||
"get-mysql-connection"
|
"get-mysql-connection"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"memory:memory/2026-05-24.md:46:71": {
|
||||||
|
"key": "memory:memory/2026-05-24.md:46:71",
|
||||||
|
"path": "memory/2026-05-24.md",
|
||||||
|
"startLine": 46,
|
||||||
|
"endLine": 71,
|
||||||
|
"source": "memory",
|
||||||
|
"snippet": "### 增强: 报告开头加入角色基本信息 (2026-05-24) [刘庆逊提出] 在 studytime-analysis 输出中加入角色基本信息,包括: - 角色ID、账号ID、角色名字、性别、年龄、账号手机号后4位 **数据源(新增)**: - MySQL Online `vala_user` 库 - `vala_app_character` 表:id, account_id, nickname, gender(0=女/1=男), birthday(varchar \"YYYY-MM-DD\") - `vala_app_account` 表:id, tel(已脱敏如 186****1625) - 手机号已脱敏,直接取后4位;年龄从 birthday 计算 **修改文件**: - `skills/studytime-analysis/scripts/studytime_analysis.py`:新增 MySQL 连接函数 `get_mysql_connection()` 和 `fetch_role_info(role_id)`,更新 `format_report()` 输出基本角色信息 - 已验证 2895 正常运行输出 - 已同步 SkillHub + Git ### Unit 显示修复: 季度名称 → 全局单元编号 (2026-05-24) [刘庆逊提出] HTML 报告中 Unit 列显示错误——显示的是季度名称(如\"小镇时光\"\"钢铁之心\")而非单元数字(0-48)。 **根因分析**: - `vala_game_chapter`(MySQL)无 `unit_",
|
||||||
|
"recallCount": 1,
|
||||||
|
"dailyCount": 0,
|
||||||
|
"groundedCount": 0,
|
||||||
|
"totalScore": 1,
|
||||||
|
"maxScore": 1,
|
||||||
|
"firstRecalledAt": "2026-05-25T05:47:41.388Z",
|
||||||
|
"lastRecalledAt": "2026-05-25T05:47:41.388Z",
|
||||||
|
"queryHashes": [
|
||||||
|
"9aff8ec9594a"
|
||||||
|
],
|
||||||
|
"recallDays": [
|
||||||
|
"2026-05-25"
|
||||||
|
],
|
||||||
|
"conceptTags": [
|
||||||
|
"studytime-analysis",
|
||||||
|
"vala-user",
|
||||||
|
"vala-app-character",
|
||||||
|
"account-id",
|
||||||
|
"女/1",
|
||||||
|
"yyyy-mm-dd",
|
||||||
|
"vala-app-account",
|
||||||
|
"get-mysql-connection"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"memory:memory/2026-05-24.md:85:110": {
|
||||||
|
"key": "memory:memory/2026-05-24.md:85:110",
|
||||||
|
"path": "memory/2026-05-24.md",
|
||||||
|
"startLine": 85,
|
||||||
|
"endLine": 110,
|
||||||
|
"source": "memory",
|
||||||
|
"snippet": "- `skills/studytime-analysis/scripts/studytime_analysis.py` — 重写 `fetch_chapter_info_map()`,新增全局 unit_index 计算;HTML 模板更新为 Level/Unit/Lesson 三列 - 已为角色 32009(zyl)重新生成 HTML 并发送 - 已同步 Git + SkillHub ## 新建技能: studycourse-analysis (2026-05-24) [刘庆逊提出] 创建角色上课情况分析技能,从四维度分析角色学习数据。 ### 技能结构 - `skills/studycourse-analysis/SKILL.md` — 技能定义 - `skills/studycourse-analysis/scripts/studycourse_analysis.py` — Python 分析脚本 ### 四步分析 1. **基础信息**:角色姓名/年龄/账号ID/手机号后4位/注册时间/购买渠道/设备/首末次完课 2. **完课耗时**:平均值/中位数、异常检测(<10min / >20min)、前后半段趋势 3. **中互动正确率**:Perfect/Good/Oops/Pass/Failed 占比和趋势 4. **知识巩固**:完成率、正确率得分分布 ### 数据源 | 类型 | 库 | 表 | 用途 | |------|-----|-----|------| | MySQL vala_user | vala_app_character | 角色信息、pu",
|
||||||
|
"recallCount": 1,
|
||||||
|
"dailyCount": 0,
|
||||||
|
"groundedCount": 0,
|
||||||
|
"totalScore": 1,
|
||||||
|
"maxScore": 1,
|
||||||
|
"firstRecalledAt": "2026-05-25T05:47:41.388Z",
|
||||||
|
"lastRecalledAt": "2026-05-25T05:47:41.388Z",
|
||||||
|
"queryHashes": [
|
||||||
|
"9aff8ec9594a"
|
||||||
|
],
|
||||||
|
"recallDays": [
|
||||||
|
"2026-05-25"
|
||||||
|
],
|
||||||
|
"conceptTags": [
|
||||||
|
"fetch-chapter-info-map",
|
||||||
|
"unit-index",
|
||||||
|
"level/unit/lesson",
|
||||||
|
"studycourse-analysis",
|
||||||
|
"平均值/中位数",
|
||||||
|
"perfect/good/oops/pass/failed",
|
||||||
|
"vala-user",
|
||||||
|
"vala-app-character"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
memory/2026-05-25.md
Normal file
25
memory/2026-05-25.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# 2026-05-25 工作日志
|
||||||
|
|
||||||
|
## user-info 技能重写
|
||||||
|
|
||||||
|
[刘庆逊提出] 修复 `user-info` 技能,使其匹配线上实际数据库结构。
|
||||||
|
|
||||||
|
### 问题
|
||||||
|
旧脚本引用的表(`bi_vala_app_account`、`account_login`、`account_detail_info`、`bi_vala_order`、`bi_level_unit_lesson`)在线上数据库均不存在。
|
||||||
|
|
||||||
|
### 修复内容
|
||||||
|
- **scripts/query_user_info.py** 完整重写:
|
||||||
|
- 表名改为实际线上表:`vala_user.vala_app_account`、`vala_user.vala_app_character`、`vala_order.vala_seasonal_ticket`、PG `user_chapter_play_record_0~7`
|
||||||
|
- 手机号查询通过 `tel LIKE '前缀%后缀'` 脱敏匹配
|
||||||
|
- Chapter → Level/Unit/Lesson 映射复用 studytime-analysis 的 `fetch_chapter_info_map()` 逻辑
|
||||||
|
- 订单数据改用 `vala_seasonal_ticket`(赛季通票),因线上无标准订单表
|
||||||
|
- 设备/地域信息标注为暂不可用(线上无对应表)
|
||||||
|
- PG 时区处理:`created_at` 为 tz-aware,统一转 naive 比较
|
||||||
|
- **SKILL.md** 更新至 v2.0.0,补充数据覆盖说明
|
||||||
|
- **references/database_schema.md** 重写为实际线上表结构
|
||||||
|
|
||||||
|
### 已验证的查询方式
|
||||||
|
- `--account-id 14157` ✓
|
||||||
|
- `--role-id 18556` ✓
|
||||||
|
- `--phone 18000007778` ✓(脱敏匹配)
|
||||||
|
- `--order-id <id>` ✓(含账号有效性校验)
|
||||||
79
skills/user-info/SKILL.md
Normal file
79
skills/user-info/SKILL.md
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
name: user-info
|
||||||
|
version: 2.0.0
|
||||||
|
description: Query complete user info from online databases — resolve account/role/phone/ticket ID to unified account profile with tickets, characters, learning progress and activity levels.
|
||||||
|
---
|
||||||
|
|
||||||
|
# user-info — 用户信息查询
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
用户提到以下任意一种请求时触发:
|
||||||
|
- "帮我查询一下账号 [ID] 的信息"
|
||||||
|
- "查一下角色 [ID] 的信息"
|
||||||
|
- "查一下手机号 [号码] 的信息"
|
||||||
|
- "查一下订单号 [ID] 的信息"
|
||||||
|
- "查一下用户信息" / "用户信息查询"
|
||||||
|
|
||||||
|
## Core Rules
|
||||||
|
|
||||||
|
1. 从用户输入中提取查询条件(账号ID / 角色ID / 手机号 / 订单号),数字型优先匹配 ID,11 位数字优先匹配手机号
|
||||||
|
2. 调用 `scripts/query_user_info.py` 执行查询,传入对应参数
|
||||||
|
3. 将脚本输出的格式化结果原样返回给用户
|
||||||
|
4. 若脚本报错(如未找到账号),将错误信息如实反馈给用户
|
||||||
|
5. 所有查询均为只读操作,不修改任何数据
|
||||||
|
|
||||||
|
## 执行命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 skills/user-info/scripts/query_user_info.py --account-id <ID>
|
||||||
|
python3 skills/user-info/scripts/query_user_info.py --role-id <ID>
|
||||||
|
python3 skills/user-info/scripts/query_user_info.py --phone <号码>
|
||||||
|
python3 skills/user-info/scripts/query_user_info.py --order-id <ID>
|
||||||
|
```
|
||||||
|
|
||||||
|
支持 `--json` 参数输出 JSON 格式。
|
||||||
|
|
||||||
|
## 查询内容
|
||||||
|
|
||||||
|
脚本自动完成以下五步查询并格式化输出:
|
||||||
|
|
||||||
|
| 步骤 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 第1步 | 解析输入 → 定位唯一 account_id |
|
||||||
|
| 第2步 | 账号基本信息:注册时间、最近活跃、角色数、赛季通票数、下载渠道、注册来源 |
|
||||||
|
| 第3步 | 全部赛季通票(订单):类型、套餐名、发放/过期/使用时间、状态 |
|
||||||
|
| 第4步 | 全部角色:名称、性别、年龄、当前课程位置(Level/Unit/Lesson)、上次完课时间、活跃度 |
|
||||||
|
| 第5步 | 汇总格式化输出 |
|
||||||
|
|
||||||
|
## 活跃度判定规则
|
||||||
|
|
||||||
|
- 无完课记录 → **无完课记录**
|
||||||
|
- 最近 14 天无完课记录 → **流失**
|
||||||
|
- 最近 14 天完课 > 10 次 → **高活跃**
|
||||||
|
- 最近 14 天完课 < 5 次 → **低活跃**
|
||||||
|
- 其余 → **正常活跃**
|
||||||
|
|
||||||
|
## 数据覆盖说明
|
||||||
|
|
||||||
|
线上数据库表结构与测试环境不同,以下字段暂不可用:
|
||||||
|
|
||||||
|
| 字段 | 原因 |
|
||||||
|
|------|------|
|
||||||
|
| 登录设备记录 | 线上无 `account_login` 表 |
|
||||||
|
| 地域信息 | 线上无 `account_detail_info` 表 |
|
||||||
|
| 商品订单(金额、渠道) | 线上仅 `vala_seasonal_ticket`(赛季通票),无标准订单表 |
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| 内容 | 文件 |
|
||||||
|
|------|------|
|
||||||
|
| 数据库表结构参考 | `references/database_schema.md` |
|
||||||
|
| 查询脚本 | `scripts/query_user_info.py` |
|
||||||
|
|
||||||
|
## 依赖
|
||||||
|
|
||||||
|
- Python 3
|
||||||
|
- pymysql, psycopg2-binary
|
||||||
|
- MySQL Online:vala_user(账号/角色)、vala_order(赛季通票)、vala(章节/赛季映射)
|
||||||
|
- PostgreSQL Online vala(user_chapter_play_record_0~7)
|
||||||
103
skills/user-info/references/database_schema.md
Normal file
103
skills/user-info/references/database_schema.md
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
# 数据库表结构参考 — user-info 技能
|
||||||
|
|
||||||
|
## 涉及数据源
|
||||||
|
|
||||||
|
| 数据库 | 数据库名 | 用途 | 表 |
|
||||||
|
|--------|---------|------|-----|
|
||||||
|
| MySQL Online | vala_user | 账号/角色 | vala_app_account, vala_app_character |
|
||||||
|
| MySQL Online | vala_order | 赛季通票(订单) | vala_seasonal_ticket |
|
||||||
|
| MySQL Online | vala | 课程结构 | vala_game_chapter, vala_game_season_package |
|
||||||
|
| PostgreSQL Online | vala | 学习行为 | user_chapter_play_record_0~7 |
|
||||||
|
|
||||||
|
## MySQL 核心表
|
||||||
|
|
||||||
|
### vala_user.vala_app_account — 用户账号表
|
||||||
|
| 字段 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| id | 账号ID |
|
||||||
|
| tel | 手机号(脱敏,格式 138****1234) |
|
||||||
|
| created_at | 注册时间 |
|
||||||
|
| updated_at | 最近活跃时间 |
|
||||||
|
| login_times | 登录次数 |
|
||||||
|
| key_from | 注册来源 |
|
||||||
|
| download_channel | 下载渠道编码 |
|
||||||
|
| status | 1=有效 |
|
||||||
|
| deleted_at | 软删除标记 |
|
||||||
|
|
||||||
|
### vala_user.vala_app_character — 角色表
|
||||||
|
| 字段 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| id | 角色ID |
|
||||||
|
| account_id | 所属账号ID |
|
||||||
|
| nickname | 角色名称 |
|
||||||
|
| gender | 0=女, 1=男 |
|
||||||
|
| birthday | 生日 (YYYY-MM-DD) |
|
||||||
|
| latest_login | 最后登录时间 |
|
||||||
|
| created_at | 创建时间 |
|
||||||
|
| purchase_season_package | 已购赛季包列表 |
|
||||||
|
| deleted_at | 软删除标记 |
|
||||||
|
|
||||||
|
### vala_order.vala_seasonal_ticket — 赛季通票表(订单替代)
|
||||||
|
| 字段 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| id | 通票ID |
|
||||||
|
| account_id | 账号ID |
|
||||||
|
| season_package_name | 套餐名称 |
|
||||||
|
| ticket_type | 1=赛季通票, 2=兑换卡 |
|
||||||
|
| status | -1=已过期, 0=未使用, 1=已使用 |
|
||||||
|
| give_time | 发放时间 (Unix时间戳) |
|
||||||
|
| expire_time | 过期时间 (Unix时间戳) |
|
||||||
|
| used_time | 使用时间 (Unix时间戳) |
|
||||||
|
| created_at | 创建时间 |
|
||||||
|
|
||||||
|
### vala.vala_game_chapter — 课程章节表
|
||||||
|
| 字段 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| id | 章节ID(关联 play_record.chapter_id) |
|
||||||
|
| cn_name | 中文名称 |
|
||||||
|
| season_package_id | 所属赛季包ID |
|
||||||
|
| lesson_type | 1=常规课(用于单元分组) |
|
||||||
|
| index | 课程序号 |
|
||||||
|
|
||||||
|
### vala.vala_game_season_package — 赛季包表
|
||||||
|
| 字段 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| id | 赛季包ID |
|
||||||
|
| level | 课程级别 (A1/A2/B1...) |
|
||||||
|
| season_of_quarter | 季度序号 |
|
||||||
|
| cn_name | 中文名称 |
|
||||||
|
|
||||||
|
## PostgreSQL 核心表
|
||||||
|
|
||||||
|
### vala.user_chapter_play_record_0~7 — 章节完课记录(分8表)
|
||||||
|
| 字段 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| user_id | 角色ID |
|
||||||
|
| chapter_id | 章节ID(关联 MySQL vala_game_chapter.id) |
|
||||||
|
| chapter_unique_id | 完课唯一标识 |
|
||||||
|
| level | 课程级别 (如 "A2") |
|
||||||
|
| play_status | 1=完成 |
|
||||||
|
| created_at | 创建时间 (tz-aware) |
|
||||||
|
| updated_at | 更新时间 (tz-aware) |
|
||||||
|
|
||||||
|
## 单元计算规则
|
||||||
|
|
||||||
|
Chapter → Level/Unit/Lesson 映射通过 `vala_game_chapter` + `vala_game_season_package` 计算:
|
||||||
|
|
||||||
|
- 每 5 个连续 lesson_type=1 的章节组成一个 Unit
|
||||||
|
- season_of_quarter=0 全部属 Unit 0
|
||||||
|
- season_of_quarter≥1 时基础 Unit = 1+12×(s-1),再叠加组内偏移
|
||||||
|
|
||||||
|
## 渠道编码映射
|
||||||
|
|
||||||
|
| 编码 | 渠道 | 编码 | 渠道 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 11 | 苹果 | 12 | 华为 |
|
||||||
|
| 13 | 小米 | 14 | 荣耀 |
|
||||||
|
| 15 | 应用宝 | 17 | 魅族 |
|
||||||
|
| 18 | VIVO | 19 | OPPO |
|
||||||
|
| 21 | 学而思 | 22 | 讯飞 |
|
||||||
|
| 23 | 步步高 | 24 | 作业帮 |
|
||||||
|
| 25 | 小度 | 26 | 希沃 |
|
||||||
|
| 27 | 京东方 | 41 | 官网 |
|
||||||
|
| 71 | 小程序 | 其他 | 站外 |
|
||||||
545
skills/user-info/scripts/query_user_info.py
Normal file
545
skills/user-info/scripts/query_user_info.py
Normal file
@ -0,0 +1,545 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
用户信息查询脚本 — user-info 技能核心引擎
|
||||||
|
查询指定用户(按账号ID/角色ID/手机号/订单号)的完整信息:
|
||||||
|
1. 解析到唯一 account_id
|
||||||
|
2. 账号基本信息(注册时间、最近活跃、角色数、订单数、设备、地域)
|
||||||
|
3. 全部订单(赛季通票)信息
|
||||||
|
4. 全部角色信息(含学习进度、活跃度)
|
||||||
|
5. 格式化输出
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import warnings
|
||||||
|
from datetime import datetime, timedelta, date
|
||||||
|
from collections import OrderedDict as OD
|
||||||
|
|
||||||
|
warnings.filterwarnings("ignore")
|
||||||
|
|
||||||
|
# ── 数据库连接配置 ──────────────────────────────────────────────
|
||||||
|
MYSQL_CONFIG = {
|
||||||
|
"host": "bj-cdb-dh2fkqa0.sql.tencentcdb.com",
|
||||||
|
"port": 27751,
|
||||||
|
"user": "read_only",
|
||||||
|
"password": "fsdo45ijfmfmuu77$%^&",
|
||||||
|
"charset": "utf8mb4",
|
||||||
|
}
|
||||||
|
|
||||||
|
PG_CONFIG = {
|
||||||
|
"host": "bj-postgres-16pob4sg.sql.tencentcdb.com",
|
||||||
|
"port": 28591,
|
||||||
|
"user": "ai_member",
|
||||||
|
"password": "LdfjdjL83h3h3^$&**YGG*",
|
||||||
|
"dbname": "vala",
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 映射表 ──────────────────────────────────────────────────────
|
||||||
|
CHANNEL_MAP = {
|
||||||
|
"11": "苹果", "12": "华为", "13": "小米", "14": "荣耀",
|
||||||
|
"15": "应用宝", "17": "魅族", "18": "VIVO", "19": "OPPO",
|
||||||
|
"21": "学而思", "22": "讯飞", "23": "步步高", "24": "作业帮",
|
||||||
|
"25": "小度", "26": "希沃", "27": "京东方",
|
||||||
|
"41": "官网", "71": "小程序",
|
||||||
|
}
|
||||||
|
|
||||||
|
GENDER_MAP = {"0": "女", "1": "男"}
|
||||||
|
TICKET_STATUS_MAP = {-1: "已过期", 0: "未使用", 1: "已使用"}
|
||||||
|
TICKET_TYPE_MAP = {1: "赛季通票", 2: "兑换卡"}
|
||||||
|
|
||||||
|
# ── 数据库连接 ──────────────────────────────────────────────────
|
||||||
|
def get_mysql_conn(db="vala_user"):
|
||||||
|
import pymysql
|
||||||
|
return pymysql.connect(
|
||||||
|
host=MYSQL_CONFIG["host"], port=MYSQL_CONFIG["port"],
|
||||||
|
user=MYSQL_CONFIG["user"], password=MYSQL_CONFIG["password"],
|
||||||
|
database=db, charset=MYSQL_CONFIG["charset"],
|
||||||
|
cursorclass=pymysql.cursors.DictCursor,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_pg_conn():
|
||||||
|
import psycopg2
|
||||||
|
return psycopg2.connect(
|
||||||
|
host=PG_CONFIG["host"], port=PG_CONFIG["port"],
|
||||||
|
user=PG_CONFIG["user"], password=PG_CONFIG["password"],
|
||||||
|
dbname=PG_CONFIG["dbname"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Chapter Info Map(复刻 studytime-analysis 逻辑)─────────────
|
||||||
|
_chapter_map_cache = None
|
||||||
|
|
||||||
|
def fetch_chapter_info_map():
|
||||||
|
"""建立 chapter_id → {level, unit_index, lesson_index, lesson_type} 映射
|
||||||
|
单元计算规则:每 5 个连续 lesson 章节(lesson_type=1)组成一个单元;
|
||||||
|
season_of_quarter=0 全部属 unit 0; s>=1 时 base=1+12*(s-1)
|
||||||
|
"""
|
||||||
|
global _chapter_map_cache
|
||||||
|
if _chapter_map_cache is not None:
|
||||||
|
return _chapter_map_cache
|
||||||
|
|
||||||
|
conn = get_mysql_conn("vala")
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("""
|
||||||
|
SELECT gc.id, IFNULL(sp.level,''), IFNULL(sp.season_of_quarter,-1),
|
||||||
|
gc.`index`, gc.lesson_type
|
||||||
|
FROM vala_game_chapter gc
|
||||||
|
LEFT JOIN vala_game_season_package sp ON gc.season_package_id = sp.id
|
||||||
|
ORDER BY sp.season_of_quarter, gc.id
|
||||||
|
""")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# 按 (level, season_of_quarter) 分组
|
||||||
|
seasons = OD()
|
||||||
|
for row in rows:
|
||||||
|
ch_id = int(row["id"])
|
||||||
|
lv = (row["IFNULL(sp.level,'')"] or "").strip()
|
||||||
|
sq = int(row["IFNULL(sp.season_of_quarter,-1)"] or -1)
|
||||||
|
li = int(row["index"] or 0)
|
||||||
|
lt = int(row["lesson_type"] or 1)
|
||||||
|
key = (lv, sq)
|
||||||
|
seasons.setdefault(key, []).append((ch_id, lv, li, lt))
|
||||||
|
|
||||||
|
def base(s):
|
||||||
|
return 0 if s <= 0 else 1 + 12 * (s - 1)
|
||||||
|
|
||||||
|
_chapter_map_cache = {}
|
||||||
|
for (lv_key, sq), ch_list in seasons.items():
|
||||||
|
regular = [(cid, lv, li) for cid, lv, li, lt in ch_list if lt == 1]
|
||||||
|
uid_map = {}
|
||||||
|
if sq <= 0:
|
||||||
|
for cid, lv, li in regular:
|
||||||
|
uid_map[cid] = 0
|
||||||
|
else:
|
||||||
|
for pos, (cid, lv, li) in enumerate(regular):
|
||||||
|
uid_map[cid] = pos // 5
|
||||||
|
for cid, lv, li, lt in ch_list:
|
||||||
|
u = uid_map.get(cid, -1)
|
||||||
|
_chapter_map_cache[cid] = dict(
|
||||||
|
level=lv, unit_index=base(sq) + u if u >= 0 else -1,
|
||||||
|
lesson_index=li, lesson_type=lt)
|
||||||
|
return _chapter_map_cache
|
||||||
|
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════
|
||||||
|
# 第1步:解析 account_id
|
||||||
|
# ══════════════════════════════════════════════════════════════════
|
||||||
|
def resolve_account_id(mysql, account_id=None, role_id=None, phone=None, order_id=None):
|
||||||
|
"""从任意输入定位到唯一的 account_id"""
|
||||||
|
given = [x for x in [account_id, role_id, phone, order_id] if x is not None]
|
||||||
|
if len(given) != 1:
|
||||||
|
print("错误:请指定 --account-id、--role-id、--phone、--order-id 中的恰好一个", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
aid = None
|
||||||
|
with mysql.cursor() as cur:
|
||||||
|
if account_id is not None:
|
||||||
|
cur.execute(
|
||||||
|
"SELECT id FROM vala_app_account WHERE id = %s AND status = 1 AND deleted_at IS NULL",
|
||||||
|
(account_id,))
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row:
|
||||||
|
aid = row["id"]
|
||||||
|
else:
|
||||||
|
print(f"错误:未找到账号 ID={account_id},可能已删除或不存在", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
elif role_id is not None:
|
||||||
|
cur.execute(
|
||||||
|
"SELECT account_id FROM vala_app_character WHERE id = %s AND deleted_at IS NULL",
|
||||||
|
(role_id,))
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row:
|
||||||
|
aid = row["account_id"]
|
||||||
|
else:
|
||||||
|
print(f"错误:未找到角色 ID={role_id},可能已删除或不存在", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
elif phone is not None:
|
||||||
|
phone = str(phone).strip()
|
||||||
|
if len(phone) < 7:
|
||||||
|
print(f"错误:手机号太短,请输入完整号码", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
prefix = phone[:3]
|
||||||
|
suffix = phone[-4:]
|
||||||
|
cur.execute(
|
||||||
|
"SELECT id, tel FROM vala_app_account "
|
||||||
|
"WHERE tel LIKE %s AND status = 1 AND deleted_at IS NULL",
|
||||||
|
(f"{prefix}%{suffix}",))
|
||||||
|
rows = cur.fetchall()
|
||||||
|
if len(rows) == 0:
|
||||||
|
print(f"错误:未找到手机号 {phone} 对应的有效账号", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif len(rows) == 1:
|
||||||
|
aid = rows[0]["id"]
|
||||||
|
else:
|
||||||
|
ids = [str(r["id"]) for r in rows]
|
||||||
|
print(f"错误:手机号 {phone} 匹配到 {len(rows)} 个账号 (ID: {', '.join(ids)}),请使用账号ID查询", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
elif order_id is not None:
|
||||||
|
order_mysql = get_mysql_conn("vala_order")
|
||||||
|
try:
|
||||||
|
with order_mysql.cursor() as oc:
|
||||||
|
oc.execute(
|
||||||
|
"SELECT account_id FROM vala_seasonal_ticket WHERE id = %s",
|
||||||
|
(order_id,))
|
||||||
|
row = oc.fetchone()
|
||||||
|
if row:
|
||||||
|
tmp_aid = row["account_id"]
|
||||||
|
# 验证账号有效性
|
||||||
|
cur.execute(
|
||||||
|
"SELECT id FROM vala_app_account WHERE id = %s AND status = 1 AND deleted_at IS NULL",
|
||||||
|
(tmp_aid,))
|
||||||
|
if cur.fetchone():
|
||||||
|
aid = tmp_aid
|
||||||
|
else:
|
||||||
|
print(f"错误:订单 ID={order_id} 对应的账号 {tmp_aid} 已失效或不存在", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print(f"错误:未找到订单 ID={order_id}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
finally:
|
||||||
|
order_mysql.close()
|
||||||
|
|
||||||
|
return aid
|
||||||
|
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════
|
||||||
|
# 第2步:账号基本信息
|
||||||
|
# ══════════════════════════════════════════════════════════════════
|
||||||
|
def fetch_account_basic_info(mysql, account_id):
|
||||||
|
info = {}
|
||||||
|
with mysql.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"SELECT id, tel, created_at, updated_at, login_times, key_from, download_channel "
|
||||||
|
"FROM vala_app_account WHERE id = %s", (account_id,))
|
||||||
|
acct = cur.fetchone()
|
||||||
|
if not acct:
|
||||||
|
return None
|
||||||
|
|
||||||
|
info["account_id"] = acct["id"]
|
||||||
|
info["tel"] = acct.get("tel") or "未知"
|
||||||
|
info["register_time"] = str(acct["created_at"]) if acct["created_at"] else "未知"
|
||||||
|
info["last_active"] = str(acct["updated_at"]) if acct["updated_at"] else "未知"
|
||||||
|
info["login_times"] = acct.get("login_times") or 0
|
||||||
|
info["key_from"] = acct.get("key_from") or "未知"
|
||||||
|
raw_ch = str(acct.get("download_channel") or "")
|
||||||
|
info["download_channel"] = CHANNEL_MAP.get(raw_ch, raw_ch) if raw_ch else "未知"
|
||||||
|
|
||||||
|
# 角色数
|
||||||
|
cur.execute(
|
||||||
|
"SELECT COUNT(*) AS cnt FROM vala_app_character "
|
||||||
|
"WHERE account_id = %s AND deleted_at IS NULL", (account_id,))
|
||||||
|
info["role_count"] = cur.fetchone()["cnt"]
|
||||||
|
|
||||||
|
# 订单数(在 vala_order 库)
|
||||||
|
order_mysql = get_mysql_conn("vala_order")
|
||||||
|
try:
|
||||||
|
with order_mysql.cursor() as oc:
|
||||||
|
oc.execute(
|
||||||
|
"SELECT COUNT(*) AS cnt FROM vala_seasonal_ticket WHERE account_id = %s",
|
||||||
|
(account_id,))
|
||||||
|
info["ticket_count"] = oc.fetchone()["cnt"]
|
||||||
|
finally:
|
||||||
|
order_mysql.close()
|
||||||
|
|
||||||
|
# 设备和地域:线上数据库无 account_login / account_detail_info 表
|
||||||
|
info["recent_devices"] = []
|
||||||
|
info["region"] = "暂无数据(线上无地域详情表)"
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════
|
||||||
|
# 第3步:全部订单(赛季通票)信息
|
||||||
|
# ══════════════════════════════════════════════════════════════════
|
||||||
|
def fetch_tickets(account_id):
|
||||||
|
order_mysql = get_mysql_conn("vala_order")
|
||||||
|
try:
|
||||||
|
with order_mysql.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"SELECT id, season_package_name, ticket_type, status, "
|
||||||
|
"give_time, expire_time, used_time, created_at "
|
||||||
|
"FROM vala_seasonal_ticket "
|
||||||
|
"WHERE account_id = %s ORDER BY created_at DESC", (account_id,))
|
||||||
|
rows = cur.fetchall()
|
||||||
|
finally:
|
||||||
|
order_mysql.close()
|
||||||
|
|
||||||
|
tickets = []
|
||||||
|
for r in rows:
|
||||||
|
tickets.append({
|
||||||
|
"id": r["id"],
|
||||||
|
"name": r["season_package_name"] or "未知套餐",
|
||||||
|
"ticket_type": TICKET_TYPE_MAP.get(r["ticket_type"], f"类型{r['ticket_type']}"),
|
||||||
|
"status": TICKET_STATUS_MAP.get(r["status"], f"状态{r['status']}"),
|
||||||
|
"give_time": _ts_to_str(r["give_time"]),
|
||||||
|
"expire_time": _ts_to_str(r["expire_time"]),
|
||||||
|
"used_time": _ts_to_str(r["used_time"]),
|
||||||
|
"created_at": str(r["created_at"]) if r.get("created_at") else "未知",
|
||||||
|
})
|
||||||
|
return tickets
|
||||||
|
|
||||||
|
|
||||||
|
def _ts_to_str(ts):
|
||||||
|
if not ts or ts == 0:
|
||||||
|
return "未知"
|
||||||
|
try:
|
||||||
|
return datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
except Exception:
|
||||||
|
return str(ts)
|
||||||
|
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════
|
||||||
|
# 第4步:全部角色信息(含学习进度、活跃度)
|
||||||
|
# ══════════════════════════════════════════════════════════════════
|
||||||
|
def fetch_characters(mysql, account_id, cmap):
|
||||||
|
with mysql.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"SELECT id, nickname, gender, birthday, latest_login, created_at "
|
||||||
|
"FROM vala_app_character "
|
||||||
|
"WHERE account_id = %s AND deleted_at IS NULL ORDER BY id", (account_id,))
|
||||||
|
chars = cur.fetchall()
|
||||||
|
|
||||||
|
if not chars:
|
||||||
|
return []
|
||||||
|
|
||||||
|
from psycopg2.tz import FixedOffsetTimezone
|
||||||
|
now = datetime.now()
|
||||||
|
today = now.date()
|
||||||
|
cutoff_14d = now - timedelta(days=14)
|
||||||
|
# PG 时区为 Asia/Shanghai (UTC+8)
|
||||||
|
cutoff_14d_aware = cutoff_14d.replace(tzinfo=FixedOffsetTimezone(offset=8*60, name="CST"))
|
||||||
|
|
||||||
|
pg = get_pg_conn()
|
||||||
|
try:
|
||||||
|
with pg.cursor() as cur:
|
||||||
|
result = []
|
||||||
|
for ch in chars:
|
||||||
|
ch_id = ch["id"]
|
||||||
|
|
||||||
|
# ── 汇总 8 张分表的所有完课记录 ──
|
||||||
|
last_completion = None
|
||||||
|
last_chapter_id = None
|
||||||
|
last_chapter_level = None
|
||||||
|
recent_count = 0
|
||||||
|
total_count = 0
|
||||||
|
|
||||||
|
# 构造 UNION ALL 查询,参数需要不同名称
|
||||||
|
union_sql = " UNION ALL ".join(
|
||||||
|
[f"SELECT user_id, chapter_id, level, created_at, updated_at "
|
||||||
|
f"FROM user_chapter_play_record_{i} "
|
||||||
|
f"WHERE user_id = %(uid_{i})s AND play_status = 1"
|
||||||
|
for i in range(8)])
|
||||||
|
params = {}
|
||||||
|
for i in range(8):
|
||||||
|
params[f"uid_{i}"] = ch_id
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
f"SELECT chapter_id, level, created_at, updated_at FROM ({union_sql}) sub "
|
||||||
|
f"ORDER BY updated_at DESC", params)
|
||||||
|
rows = cur.fetchall()
|
||||||
|
|
||||||
|
for r in rows:
|
||||||
|
dt = r[2] or r[3] # created_at or updated_at
|
||||||
|
if dt:
|
||||||
|
total_count += 1
|
||||||
|
dt_naive = dt.replace(tzinfo=None) if dt.tzinfo else dt
|
||||||
|
if last_completion is None or dt_naive > last_completion:
|
||||||
|
last_completion = dt_naive
|
||||||
|
last_chapter_id = r[0]
|
||||||
|
last_chapter_level = r[1]
|
||||||
|
if dt_naive >= cutoff_14d:
|
||||||
|
recent_count += 1
|
||||||
|
|
||||||
|
# ── 当前课程位置 ──
|
||||||
|
course_info = "-"
|
||||||
|
if last_chapter_id and cmap:
|
||||||
|
ci = cmap.get(last_chapter_id)
|
||||||
|
if ci:
|
||||||
|
lv = ci.get("level") or last_chapter_level or ""
|
||||||
|
ui = ci.get("unit_index", -1)
|
||||||
|
lsn = ci.get("lesson_index", -1)
|
||||||
|
parts = []
|
||||||
|
if lv:
|
||||||
|
parts.append(f"Level {lv}")
|
||||||
|
if ui >= 0:
|
||||||
|
parts.append(f"Unit {ui}")
|
||||||
|
if lsn >= 0:
|
||||||
|
parts.append(f"Lesson {lsn}")
|
||||||
|
course_info = " > ".join(parts) if parts else str(last_chapter_id)
|
||||||
|
elif last_chapter_level:
|
||||||
|
course_info = f"Level {last_chapter_level} (Chapter {last_chapter_id})"
|
||||||
|
else:
|
||||||
|
course_info = f"Chapter {last_chapter_id}"
|
||||||
|
elif last_chapter_level:
|
||||||
|
course_info = f"Level {last_chapter_level}"
|
||||||
|
|
||||||
|
# ── 年龄 ──
|
||||||
|
age = "未知"
|
||||||
|
birthday = ch.get("birthday")
|
||||||
|
if birthday and str(birthday).strip():
|
||||||
|
try:
|
||||||
|
bd_str = str(birthday).strip()[:10]
|
||||||
|
bd = datetime.strptime(bd_str, "%Y-%m-%d").date()
|
||||||
|
age = str(today.year - bd.year - ((today.month, today.day) < (bd.month, bd.day)))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ── 活跃度判定 ──
|
||||||
|
if total_count == 0:
|
||||||
|
activity = "无完课记录"
|
||||||
|
elif last_completion and last_completion < cutoff_14d:
|
||||||
|
activity = "流失"
|
||||||
|
elif recent_count > 10:
|
||||||
|
activity = "高活跃"
|
||||||
|
elif recent_count < 5:
|
||||||
|
activity = "低活跃"
|
||||||
|
else:
|
||||||
|
activity = "正常活跃"
|
||||||
|
|
||||||
|
result.append({
|
||||||
|
"id": ch_id,
|
||||||
|
"nickname": ch.get("nickname") or "未命名",
|
||||||
|
"gender": GENDER_MAP.get(str(ch.get("gender")), "未知"),
|
||||||
|
"age": age,
|
||||||
|
"birthday": birthday or "未知",
|
||||||
|
"course": course_info,
|
||||||
|
"last_completion": last_completion.strftime("%Y-%m-%d %H:%M:%S") if last_completion else "无记录",
|
||||||
|
"total_completions": total_count,
|
||||||
|
"recent_14d": recent_count,
|
||||||
|
"activity": activity,
|
||||||
|
"latest_login": str(ch["latest_login"]) if ch.get("latest_login") else "未知",
|
||||||
|
"created_at": str(ch["created_at"]) if ch.get("created_at") else "未知",
|
||||||
|
})
|
||||||
|
finally:
|
||||||
|
pg.close()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════
|
||||||
|
# 第5步:格式化输出
|
||||||
|
# ══════════════════════════════════════════════════════════════════
|
||||||
|
def format_output_text(account_id, basics, tickets, characters):
|
||||||
|
lines = []
|
||||||
|
lines.append("=" * 70)
|
||||||
|
lines.append(f" 用户信息查询结果")
|
||||||
|
lines.append("=" * 70)
|
||||||
|
|
||||||
|
if basics is None:
|
||||||
|
lines.append("未查询到该账号信息")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
# ── 账号基本信息 ──
|
||||||
|
lines.append("")
|
||||||
|
lines.append("📋 账号基本信息")
|
||||||
|
lines.append(f" 账号ID: {basics['account_id']}")
|
||||||
|
lines.append(f" 手机号: {basics['tel']}")
|
||||||
|
lines.append(f" 注册时间: {basics['register_time']}")
|
||||||
|
lines.append(f" 最近活跃: {basics['last_active']}")
|
||||||
|
lines.append(f" 登录次数: {basics['login_times']}")
|
||||||
|
lines.append(f" 注册来源: {basics['key_from']}")
|
||||||
|
lines.append(f" 下载渠道: {basics['download_channel']}")
|
||||||
|
lines.append(f" 角色数量: {basics['role_count']}")
|
||||||
|
lines.append(f" 赛季通票数: {basics['ticket_count']}")
|
||||||
|
lines.append(f" 所属地区: {basics['region']}")
|
||||||
|
|
||||||
|
if basics["recent_devices"]:
|
||||||
|
lines.append("")
|
||||||
|
lines.append("📱 最近登录设备 (最近10次):")
|
||||||
|
for i, dev in enumerate(basics["recent_devices"], 1):
|
||||||
|
lines.append(f" {i}. {dev}")
|
||||||
|
else:
|
||||||
|
lines.append(f" 登录设备: 暂无数据")
|
||||||
|
|
||||||
|
# ── 订单(通票)信息 ──
|
||||||
|
lines.append("")
|
||||||
|
lines.append("─" * 70)
|
||||||
|
lines.append(f"🎫 赛季通票 (共 {len(tickets)} 张)")
|
||||||
|
lines.append("─" * 70)
|
||||||
|
if tickets:
|
||||||
|
for i, t in enumerate(tickets, 1):
|
||||||
|
lines.append(f"\n [{i}] {t['name']}")
|
||||||
|
lines.append(f" 类型: {t['ticket_type']} | 状态: {t['status']}")
|
||||||
|
lines.append(f" 发放时间: {t['give_time']}")
|
||||||
|
lines.append(f" 过期时间: {t['expire_time']}")
|
||||||
|
lines.append(f" 使用时间: {t['used_time']}")
|
||||||
|
lines.append(f" 创建时间: {t['created_at']}")
|
||||||
|
else:
|
||||||
|
lines.append(" (无记录)")
|
||||||
|
|
||||||
|
# ── 角色信息 ──
|
||||||
|
lines.append("")
|
||||||
|
lines.append("─" * 70)
|
||||||
|
lines.append(f"🎮 角色信息 (共 {len(characters)} 个)")
|
||||||
|
lines.append("─" * 70)
|
||||||
|
if characters:
|
||||||
|
for i, c in enumerate(characters, 1):
|
||||||
|
lines.append(f"\n [{i}] {c['nickname']} (角色ID: {c['id']})")
|
||||||
|
lines.append(f" 性别: {c['gender']} | 年龄: {c['age']} 岁 | 生日: {c['birthday']}")
|
||||||
|
lines.append(f" 当前课程: {c['course']}")
|
||||||
|
lines.append(f" 上次完课: {c['last_completion']}")
|
||||||
|
lines.append(f" 累计完课: {c['total_completions']} 节 | 近14天: {c['recent_14d']} 节")
|
||||||
|
lines.append(f" 活跃程度: {c['activity']}")
|
||||||
|
lines.append(f" 最后登录: {c['latest_login']}")
|
||||||
|
else:
|
||||||
|
lines.append(" (无角色)")
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
lines.append("=" * 70)
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════
|
||||||
|
# 主流程
|
||||||
|
# ══════════════════════════════════════════════════════════════════
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="查询用户完整信息")
|
||||||
|
parser.add_argument("--account-id", type=int, help="账号ID")
|
||||||
|
parser.add_argument("--role-id", type=int, help="角色ID")
|
||||||
|
parser.add_argument("--phone", type=str, help="手机号")
|
||||||
|
parser.add_argument("--order-id", type=int, help="订单(通票)ID")
|
||||||
|
parser.add_argument("--json", action="store_true", help="输出JSON格式")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# 第1步:解析 account_id
|
||||||
|
mysql = get_mysql_conn("vala_user")
|
||||||
|
try:
|
||||||
|
account_id = resolve_account_id(mysql, args.account_id, args.role_id, args.phone, args.order_id)
|
||||||
|
|
||||||
|
# 第2步:账号基本信息
|
||||||
|
basics = fetch_account_basic_info(mysql, account_id)
|
||||||
|
|
||||||
|
# 第3步:订单(通票)信息
|
||||||
|
tickets = fetch_tickets(account_id)
|
||||||
|
|
||||||
|
# 第4步:角色信息(含学习进度)
|
||||||
|
cmap = fetch_chapter_info_map()
|
||||||
|
characters = fetch_characters(mysql, account_id, cmap)
|
||||||
|
finally:
|
||||||
|
mysql.close()
|
||||||
|
|
||||||
|
# 第5步:输出
|
||||||
|
if args.json:
|
||||||
|
result = {
|
||||||
|
"account_basics": basics,
|
||||||
|
"tickets": tickets,
|
||||||
|
"characters": characters,
|
||||||
|
}
|
||||||
|
class DateTimeEncoder(json.JSONEncoder):
|
||||||
|
def default(self, obj):
|
||||||
|
if isinstance(obj, (datetime, date)):
|
||||||
|
return str(obj)
|
||||||
|
return super().default(obj)
|
||||||
|
print(json.dumps(result, ensure_ascii=False, indent=2, cls=DateTimeEncoder))
|
||||||
|
else:
|
||||||
|
print(format_output_text(account_id, basics, tickets, characters))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue
Block a user