feat: 完善淘宝小程序拍照、报告与登录流程

接入本地实时识别和自动三连拍,调整报告裁切、商品跳转、分享标题及居中页眉;完善游客会话、后台渠道隔离、部署脚本与回归测试。
This commit is contained in:
xsl
2026-11-04 15:07:51 +08:00
parent 381da60ae4
commit 774376fa16
147 changed files with 17420 additions and 597 deletions
+21
View File
@@ -3,8 +3,29 @@
这份目录存放**不在 git 里自动生效**的服务器侧配置:systemd unit、nginx 片段、安装脚本。
换机器/换人接手时,按本文重建即可。
## 淘宝拍照语音静态资源
`taobao-voice-media.py` 用于修复 `/state/assets/voice/` 的媒体响应。当前已部署:清单内 14 个公开 MP3 和 manifest.json 从 `/home/ubuntu/juzi/h5/assets/voice/` 复制到 `/var/www/juzi-voice-20261102/`,Nginx 在原 HTTPS server 增加该路径的 alias,返回 `audio/mpeg` 并支持 Range/206。此步骤只 reload Nginx,不重启三个业务应用;原 `/state/` 页面、后台认证、数据目录禁用及 `/tb/` 白名单保留。
在服务器使用 `sudo python3 /tmp/juzi-voice-media-20261102.py` 执行上传后的脚本;脚本验证清单和路径、拒绝源/目标符号链接、备份配置并执行 `nginx -t`,验证或 reload 失败会回滚。原站点配置备份为 `/etc/nginx/sites-available/juzi.voice-20261102.bak`。若日后更新语音文件,重新执行脚本刷新公开副本。14 条线上音频完整内容已与本地文件逐一比对一致。
客户端声音状态见 `[voice] VOICE-20261102-02` 真机 Console 日志;事件包括 loading/canplay/play_requested/playing/error。日志不包含原生错误对象、账号、照片或完整 URL。
## 拓扑
### 公开视频(2026-11-02)
用户指定的 `261026改双仓5.mp4` 已上传,源文件 SHA256 为 `46ae3089e4da884f03313b71809814813b88f1abc92d374dc8210f01db10b026`。视频为 2160×3840 H.264/AAC,约 32 秒;仅用 FFmpeg `-c copy -movflags +faststart` 调整封装,不重编码。
- 服务器文件:`/var/www/juzi-media/videos/261026-shuangcang5-46ae3089.mp4`,SHA256 `64790107cce2f78e68668474fb06d2e11acf3d76d9e05b7ccec4f588e89eb02e`。
- 播放页:`https://1me.xajuzi.com/media/videos/261026-shuangcang5-46ae3089.html`。
- 二维码:项目内 `deliverables/261026-shuangcang5/video-qr.png`,已解码确认指向上述播放页。
- 发布脚本:`deploy/publish-video.py`;Nginx 为 `/media/videos/` 单独 alias,禁止目录索引及写入方法,支持 MP4 MIME、Range/206。
- 本次配置备份:`/etc/nginx/sites-available/juzi.public-video-20261102T113656914226Z.bak`。仅 reload Nginx,三个业务服务未重启。
- 验证:上传文件哈希一致、公网 HTML 200、MP4 206/32 字节范围请求正常;移动视口 Chromium 已实际开始播放,视频无解码错误。手机淘宝/微信内的具体播放体验仍以扫码为准。
微信反馈(能开页、播放卡住)后,另发布 `261026-shuangcang5-mobile-v2.mp4/.html`:720×1280、H.264 Main Level 3.1、AAC、faststart,5,329,575 字节,SHA256 `9f34e5a568230e87fb206f101467db9eb22d31c5951cedb26d02eacad709e33c`。保留原 4K 文件;旧 HTML 也改为播放兼容版,备份在 `/tmp/juzi-public-video-20261102-46ae3089/legacy-player-before-v2.html`。新二维码 `deliverables/261026-shuangcang5/video-qr-mobile-v2.png` 指向新的 HTML 路径以避开旧页面缓存。新增用户点击播放按钮及内联属性、等待/失败提示和直接视频链接;移动视口浏览器通过真实按钮点击已开始播放 720×1280 视频,微信真机仍待用户扫码验证。
```
淘宝小程序(taobao/,AppKey 35486228)
→ 淘宝云网关(注入 app_key / open_id / sign,单请求 10s / 128KB 上限)
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Publish an explicitly selected public video and player; reload nginx only."""
import argparse
import hashlib
from pathlib import Path
import re
import shutil
import subprocess
from datetime import datetime, timezone
PUBLIC = Path('/var/www/juzi-media/videos')
CONFIG = Path('/etc/nginx/sites-available/juzi')
MARKER = '# BEGIN juzi-public-videos'
BLOCK = ''' # BEGIN juzi-public-videos
location ^~ /media/videos/ {
alias /var/www/juzi-media/videos/;
autoindex off;
types { video/mp4 mp4; text/html html; }
default_type application/octet-stream;
charset utf-8;
add_header X-Content-Type-Options nosniff;
add_header Cache-Control "public, max-age=3600";
limit_except GET HEAD { deny all; }
}
# END juzi-public-videos
'''
def checked(args):
subprocess.run(args, check=True, capture_output=True, text=True)
def digest(path):
result = hashlib.sha256()
with path.open('rb') as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b''):
result.update(chunk)
return result.hexdigest()
def main():
parser = argparse.ArgumentParser()
parser.add_argument('staging')
parser.add_argument('stem')
parser.add_argument('sha256')
args = parser.parse_args()
assert re.fullmatch(r'[a-z0-9-]{1,80}', args.stem)
assert re.fullmatch(r'[a-f0-9]{64}', args.sha256)
staging = Path(args.staging)
assert staging.resolve() == staging and staging.parent == Path('/tmp')
assert staging.name.startswith('juzi-public-video-') and not staging.is_symlink()
sources = [staging / (args.stem + suffix) for suffix in ('.mp4', '.html')]
assert all(p.is_file() and not p.is_symlink() for p in sources)
assert digest(sources[0]) == args.sha256, 'uploaded video checksum mismatch'
assert not CONFIG.is_symlink()
original = CONFIG.read_text()
if MARKER not in original:
assert 'location ^~ /media/videos/' not in original
assert original.count(' location /state/ {') == 1
for directory in [PUBLIC.parent, PUBLIC]:
assert not directory.is_symlink()
directory.mkdir(mode=0o755, exist_ok=True)
assert directory.resolve() == directory
for source in sources:
target = PUBLIC / source.name
assert not target.is_symlink()
if target.exists():
assert digest(target) == digest(source), 'refusing to overwrite a different published file'
else:
shutil.copyfile(source, target)
target.chmod(0o644)
if MARKER not in original:
stamp = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S%fZ')
backup = CONFIG.with_name('juzi.public-video-' + stamp + '.bak')
assert not backup.exists()
shutil.copy2(CONFIG, backup)
CONFIG.write_text(original.replace(' location /state/ {', BLOCK + ' location /state/ {', 1))
try:
checked(['nginx', '-t'])
checked(['systemctl', 'reload', 'nginx'])
except Exception:
shutil.copy2(backup, CONFIG)
checked(['nginx', '-t'])
checked(['systemctl', 'reload', 'nginx'])
raise
print('nginx backup: ' + str(backup))
else:
assert BLOCK in original, 'existing video location differs; inspect before changing'
checked(['nginx', '-t'])
print('video sha256: ' + digest(PUBLIC / sources[0].name))
print('player: https://1me.xajuzi.com/media/videos/' + args.stem + '.html')
if __name__ == '__main__':
main()
+1 -1
View File
@@ -1,5 +1,5 @@
[Unit]
Description=1me taobao mini program backend (Node, /tb/ debug instance)
Description=1'me taobao mini program backend (Node, /tb/ debug instance)
After=network.target
[Service]
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Serve public voice assets with MP3 MIME/range support; keep app services running.
Run on the existing Ubuntu host using sudo. Only manifest-listed public files
are copied. Nginx config is backed up and rolled back if validation/reload fails.
"""
import json
from pathlib import Path
import re
import shutil
import subprocess
SOURCE = Path('/home/ubuntu/juzi/h5/assets/voice')
PUBLIC = Path('/var/www/juzi-voice-20261102')
CONFIG = Path('/etc/nginx/sites-available/juzi')
BACKUP = CONFIG.with_name('juzi.voice-20261102.bak')
MARKER = '# BEGIN juzi-voice-media 20261102'
BLOCK = ''' # BEGIN juzi-voice-media 20261102
location ^~ /state/assets/voice/ {
alias /var/www/juzi-voice-20261102/;
types { audio/mpeg mp3; application/json json; }
default_type application/octet-stream;
add_header Cache-Control "public, max-age=3600";
limit_except GET HEAD { deny all; }
}
# END juzi-voice-media 20261102
'''
def checked(command):
result = subprocess.run(command, capture_output=True, text=True)
if result.returncode:
raise RuntimeError('command failed: ' + ' '.join(command) + '\n' + result.stderr)
def main():
manifest = json.loads((SOURCE / 'manifest.json').read_text())
names = manifest['files']
assert names and all(isinstance(name, str) and re.fullmatch(r'[a-z0-9-]+', name) for name in names)
files = [SOURCE / (name + '.mp3') for name in names] + [SOURCE / 'manifest.json']
assert all(file.is_file() and not file.is_symlink() and file.resolve().parent == SOURCE.resolve() for file in files)
original = CONFIG.read_text()
if MARKER not in original:
assert not BACKUP.exists(), 'existing backup must be preserved'
assert original.count(' location /state/ {') == 1
PUBLIC.mkdir(mode=0o755, parents=True, exist_ok=True)
assert PUBLIC.resolve() == Path('/var/www/juzi-voice-20261102')
for file in files:
target = PUBLIC / file.name
assert not target.is_symlink()
shutil.copyfile(file, target)
target.chmod(0o644)
if MARKER not in original:
shutil.copy2(CONFIG, BACKUP)
CONFIG.write_text(original.replace(' location /state/ {', BLOCK + ' location /state/ {', 1))
try:
checked(['nginx', '-t'])
checked(['systemctl', 'reload', 'nginx'])
except Exception:
shutil.copy2(BACKUP, CONFIG)
checked(['nginx', '-t'])
checked(['systemctl', 'reload', 'nginx'])
raise
else:
checked(['nginx', '-t'])
print(json.dumps({'publicFiles': len(files), 'nginxConfigValid': True,
'backup': str(BACKUP), 'businessServicesRestarted': False}))
if __name__ == '__main__':
main()