Files
banban/mini-program/design.md

484 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Banban 统一数据与接口设计文档Mini Program + talkingq-urlV4
## 1. 目标与边界
本设计用于统一 `mini-program``talkingq-url` 的数据库模型与接口语义,目标是:
1. 两个服务使用同一套设备身份(`device_id`)。
2. 保留 `talkingq-url/mysql/init/01-init.sql` 现有表结构不变。
3. 在不改动基础表的前提下,补齐家长/小孩/绑定/聊天/定位能力。
4. 家长账号体系统一采用微信小程序登录,服务端基于 `code2Session` 换取真实 `openid/unionid`
硬性边界:
1. `01-init.sql` 中现有表不改表名、不改字段、不改约束。
2. 所有新增表仅做增量扩展。
3. 新增表字段命名风格对齐现有风格:`snake_case``*_id``is_active``status``created_at``updated_at`
4. 微信 `AppSecret` 只能保存在服务端配置中,小程序端只持有一次性 `code`
## 2. 现有冻结表(来自 01-init.sql
以下表为设备能力基础层,保持不变:
1. `device_auth`:设备身份白名单(`device_id + serial_number`)。
2. `device_configs`:设备角色与基础配置(含 `volume`)。
3. `conversation_histories`:设备与角色会话头。
4. `conversation_messages`:设备与角色会话消息。
5. `roles` / `role_languages`:角色与多语言配置。
6. `device_firmware_update`:设备固件升级状态。
7. `system_config`:系统配置项。
职责说明:
1. `conversation_histories/conversation_messages` 继续只服务“设备 <-> AI 角色”的对话,不承担家长-小孩 IM。
2. 设备鉴权统一走 `device_auth``device_id` 为全局设备业务标识。
## 3. 新增主体模型
### 3.1 家长主体表 `parents`
建议字段:
1. `user_id` BIGINT UNSIGNED PK AUTO_INCREMENT
2. `openid` VARCHAR(64) UNIQUE NOT NULL
3. `unionid` VARCHAR(64) NULL
4. `nickname` VARCHAR(64) NULL
5. `avatar_url` VARCHAR(255) NULL
6. `avatar_file_key` VARCHAR(255) NULL
7. `phone` VARCHAR(20) NULL
8. `status` TINYINT NOT NULL DEFAULT 1
9. `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
10. `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
说明:
1. `avatar_file_key` 作为头像主存储值,保存 COS 对象 key。
2. `avatar_url` 作为兼容或展示字段使用,不作为唯一主依据。
3. `openid` 必须存储微信服务端 `code2Session` 返回的真实 OpenID禁止将前端 `code`、测试用户名或设备标识写入该字段。
4. `unionid` 仅在微信返回时写入;历史记录为空时,后续登录可补写,不作为首次登录成功前置条件。
5. `nickname/avatar_url` 为展示资料,可由小程序端在登录时上传并在后续更新,但不参与身份认证。
6. `phone` 为业务补充字段,与微信登录态解耦;如后续接入手机号授权,应基于服务端掌握的 `session_key` 解密后再写入。
索引建议:
1. `uk_openid(openid)`
2. `idx_unionid(unionid)`
3. `idx_phone(phone)`
### 3.2 小孩主体表 `children`
建议字段:
1. `child_id` BIGINT UNSIGNED PK AUTO_INCREMENT
2. `child_name` VARCHAR(32) NOT NULL
3. `child_gender` TINYINT NOT NULL DEFAULT 2
4. `child_birthday` DATE NULL
5. `status` TINYINT NOT NULL DEFAULT 1
6. `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
7. `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
说明:
1. 不在库内存储 `child_age` 计算列,年龄由查询层按时间实时计算,避免跨年缓存误差。
### 3.3 家长-小孩关系表 `parent_child_relations`
建议字段:
1. `id` BIGINT UNSIGNED PK AUTO_INCREMENT
2. `user_id` BIGINT UNSIGNED NOT NULL
3. `child_id` BIGINT UNSIGNED NOT NULL
4. `relation_type` TINYINT NOT NULL DEFAULT 9
5. `is_primary` TINYINT(1) NOT NULL DEFAULT 0
6. `status` TINYINT NOT NULL DEFAULT 1
7. `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
8. `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
约束建议:
1. `UNIQUE(user_id, child_id)`
2. `FOREIGN KEY (user_id) REFERENCES parents(user_id)`
3. `FOREIGN KEY (child_id) REFERENCES children(child_id)`
### 3.4 设备归属表 `device_bindings`
用途:表达“当前哪块设备归属到哪个家长,并可选关联到哪个小孩”,并与 `device_auth` 对齐。
建议字段:
1. `id` BIGINT UNSIGNED PK AUTO_INCREMENT
2. `device_id` VARCHAR(64) NOT NULL
3. `owner_user_id` BIGINT UNSIGNED NOT NULL
4. `child_id` BIGINT UNSIGNED NULL
5. `status` TINYINT NOT NULL DEFAULT 1
6. `bound_at` DATETIME NOT NULL
7. `unbound_at` DATETIME NULL
8. `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
9. `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
说明:
1. 设备可以先归属到家长,再在后续流程中补充关联 `child_id`
2. `child_id` 为空表示当前设备已归属,但尚未绑定到具体小孩。
约束建议:
1. `UNIQUE(device_id)`
2. `UNIQUE(child_id)`
3. `INDEX idx_owner_user_id(owner_user_id)`
4. `FOREIGN KEY (device_id) REFERENCES device_auth(device_id)`
5. `FOREIGN KEY (owner_user_id) REFERENCES parents(user_id)`
6. `FOREIGN KEY (child_id) REFERENCES children(child_id)`
### 3.5 绑定会话表 `device_bind_sessions`
建议字段:
1. `id` BIGINT UNSIGNED PK AUTO_INCREMENT
2. `bind_token` CHAR(36) UNIQUE NOT NULL
3. `device_id` VARCHAR(64) NOT NULL
4. `initiator_user_id` BIGINT UNSIGNED NOT NULL
5. `target_child_id` BIGINT UNSIGNED NULL
6. `challenge_code_hash` CHAR(64) NULL
7. `challenge_set_at` DATETIME NULL
8. `expires_at` DATETIME NOT NULL
9. `max_attempt_count` TINYINT UNSIGNED NOT NULL DEFAULT 5
10. `attempt_count` TINYINT UNSIGNED NOT NULL DEFAULT 0
11. `status` TINYINT NOT NULL DEFAULT 1
12. `confirmed_at` DATETIME NULL
13. `consumed_at` DATETIME NULL
14. `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
15. `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
约束建议:
1. `FOREIGN KEY (device_id) REFERENCES device_auth(device_id)`
2. `FOREIGN KEY (initiator_user_id) REFERENCES parents(user_id)`
3. `FOREIGN KEY (target_child_id) REFERENCES children(child_id)`
### 3.6 绑定历史表 `device_bind_history`
建议字段:
1. `id` BIGINT UNSIGNED PK AUTO_INCREMENT
2. `device_id` VARCHAR(64) NOT NULL
3. `child_id` BIGINT UNSIGNED NOT NULL
4. `bound_by_user_id` BIGINT UNSIGNED NOT NULL
5. `unbound_by_user_id` BIGINT UNSIGNED NULL
6. `bind_source` TINYINT NOT NULL DEFAULT 1
7. `bound_at` DATETIME NOT NULL
8. `unbound_at` DATETIME NULL
9. `unbind_reason` VARCHAR(191) NULL
10. `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
约束建议:
1. `FOREIGN KEY (device_id) REFERENCES device_auth(device_id)`
2. `FOREIGN KEY (child_id) REFERENCES children(child_id)`
3. `FOREIGN KEY (bound_by_user_id) REFERENCES parents(user_id)`
4. `FOREIGN KEY (unbound_by_user_id) REFERENCES parents(user_id)`
### 3.7 卡片表 `cards`
建议字段:
1. `card_id` BIGINT UNSIGNED PK AUTO_INCREMENT
2. `card_uuid` VARCHAR(64) UNIQUE NOT NULL
3. `device_id` VARCHAR(64) UNIQUE NULL
4. `card_name` VARCHAR(64) NULL
5. `status` TINYINT NOT NULL DEFAULT 0
6. `total_swaps` INT NOT NULL DEFAULT 0
7. `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
8. `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
约束建议:
1. `FOREIGN KEY (device_id) REFERENCES device_auth(device_id)`
### 3.8 设备设置表 `device_settings`
建议字段:
1. `setting_id` BIGINT UNSIGNED PK AUTO_INCREMENT
2. `device_id` VARCHAR(64) NOT NULL UNIQUE
3. `sleep_mode` TINYINT NOT NULL DEFAULT 0
4. `disable_time_start` TIME NULL
5. `disable_time_end` TIME NULL
6. `timezone` VARCHAR(32) NOT NULL DEFAULT 'Asia/Shanghai'
7. `volume` TINYINT UNSIGNED NULL
8. `brightness` TINYINT UNSIGNED NULL
9. `disable_weekdays` VARCHAR(32) NULL
10. `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
11. `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
兼容规则:
1. `volume` 同步写入 `device_configs.volume`,防止两服务读到不同值。
2. 若只允许单一音量来源,则以 `device_configs.volume` 为准,`device_settings.volume` 可后续下线。
3. `disable_time_start/disable_time_end` 与历史命名 `sleep_start/sleep_end` 一一映射。
4. 禁用时段按 `timezone` 解释;跨天时段(如 22:00-07:00按“跨日窗口”处理。
## 4. 聊天与定位模型(新增,不改 01 现有会话表)
### 4.1 IM 会话表 `im_conversations`
用途:仅服务家长-小孩、小孩-小孩 1v1不与 `conversation_histories` 混用。
建议字段:
1. `id` BIGINT UNSIGNED PK AUTO_INCREMENT
2. `conversation_type` TINYINT UNSIGNED NOT NULL
3. `participant_a_type` TINYINT UNSIGNED NOT NULL
4. `participant_a_id` VARCHAR(64) NOT NULL
5. `participant_b_type` TINYINT UNSIGNED NOT NULL
6. `participant_b_id` VARCHAR(64) NOT NULL
7. `pair_key` VARCHAR(191) NOT NULL
8. `status` TINYINT NOT NULL DEFAULT 1
9. `last_seq` BIGINT UNSIGNED NOT NULL DEFAULT 0
10. `message_count` BIGINT UNSIGNED NOT NULL DEFAULT 0
11. `last_message_preview` VARCHAR(255) NULL
12. `last_message_at` DATETIME NULL
13. `ext_json` JSON NULL
14. `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
15. `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
说明:
1. 会话核心字段保持通用,不使用 `child_low_id/child_high_id` 这类场景耦合命名。
2. `conversation_type` 使用类型码,可扩展;新增会话类型优先通过“类型码 + 业务校验 + `ext_json`”扩展,无需改核心表。
3. 当前建议启用类型:`1-child_peer``2-parent_child`
约束建议:
1. `UNIQUE(conversation_type, pair_key)`
2. `INDEX idx_participant_a (participant_a_type, participant_a_id)`
3. `INDEX idx_participant_b (participant_b_type, participant_b_id)`
4. `INDEX idx_last_message_at (last_message_at)`
### 4.2 IM 消息表 `im_messages`
建议字段:
1. `id` BIGINT UNSIGNED PK AUTO_INCREMENT
2. `conversation_id` BIGINT UNSIGNED NOT NULL
3. `seq` BIGINT UNSIGNED NOT NULL
4. `sender_type` TINYINT UNSIGNED NOT NULL
5. `sender_id` VARCHAR(64) NOT NULL
6. `receiver_type` TINYINT UNSIGNED NOT NULL
7. `receiver_id` VARCHAR(64) NOT NULL
8. `content_type` TINYINT UNSIGNED NOT NULL
9. `content_text` MEDIUMTEXT NULL
10. `content_json` JSON NULL
11. `media_file_key` VARCHAR(255) NULL
12. `media_duration_ms` INT UNSIGNED NULL
13. `media_mime_type` VARCHAR(64) NULL
14. `media_size_bytes` BIGINT UNSIGNED NULL
15. `media_transcript_text` TEXT NULL
16. `client_msg_id` VARCHAR(64) NULL
17. `sender_name_snapshot` VARCHAR(64) NULL
18. `sender_avatar_snapshot` VARCHAR(255) NULL
19. `receiver_name_snapshot` VARCHAR(64) NULL
20. `receiver_avatar_snapshot` VARCHAR(255) NULL
21. `ext_json` JSON NULL
22. `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
23. `deleted_at` DATETIME NULL
约束建议:
1. `UNIQUE(conversation_id, seq)`
2. `UNIQUE(conversation_id, client_msg_id)`
3. `FOREIGN KEY (conversation_id) REFERENCES im_conversations(id) ON DELETE CASCADE`
4. `INDEX idx_sender (sender_type, sender_id, created_at)`
5. `INDEX idx_receiver (receiver_type, receiver_id, created_at)`
#### 4.2.1 文本与语音消息规则
1. `content_type=1(text)``content_text` 必填。
2. `content_type=2(audio)``media_file_key` 必填,建议同时写入 `media_duration_ms``media_mime_type``media_size_bytes`
3. `content_type=3(image)``media_file_key` 必填。
4. `content_type=4(json)``content_json` 必填。
5. 语音转写内容可写入 `media_transcript_text`,用于检索与展示。
6. 展示层优先使用 `sender_name_snapshot/sender_avatar_snapshot``receiver_*_snapshot`,减少联表查询。
### 4.3 位置表
`child_location_current`
1. `child_id` BIGINT UNSIGNED PK
2. `device_id` VARCHAR(64) NOT NULL
3. `coord_type` VARCHAR(16) NOT NULL DEFAULT 'gcj02'
4. `lat` DECIMAL(10,7) NOT NULL
5. `lng` DECIMAL(10,7) NOT NULL
6. `accuracy_m` INT UNSIGNED NULL
7. `altitude_m` DECIMAL(8,2) NULL
8. `speed_mps` DECIMAL(8,2) NULL
9. `heading_deg` SMALLINT UNSIGNED NULL
10. `source` TINYINT UNSIGNED NOT NULL
11. `battery_pct` TINYINT UNSIGNED NULL
12. `device_time` DATETIME NOT NULL
13. `server_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
14. `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
`child_location_history`
1. `id` BIGINT UNSIGNED PK AUTO_INCREMENT
2. `child_id` BIGINT UNSIGNED NOT NULL
3. `device_id` VARCHAR(64) NOT NULL
4. `coord_type` VARCHAR(16) NOT NULL DEFAULT 'gcj02'
5. 其余定位字段与 `child_location_current` 对齐
6. `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
#### 4.3.1 位置上报协议(统一格式)
1. 上报字段:`device_id``child_id``coord_type``lat``lng``accuracy_m``source``device_time``battery_pct`
2. 坐标系标准:统一使用 `gcj02`
3. 坐标格式:统一按 `lat,lng`(纬度在前,经度在后)传输和存储。
4. 若设备侧提供 `wgs84`,服务端需先转换到 `gcj02` 后再入库;原始坐标可选写入 `ext_json` 备查。
5. 写入规则:同事务写 `child_location_history` 并覆盖 `child_location_current`
## 5. 核心业务流程
### 5.1 家长登录流程(微信小程序)
1. 小程序调用 `wx.login` / `Taro.login` 获取一次性 `code`
2. 小程序调用后端 `POST /auth/login`,上传 `code`,并可附带 `nickname/avatar_url` 作为资料字段。
3. 后端使用服务端配置的 `wechat_app_id + wechat_app_secret` 调用微信 `code2Session`
4. 后端从微信响应中获取真实 `openid`、可选 `unionid`,以及仅供服务端使用的 `session_key`
5. 后端以 `openid` 为唯一身份键 upsert `parents`;若本次拿到 `unionid` 且库中为空,则补写 `unionid`
6. 后端签发本地 `access_token(JWT)` 返回给小程序,后续业务接口仍统一使用本地 Bearer Token。
7. 若微信换码失败、`openid` 缺失、`code` 失效或被重复使用,登录直接失败,不创建本地用户。
8. `session_key` 仅用于后续需要的微信敏感数据解密能力(如手机号);当前阶段不作为登录态主键,也不写入 `parents` 主表。
### 5.2 设备注册与鉴权
1. 生产或后台调用 `talkingq-url` 现有注册接口,写入 `device_auth`
2. 手表请求携带 `X-Device-ID``X-Device-Serial`
3. 后端基于 `device_auth` 鉴权通过后继续处理聊天/定位/绑定。
### 5.3 绑定流程(扫码 + 确认码)
1. 家长登录后发起 `bind/start`,创建 `device_bind_sessions`
2. 手表上报确认码,更新会话挑战信息。
3. 家长提交确认码。
4. 后端校验通过后开启事务。
5. upsert `parent_child_relations`
6. upsert `device_bindings`(保证一设备一行、一小孩一行)。
7. 追加 `device_bind_history`
8. 更新会话状态为 `confirmed/consumed` 并提交事务。
### 5.4 聊天流程
1. 设备 AI 对话仍走 `conversation_histories + conversation_messages`
2. 家长-小孩、小孩-小孩消息走 `im_conversations + im_messages`
3. IM 消息统一使用 `sender_type/sender_id + receiver_type/receiver_id`,不按场景拆专用发送者字段。
4. `mini-program` 侧不再新增独立的 `chat_*` 消息表定义,统一以 `im_*` 为 IM 正表。
5. 发送接口必须使用 `client_msg_id` 幂等。
6. 查询前必须校验 `parent_child_relations` 权限。
7. 列表展示优先读取消息快照字段,降低跨表查询成本。
### 5.5 位置流程
1. 手表上报定位写入 `child_location_history`
2. 同事务更新 `child_location_current`
3. 小程序查询当前定位读 `child_location_current`,轨迹读 `child_location_history`
4. 地址解析使用腾讯位置服务;若解析失败,前端降级展示经纬度。
## 6. 统一约束与实现规则
1. 所有设备引用字段统一使用 `device_id VARCHAR(64)`,不再新增 `watch_device_id BIGINT`
2. 设备主身份只认 `device_auth.device_id`
3. 跨主体通用 ID 字段统一采用字符串(如 `participant_*_id``sender_id``receiver_id`)。
4. 所有业务表统一包含 `created_at/updated_at`(历史流水类可无 `updated_at`)。
5. `status` 用于业务态,`is_active` 用于启停态;两者不混用。
6. 关键写路径(绑定确认、发消息、位置写入)必须事务化。
7. 腾讯位置服务启用前提:必须配置合法 `key`,并完成域名/来源白名单与配额设置。
8. 地图相关接口统一使用 `gcj02``lat,lng`;禁止混用 `wgs84/bd09` 直接入库。
9. 小程序端只上送一次性 `code`,不得自行调用微信换取 `openid`,更不得持有 `AppSecret`
10. `code` 只用于调用微信 `code2Session`,不得入库为 `openid``unionid` 或任何长期身份字段。
11. `session_key` 不返回前端,不写入 `parents` 主表;如需支持手机号等敏感数据解密,应服务端短期缓存并设置明确过期时间。
12. `nickname/avatar_url` 仅作资料字段,不能作为登录态、账号归并或权限判断依据。
## 7. 分阶段落地
1. 第一阶段:落地微信登录链路,打通 `Taro.login -> /auth/login -> code2Session -> parents -> JWT`
2. 第二阶段:落地 `parents/children/parent_child_relations/device_bind_*`,打通绑定链路。
3. 第三阶段:落地 `im_conversations/im_messages`,打通家长与小孩 IM。
4. 第四阶段:落地 `child_location_current/history`,打通定位链路。
5. 第五阶段:补充审计、告警、实时推送和运营能力。
## 8. 验收标准
1. 家长可通过真实微信登录完成换码,并获取本地 Bearer Token。
2. `parents.openid` 存储值来自微信返回的真实 OpenID而不是前端 `code` 或测试标识。
3. 不修改 `01-init.sql` 任一现有表,服务可正常启动。
4. 设备鉴权、角色配置、OTA能力保持可用。
5. 家长可完成扫码绑定、解绑、重绑,且历史保留。
6. 家长可查看自己小孩与其他小孩完整双向消息。
7. 小程序可查看小孩最新位置与历史轨迹。
## 9. 共享库落地规范
### 9.1 唯一数据库与唯一 Schema
1. `mini-program``talkingq-url` 必须共用同一个 MySQL 数据库:`talkingq`
2. `database/talkingq_shared_schema.sql` 是唯一 schema 真相源。
3. `mini-program/database/init.sql` 仅保留给历史上的“单服务独立开发”场景,不再作为共享部署的建库依据。
### 9.2 表归属Table Ownership
`talkingq-url` 拥有以下表的写入责任:
1. `device_auth`
2. `device_configs`
3. `conversation_histories`
4. `conversation_messages`
5. `roles`
6. `role_languages`
7. `device_firmware_update`
8. `system_config`
`mini-program` 拥有以下表的写入责任:
1. `parents`
2. `children`
3. `parent_child_relations`
4. `device_bindings`
5. `device_bind_sessions`
6. `device_bind_history`
7. `device_settings`
8. `cards`
9. `im_conversations`
10. `im_messages`
11. `child_location_current`
12. `child_location_history`
约束:
1. 两个服务允许跨服务读取共享依赖表,但不允许跨服务写对方负责的主表。
2. `mini-program` 只读 `device_auth`,不直接承担设备注册主流程。
3. `talkingq-url` 不写 `parents/children/device_bindings/im_* /child_location_*`
### 9.3 共享字段与真相源
1. 设备主身份统一以 `device_auth.device_id` 为准。
2. 设备与 AI 的历史消息统一以 `conversation_histories + conversation_messages` 为准。
3. 家长/儿童 IM 统一以 `im_conversations + im_messages` 为准。
4. 设备音量如需双端共享,以 `device_configs.volume` 为真相源;`device_settings.volume` 仅作小程序展示和兼容字段。
### 9.4 迁移原则
1. 目标库固定为 `talkingq`
2. 迁移时以 `talkingq-url` 当前使用的 `talkingq` 数据为准。
3. 历史 `mini_program` 库中的业务数据不并入 `talkingq`,也不作为冲突解决依据。
4. 迁移动作只做两件事:
1.`talkingq` 上补齐 `mini-program` 所需扩展表。
2.`mini-program` 服务配置切换到 `talkingq`
5. `mini_program` 库可在切换验证完成后归档或删除,但不再作为运行时数据库。
### 9.5 启动与建表策略
1. 共享部署下,服务启动默认只检查数据库连通性,不自动执行 ORM `create_all`
2. 只有显式开启开发开关时,`mini-program` 才允许自动建表。
3. 生产、联调、共享测试环境统一通过 `database/talkingq_shared_schema.sql` 或正式 migration 执行建表与变更。