Compare commits

..
64 Commits
Author SHA1 Message Date
xsl 0e34eeb1df fix: prevent duplicate titles on Taobao login page
Keep a single native title on the entry page even when the host ignores title clearing. Preserve centered headers elsewhere and normalize manifest line endings during shared-code checks.
2026-11-05 10:43:01 +08:00
xsl 873b45d3a5 fix: preserve legacy admin URLs across H5 channels
Keep local record and download paths compatible while retaining explicit external source selection. Record Taobao, H5, WeChat and rednote compatibility verification and baseline test failures.
2026-11-04 15:17:56 +08:00
xsl 774376fa16 feat: 完善淘宝小程序拍照、报告与登录流程
接入本地实时识别和自动三连拍,调整报告裁切、商品跳转、分享标题及居中页眉;完善游客会话、后台渠道隔离、部署脚本与回归测试。
2026-11-04 15:07:51 +08:00
xsl 381da60ae4 chore: commit test-phase secrets so a new dev machine needs no manual copy
测试验证阶段,换开发机时不再需要手工拷贝两个机密文件。已与用户确认安全
要求不高,代价是密钥进入 git 历史,上线前必须清理。

入库(.gitignore 用 ! 例外放行):
- backend-java/src/main/resources/application-local.yml
  淘宝 AppKey/AppSecret + verify-sign + mode=forward + forward-base
- reference/facepp-skinanalyze/credentials.env
  旷视 FACEPP_API_KEY / FACEPP_API_SECRET / FACEPP_SKINANALYZE_URL
  (从服务器同名文件裁剪而来,只保留旷视相关行,去掉了其它项目条目)

其它密钥防护保持不变:apikey.*、.env、*.env 等仍被忽略。

上线前必做(已写入条待办第 6 项与 .gitignore 注释):
  1) 重置 AppSecret 与旷视 Key
  2) git filter-repo 从历史清除这两个文件
  3) 改回环境变量 / 保密字典注入

HANDOFF 1.5 节、待办第 6 项、交接检查清单已同步为「clone 即有」。
2026-11-01 23:53:11 +08:00
xsl 8bc6251899 docs(handoff): clarify where the two secret files come from
application-local.yml 在老开发机;credentials.env 只存在服务器
(老开发机上没有这个文件),需要 scp 取。补充了目标路径、验证命令
和「只做小程序端开发可以先不拷 credentials.env」的说明。
2026-11-01 23:51:04 +08:00
xsl 2eb0d8e807 docs(proposal): add Taobao product proposal decks and their generators
产品方案 PPT 是交付物,之前只存在于本机,换开发机后会丢,所以入库。

- 3 份 pptx:智能测肤方案(正版 1.3MB / 深色版 1.3MB)+ 购物小程序方案模版(0.1MB);
  模版是 build_taobao_proposal_from_template.py 的输入,必须一起入库
- 3 个生成脚本:把硬编码的 D:\juzi\... 改为相对仓库根(Path(__file__).parents[1]),
  渲染用的截图素材 temp/ppt_assets 属临时产物,加进 .gitignore
- build_taobao_proposal_pptx.py 是更早的一版(产物 AI智能测肤...pptx 当前不在仓库),
  保留用于追溯版式
- 新增 .gitignore:/sdk-java-35479221/(淘宝开放平台自动生成的 Java SDK,
  对应已废弃的 AppKey 35479221,当前架构走 HTTP 透传,不引 SDK)
2026-11-01 23:37:38 +08:00
xsl 43a5373233 docs: add development-machine handoff and server deploy configs
换开发机继续开发。新机器只需要拉仓库 + 自备工具链 + 单独拷贝两个机密文件,
服务器/聚石塔/SAE 全部沿用,不需要重建。

- docs/HANDOFF-2026-11-02.md:开发机交接(环境、仓库地图、三套测试、
  本地怎么跑、云端资源清单、上线方式、11 条关键结论、9 项待办、故障对照表)
- deploy/:服务器侧配置入库(原先只存在于机器上)
  - systemd/juzi-taobao.service(18081,独立数据目录与凭据文件)
  - nginx/tb-location.conf(/tb/ 反代 + 只放行聚石塔 NAT EIP)
  - add-tb-nginx.sh(幂等插入,先备份,nginx -t 通过才 reload)
  - README.md(重建与回滚步骤)
- tools/env-java.ps1:优先用仓库内便携工具链,缺失时回退 JAVA_HOME/PATH,
  换机器不用再下便携 JDK/Maven
- README.md:交接文档入口更新
2026-11-01 23:34:26 +08:00
xsl fed52930cc feat(miniapp): align UI with H5 and harden device debugging
五个页面按 h5/css 的 style.css → ui-20261009 → ui-20261012 覆盖顺序逐条重排,
换算规则固定为 calc(N * var(--u)) → 2N rpx,固定 px 的字号保持 px。

- app.acss:换回 H5 设计 token(方角小倒角按钮 10rpx、卡片 14rpx、.chev 折线箭头)
- splash/profile/history:底图与按钮文字图改用 H5 资源,隐私/政策弹窗按 H5 重排
- report:环形分用 conic-gradient、人脸裁切窗口叠加 face_maps、标记点百分比盒、
  侧脸皱纹 pending/unavailable 文案;用 Playwright 抓 H5 94 个选择器几何比对,
  整页高 2438.7 vs 2438.67
- camera:淘宝 <camera> 组件在真机整页崩溃(嵌了微信专有的 <cover-view>),
  改为默认系统相机 + `config.nativeCamera` 开关 + pages/camtest 冒烟页验证组件
- permissions:淘宝用带前缀的 scope.camera(裸名返回 5022 Scope:camera信息不存在),
  先试带前缀再退回裸名,拒绝后不再重复弹窗
- voice:非 HTTPS 资源导致的异常不再冒泡成整页崩溃;app.js 加开发版错误 toast
- 开发版入口:权限诊断(启动页/相机页)、相机组件测试
- 测试 20/20(新增权限与相机能力用例)
2026-11-01 23:30:38 +08:00
xsl f5c416dd9d feat(sae): add cloud-gateway adapters and forward mode (0.0.4)
SAE 上这一版只做云网关适配层:/api/taobao/* 字节级透传给 1me.xajuzi.com 上
现有的 H5 后端(旷视调用、评分、报告都留在那边),Java 不再重写测肤逻辑。

- TaobaoEnvelope:客户端要求的 data.ok + data.http_status 信封(HTTP 恒 200)
- TaobaoAuthController:challenge/session/me/logout(native 模式)
- TaobaoSessionService:一次性 challenge + 30 天 Bearer token,按 open_id 限流
  (网关后面所有请求同源 IP,按 IP 计数会变成全站共享 30 次/分钟)
- TaobaoForwardController:mode=forward 时透传,保留原始 query 与 body 字节
  (签名覆盖原始 body,重新序列化会让验签失败)
- TaobaoSignatureVerifier:缺省 sign_method 按 md5 校验
- 打包名固定为 taobao-mvp-backend-0.0.4-forward,SAE 不需要任何环境变量
- pom 版本 0.0.4;mvn test 11/11(新增 8 个用例)
- docs:更新 SAE 操作步骤,新增本次交接文档
2026-11-01 23:30:35 +08:00
xsl 65ba3cc7a5 fix(taobao): default cloud gateway signature to the md5 form
真实网关请求不带 sign_method(2026-11-01 用 nginx access.log 抓到的真实请求离线复算确认),
缺省签名形式是 MD5(secret + content + secret),也就是 sign_method=md5 的算法,
而不是 MD5(content + secret)。旧实现按「空方法」走了后者,必然 mismatch,
真机登录一直卡在 401「淘宝身份校验失败」。

- h5/lib/taobao-auth.js:缺省 method 归一为 md5
- h5/test/taobao-auth.js:把抓包参数固化成回归测试(含旧算法的反例与 verify 正反例)
2026-11-01 23:30:29 +08:00
xsl ac0b7ca42b feat(sae): add Java MVP backend and Taobao connectivity page
Spring Boot 3.5.6 / Java 17 JAR with cloud gateway envelope and ping/whoami/echo; Taobao gateway signature verification matching the Node implementation; /pages/mvp page, splash dev entry and local transport contract test; SAE + 聚石塔 runbook and portable JDK/Maven setup; Windows 11 wmic shim for Taobao DevTools.
2026-10-31 18:35:04 +08:00
xsl 459e21dad4 docs(rednote): record verified isolated production deployment 2026-10-19 23:04:28 +08:00
xsl 4af825a5e4 feat(rednote): add isolated persistent browser H5 entry 2026-10-19 22:59:10 +08:00
xsl 9eec4369da feat(taobao): add native mini program with shared backend
Preserve H5 flows while adding Taobao identity, native capture, reports and isolated local development. Fix canceled province selection and expired-session logout. Record passing local regressions, two pre-existing H5 assertion failures and pending platform validation.
2026-10-19 20:14:31 +08:00
xsl d07f8cc1bb save 2026-10-17 17:01:46 +08:00
xsl bb6da17f54 fix(recommendation): use displayed skin age across reports and admin 2026-10-16 19:03:25 +08:00
xsl 571d78a2ed ui(report): show wrinkle loading hints over side photos 2026-10-16 16:45:07 +08:00
xsl 9181069643 perf(report): load side wrinkle layers after report entry 2026-10-16 16:34:11 +08:00
xsl b19811f974 fix(camera): add 30-second scan waiting watchdog 2026-10-16 16:07:53 +08:00
xsl 5e07985b19 fix(camera): remove post-capture photo revalidation 2026-10-16 16:01:25 +08:00
xsl 6a628f837b fix(camera): make lighting checks advisory only 2026-10-16 15:51:07 +08:00
xsl 9ff7a582af feat(admin): track capture-to-report conversion funnel 2026-10-16 15:45:13 +08:00
xsl 03a8907b4e fix(camera): lower frontal and side confidence thresholds 2026-10-16 15:35:53 +08:00
xsl 30c90f8df5 fix(camera): relax initial frontal face recognition 2026-10-16 15:27:36 +08:00
xsl 8365ef459f fix(report): remove eye conditions from contour conclusions 2026-10-16 11:05:34 +08:00
xsl 98d6189f5c final save 2026-10-15 22:16:01 +08:00
xsl 49db1a6f5b fix(camera): relax pitch and roll acceptance limits 2026-10-15 21:16:46 +08:00
xsl c6d65493b6 fix(camera): set accepted face brightness to 110–220 2026-10-15 20:51:19 +08:00
xsl fbc556d1a6 fix(report): move side photo preview fill upward 2026-10-15 20:47:41 +08:00
xsl 8115db4ec2 fix(camera): constrain brightness and adjust side photo previews 2026-10-15 20:41:28 +08:00
xsl 480fed40cc fix(camera): raise minimum face brightness to 100 2026-10-15 20:23:45 +08:00
xsl 5632ac43d7 fix(report): cap dimension scores and match zoom crops to thumbnails 2026-10-15 20:17:07 +08:00
xsl 482fb4280d fix(h5): relax capture angles and annotate acne marks 2026-10-15 19:55:21 +08:00
xsl 757b96dfad feat(report): add annotated wrinkle views and restore capture countdown 2026-10-15 19:45:21 +08:00
xsl 2d80f6dfd0 feat(report): unify display grades and add contour side photos 2026-10-15 17:14:05 +08:00
xsl 60f0965c5a feat(scoring): use independent tone hydration minima and focus-first care 2026-10-15 14:40:59 +08:00
xsl 77493bdff5 feat(h5): align report care and single-product section with updated design 2026-10-15 01:30:45 +08:00
xsl 60ae6c97ef feat(h5): update skin scoring, age display and capture countdown 2026-10-15 00:20:48 +08:00
xsl d8c67762da feat(h5): calculate integer total from weighted dimensions across reports and admin 2026-10-14 14:25:17 +08:00
xsl 61a7f7c726 fix(h5): enlarge regular brand text for optical alignment 2026-10-13 22:40:04 +08:00
xsl fc711e3b1c fix(h5): add comma after brand name on splash and photo guide 2026-10-13 20:29:40 +08:00
xsl 6acc541cd8 fix(h5): unify regular slogan typography on splash and photo guide 2026-10-13 19:01:00 +08:00
xsl 84a92137aa fix(h5): use replacement flip icon on report scanning screen 2026-10-13 18:04:32 +08:00
xsl 7381306139 fix(h5): optimize camera flipping and angle haptics, refresh splash branding 2026-10-13 17:57:55 +08:00
xsl e71368db6d fix(h5): show raw skin age, support Harmony touch pickers and add WeChat logout 2026-10-13 16:12:54 +08:00
xsl 4e42e33994 fix(h5): rename guest entry button to 游客登陆 2026-10-13 15:28:07 +08:00
xsl 1fdbb575b0 feat(h5): allow guest access without login and retain history only for WeChat 2026-10-13 15:23:23 +08:00
xsl 98935dd168 fix(h5): prevent native scrolling from stealing diagonal image swipes 2026-10-13 00:42:01 +08:00
xsl 4c478b16e3 fix(h5): prioritize horizontal report image gestures 2026-10-13 00:32:44 +08:00
xsl 3efa28b311 fix(h5): reduce report swipe conflicts and limit edge overscroll 2026-10-13 00:24:52 +08:00
xsl f1e64565b9 fix(h5): center grouped score unit and tighten report summary spacing 2026-10-13 00:12:02 +08:00
xsl 6f83f58a09 fix(h5): center score stack, lock profile scroll and gate turn guidance 2026-10-12 23:48:11 +08:00
xsl 2a49ebdab2 test(deploy): verify report touch release on public server 2026-10-12 22:44:41 +08:00
xsl 63c9e065a9 feat(h5): refine report layout, add finger-following tabs and optimize images 2026-10-12 22:36:44 +08:00
xsl bcf198ca6f 添加反馈文档 2026-10-12 21:46:47 +08:00
xsl 69dc9962c0 fix(h5): show captured face behind report scan animation 2026-10-12 21:21:49 +08:00
xsl ce6dbc20d4 feat(h5): remember profile selections and add finger-following skin swipe 2026-10-12 21:10:00 +08:00
xsl c9352fe4ca fix(h5): ease camera capture requirements and rename page title 2026-10-12 20:47:58 +08:00
xsl 3f165bf9e5 test(deploy): verify fourth feedback capture and public release 2026-10-12 20:27:45 +08:00
xsl 92a3965b7d feat(h5): apply fourth feedback and tighten local capture validation 2026-10-12 20:16:41 +08:00
xsl e53c809522 docs(deploy): record third feedback production verification 2026-10-12 16:38:36 +08:00
xsl c7b085e795 feat(h5): align third feedback with latest PSD and scanning screen 2026-10-12 16:33:10 +08:00
xsl f3e7eac909 test(deploy): verify second feedback release on public site 2026-10-12 14:40:25 +08:00
xsl fe9623a1d7 feat(h5): apply second customer UI feedback and homepage refinements 2026-10-12 14:35:39 +08:00
400 changed files with 58658 additions and 3483 deletions
+30 -3
View File
@@ -1,19 +1,46 @@
# 密钥与敏感配置 —— 绝不入库 # 密钥与敏感配置
apikey.bash apikey.bash
apikey.* apikey.*
aipkey.bash
.env .env
.env.* .env.*
*.env *.env
credentials.env
# ⚠️ 2026-11-02 测试验证阶段的例外(刻意入库,方便切换开发机)
# reference/facepp-skinanalyze/credentials.env —— 旷视 Face++ 凭据
# backend-java/src/main/resources/application-local.yml —— 淘宝 AppKey/AppSecret
# 约束:
# 1) 仅限内网私有仓库 git.xiangsilian.com/xsl/juzi,不要 fork 到公开仓库;
# 2) 正式上线前必须重置密钥 → 从 git 历史清除 → 改回环境变量/保密字典注入;
# 3) 不要复制到聊天、文档、工单等任何外部渠道。
# 下面两条用 ! 覆盖上面的通配规则(必须放在被覆盖的规则之后)。
!reference/facepp-skinanalyze/credentials.env
!/backend-java/src/main/resources/application-local.yml
# 运行时产物 # 运行时产物
node_modules/ node_modules/
*.log *.log
__pycache__/ __pycache__/
aipkey.bash
h5/data/ h5/data/
temp/ temp/
# Java MVP 构建产物
/backend-java/target/
# 便携工具链(体积大,本机安装,不入库)
/tools/jdk17/
/tools/maven/
/tools/downloads/
# 大型设计源文件,不入库
new_feedback/第三轮反馈20261012/UI外发版10.09.psd
# 淘宝开放平台自动生成的 Java SDK(对应已废弃的 AppKey 35479221,当前架构不引 SDK)
/sdk-java-35479221/
# 提案 PPT 生成时用的截图素材(临时产物,可重新截取)
/temp/ppt_assets/
# graphify 本地缓存与日期快照(可重建,无需入库) # graphify 本地缓存与日期快照(可重建,无需入库)
graphify-out/cache/ graphify-out/cache/
graphify-out/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/ graphify-out/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/
+28
View File
@@ -0,0 +1,28 @@
# 巨子测肤 · H5 与淘宝小程序
本仓库包含两个入口,共用 Node 后端、Face++ 检测、评分与报告话术。
- `h5/`:现有浏览器/微信公众号 H5、管理后台,以及共享后端 `server.js`。
- `taobao/`:淘宝原生小程序。开发、联调、测试说明见 [taobao/README.md](taobao/README.md)。
- `backend-java/`:SAE + 淘宝云网关的 Java 最小连通性 MVP;操作步骤见 [docs/SAE-Java-MVP-操作步骤.md](docs/SAE-Java-MVP-操作步骤.md)。
- 当前交接(**换开发机先读这份**):[docs/HANDOFF-2026-11-02.md](docs/HANDOFF-2026-11-02.md)。
- 上一轮交接:[docs/HANDOFF-2026-11-01.md](docs/HANDOFF-2026-11-01.md)。
- 服务器侧配置(systemd / nginx `/tb/` 白名单 / 安装脚本):[deploy/README.md](deploy/README.md)。
- `rednote/`:复用 H5 的持久浏览器身份入口,独立运行和存储。说明见 [rednote/README.md](rednote/README.md)。
- `h5/js/skin-score.js`、`report-ui.js`、`report-copy.js`:业务规则的源文件。小程序内的副本由同步脚本生成并检查哈希。
- `h5/lib/taobao-*.js`:淘宝网关验签、短期上传/异步任务和独立 API 路由。
- `docs/`、`reference/`、`design/`、`feedback/`、`new_feedback/`:需求、接口资料、设计与历轮反馈;早期文档的规则可能已被后续实现替代。
- `backend/docs/`:后台需求资料;可运行的共享服务实际在 `h5/server.js`。
- `temp/`:本地测试数据、预览和截图,已忽略,不提交。
兼容边界:允许共享后端扩展,但保持 H5 的页面、微信公众号链接、微信登录、游客不留存和历史报告行为。淘宝使用独立账号来源,不把淘宝账号与微信账号自动合并。
本轮工作只在本地开发和样本验证,未部署或修改线上服务器。启动本地隔离环境:
```powershell
cd taobao
npm ci
npm run dev:backend
```
H5 本地入口为 http://127.0.0.1:18180;这个环境固定使用测试样本,不调用真实 Face++。生产 H5 的运行说明仍见 [h5/README.md](h5/README.md)。
+56
View File
@@ -0,0 +1,56 @@
# taobao-mvp-backend
淘宝小程序 + 淘宝云网关 + SAE 的最小 Java 后端验证工程。
## 快速启动
```powershell
Set-ExecutionPolicy -Scope Process Bypass
. D:\juzi\tools\env-java.ps1
cd D:\juzi\backend-java
mvn -DskipTests package
java -jar target\taobao-mvp-backend-0.0.2.jar
```
启动后:
- 健康检查:<http://127.0.0.1:8080/actuator/health>
- 云网关测试接口:`POST http://127.0.0.1:8080/api/taobao/ping`
## 接口
| 方法 | 路径 | 作用 |
|---|---|---|
| GET | `/actuator/health` | SAE 健康检查 |
| GET/POST | `/api/taobao/ping` | 返回统一 envelope,验证连通性 |
| POST | `/api/taobao/whoami` | 返回网关注入的 query 参数(open_id/app_key/env/sign 等) |
| POST | `/api/taobao/echo` | 原样回显 JSON body,验证 POST/Content-Type |
统一响应:
```json
{"success":true,"data":{"ok":true}}
```
失败:
```json
{"success":false,"errorCode":"SIGN_INVALID","errorMessage":"..."}
```
## 环境变量(SAE)
| 变量 | 本地 | 云端 |
|---|---|---|
| `TAOBAO_APP_KEY` | 可空 | 淘宝开放平台 AppKey |
| `TAOBAO_APP_SECRET` | 可空 | AppSecret,只放服务端 |
| `TAOBAO_VERIFY_SIGN` | `false` | `true` |
云端必须用 `TAOBAO_VERIFY_SIGN=true`,否则任何知道地址的人都能直接调用接口。
## 说明
- 本工程只实现连通性 MVP:`ping/whoami/echo`。
- 真实的 `challenge/session/photo/skin-analyze/analysis/reports` 尚未迁移;现有 Node 实现见 `../h5/lib/taobao-routes.js`,可作为 Java 迁移的接口契约参考。
- SAE 部署时选 **JAR 包部署 + 标准 Java 应用运行环境 + Open JDK 17**,不要选 WAR/Tomcat/EDAS-Container。
- 端口 `8080`,健康检查 `/actuator/health`,至少 2 实例。
+50
View File
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.6</version>
<relativePath/>
</parent>
<groupId>com.juzi</groupId>
<artifactId>taobao-mvp-backend</artifactId>
<version>0.0.4</version>
<name>taobao-mvp-backend</name>
<description>Taobao mini program + SAE + cloud gateway minimal MVP backend</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<!-- 文件名带 forward 标识,避免和 0.0.2(native 模式)混淆 -->
<finalName>taobao-mvp-backend-0.0.4-forward</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,12 @@
package com.juzi.mvp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MvpApplication {
public static void main(String[] args) {
SpringApplication.run(MvpApplication.class, args);
}
}
@@ -0,0 +1,20 @@
package com.juzi.mvp.api;
import com.fasterxml.jackson.annotation.JsonInclude;
/**
* 淘宝云网关要求的统一响应结构。
* 成功:{"success":true,"data":{...}}
* 失败:{"success":false,"errorCode":"...","errorMessage":"..."}
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public record ApiResponse<T>(boolean success, T data, String errorCode, String errorMessage) {
public static <T> ApiResponse<T> ok(T data) {
return new ApiResponse<>(true, data, null, null);
}
public static <T> ApiResponse<T> error(String errorCode, String errorMessage) {
return new ApiResponse<>(false, null, errorCode, errorMessage);
}
}
@@ -0,0 +1,123 @@
package com.juzi.mvp.api;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.juzi.mvp.gateway.TaobaoSignatureVerifier;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.nio.charset.StandardCharsets;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
@RestController
@RequestMapping("/api/taobao")
public class MvpController {
private final TaobaoSignatureVerifier verifier;
private final ObjectMapper objectMapper;
public MvpController(TaobaoSignatureVerifier verifier, ObjectMapper objectMapper) {
this.verifier = verifier;
this.objectMapper = objectMapper;
}
@PostMapping(value = "/ping", consumes = MediaType.ALL_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponse<Map<String, Object>> ping(@RequestBody(required = false) String rawBody,
HttpServletRequest request) {
TaobaoSignatureVerifier.Result check = verifier.verify(request, rawBody);
if (!check.accepted()) {
return ApiResponse.error("SIGN_INVALID", check.message());
}
Map<String, Object> data = new LinkedHashMap<>();
data.put("ok", true);
data.put("pong", true);
data.put("server", "java");
data.put("time", OffsetDateTime.now(ZoneId.of("Asia/Shanghai")).toString());
data.put("transport", isGateway(request) ? "gateway" : "direct");
data.put("appKey", param(request, "app_key"));
data.put("openId", param(request, "open_id"));
data.put("env", param(request, "env"));
data.put("signVerified", check.verified());
data.put("signMessage", check.message());
return ApiResponse.ok(data);
}
@GetMapping("/ping")
public ApiResponse<Map<String, Object>> pingGet(HttpServletRequest request) {
return ping(null, request);
}
@PostMapping(value = "/whoami", consumes = MediaType.ALL_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponse<Map<String, Object>> whoami(@RequestBody(required = false) String rawBody,
HttpServletRequest request) {
TaobaoSignatureVerifier.Result check = verifier.verify(request, rawBody);
if (!check.accepted()) {
return ApiResponse.error("SIGN_INVALID", check.message());
}
Map<String, Object> data = new LinkedHashMap<>();
data.put("ok", true);
data.put("transport", isGateway(request) ? "gateway" : "direct");
data.put("signVerified", check.verified());
data.put("signMessage", check.message());
data.put("params", sortedParams(request));
return ApiResponse.ok(data);
}
@PostMapping(value = "/echo", consumes = MediaType.ALL_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponse<Map<String, Object>> echo(@RequestBody(required = false) String rawBody,
HttpServletRequest request) {
TaobaoSignatureVerifier.Result check = verifier.verify(request, rawBody);
if (!check.accepted()) {
return ApiResponse.error("SIGN_INVALID", check.message());
}
Map<String, Object> data = new LinkedHashMap<>();
data.put("ok", true);
data.put("bytes", rawBody == null ? 0 : rawBody.getBytes(StandardCharsets.UTF_8).length);
data.put("body", parseBody(rawBody));
data.put("signVerified", check.verified());
return ApiResponse.ok(data);
}
private Object parseBody(String raw) {
if (raw == null || raw.isBlank()) {
return Map.of();
}
try {
return objectMapper.readValue(raw, new TypeReference<Object>() {
});
} catch (Exception e) {
Map<String, Object> fallback = new LinkedHashMap<>();
fallback.put("_raw", raw);
fallback.put("_parseError", e.getMessage());
return fallback;
}
}
private boolean isGateway(HttpServletRequest request) {
return request.getParameter("app_key") != null;
}
private String param(HttpServletRequest request, String name) {
String value = request.getParameter(name);
return value == null ? "" : value;
}
private Map<String, String> sortedParams(HttpServletRequest request) {
Map<String, String> out = new TreeMap<>();
request.getParameterMap().forEach((key, values) -> {
if (values != null && values.length > 0 && !"sign".equals(key)) {
out.put(key, values[0]);
}
});
return out;
}
}
@@ -0,0 +1,123 @@
package com.juzi.mvp.api;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.juzi.mvp.gateway.TaobaoSignatureVerifier;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 淘宝小程序登录相关的四个接口:challenge / session / me / logout。
*
* <p>契约来自参考实现 h5/lib/taobao-routes.js 与客户端 taobao/utils/api.js:
* <ul>
* <li>HTTP 永远 200,逻辑状态放 data.http_status(见 {@link TaobaoEnvelope})。</li>
* <li>客户端不发身份,身份来自云网关注入的 query 参数(app_key / open_id / sign)。</li>
* <li>session 必须带 consent=true 和一次性的 challenge。</li>
* <li>登录后所有接口用 Authorization: Bearer &lt;token&gt;。</li>
* </ul>
*/
@RestController
@RequestMapping("/api/taobao")
@ConditionalOnProperty(name = "taobao.mode", havingValue = "native", matchIfMissing = true)
public class TaobaoAuthController {
private static final String OPEN_ID_PATTERN = "[A-Za-z0-9_-]{4,256}";
private static final String IDENTITY_FAILED = "淘宝身份校验失败,请重新授权登录";
private static final String SESSION_EXPIRED = "检测会话已失效,请重新登录";
private final TaobaoSignatureVerifier verifier;
private final TaobaoSessionService sessions;
private final ObjectMapper objectMapper;
public TaobaoAuthController(TaobaoSignatureVerifier verifier,
TaobaoSessionService sessions,
ObjectMapper objectMapper) {
this.verifier = verifier;
this.sessions = sessions;
this.objectMapper = objectMapper;
}
@PostMapping(value = "/challenge", consumes = MediaType.ALL_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponse<Map<String, Object>> challenge(HttpServletRequest request) {
String id = sessions.challenge(sessions.rateKey(request));
if (id == null) {
return ApiResponse.ok(TaobaoEnvelope.fail(429, "操作频繁,请稍后重试"));
}
Map<String, Object> data = TaobaoEnvelope.ok(200);
data.put("challenge", id);
return ApiResponse.ok(data);
}
@PostMapping(value = "/session", consumes = MediaType.ALL_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponse<Map<String, Object>> session(@RequestBody(required = false) String rawBody,
HttpServletRequest request) {
Map<String, Object> body = parseBody(rawBody);
if (!Boolean.TRUE.equals(body.get("consent"))) {
return ApiResponse.ok(TaobaoEnvelope.fail(400, "请先阅读并同意隐私协议"));
}
TaobaoSignatureVerifier.Result check = verifier.verify(request, rawBody);
if (!check.accepted()) {
return ApiResponse.ok(TaobaoEnvelope.fail(401, IDENTITY_FAILED));
}
String openId = request.getParameter("open_id");
if (openId == null || !openId.matches(OPEN_ID_PATTERN)) {
// 只有经由云网关的请求才带 open_id;本地直连需要自己拼 ?open_id=...
return ApiResponse.ok(TaobaoEnvelope.fail(401, "未取得淘宝身份,请在手机淘宝内重新打开"));
}
Object challenge = body.get("challenge");
if (!(challenge instanceof String challengeId) || !sessions.consume(challengeId)) {
return ApiResponse.ok(TaobaoEnvelope.fail(401, IDENTITY_FAILED));
}
TaobaoSessionService.Token token = sessions.issue(openId);
Map<String, Object> data = TaobaoEnvelope.ok(200);
data.put("token", token.value());
data.put("expires_in", token.expiresInSeconds());
data.put("user", user(token.userId()));
return ApiResponse.ok(data);
}
@PostMapping(value = "/me", consumes = MediaType.ALL_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponse<Map<String, Object>> me(HttpServletRequest request) {
TaobaoSessionService.Session session = sessions.find(TaobaoSessionService.bearerToken(request));
if (session == null) {
return ApiResponse.ok(TaobaoEnvelope.fail(401, SESSION_EXPIRED));
}
Map<String, Object> data = TaobaoEnvelope.ok(200);
data.put("user", user(session.userId()));
return ApiResponse.ok(data);
}
@PostMapping(value = "/logout", consumes = MediaType.ALL_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponse<Map<String, Object>> logout(HttpServletRequest request) {
sessions.revoke(TaobaoSessionService.bearerToken(request));
return ApiResponse.ok(TaobaoEnvelope.ok(200));
}
private Map<String, Object> user(String userId) {
Map<String, Object> user = new LinkedHashMap<>();
user.put("id", userId);
user.put("kind", "taobao");
return user;
}
private Map<String, Object> parseBody(String raw) {
if (raw == null || raw.isBlank()) {
return Map.of();
}
try {
return objectMapper.readValue(raw, new TypeReference<Map<String, Object>>() {
});
} catch (Exception e) {
return Map.of();
}
}
}
@@ -0,0 +1,33 @@
package com.juzi.mvp.api;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 淘宝小程序客户端读取的 data 结构约定。
*
* <p>客户端(taobao/utils/api.js:13-16)只认 data.ok 和 data.http_status:
* HTTP 状态码永远是 200,逻辑状态(202/400/401/404/409/413/429)放在 data.http_status 里。
* 失败时 message 就是 toast 文案,status 401 还会触发客户端自动登出。
* 这与参考实现 h5/lib/taobao-routes.js:8 保持一致。
*/
public final class TaobaoEnvelope {
private TaobaoEnvelope() {
}
public static Map<String, Object> ok(int httpStatus) {
Map<String, Object> data = new LinkedHashMap<>();
data.put("ok", true);
data.put("http_status", httpStatus);
return data;
}
public static Map<String, Object> fail(int httpStatus, String message) {
Map<String, Object> data = new LinkedHashMap<>();
data.put("ok", false);
data.put("http_status", httpStatus);
data.put("message", message);
return data;
}
}
@@ -0,0 +1,117 @@
package com.juzi.mvp.api;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
/**
* taobao.mode=forward 时启用:把 /api/taobao/* 业务接口字节级转发给外层 H5 后端(Node)。
*
* <p>为什么必须字节级透传:对端用「原始 query string + 原始 body」验淘宝签名
* (h5/lib/taobao-auth.js:16-29)。Java 一旦重新序列化 JSON 或重排 query,
* 签名字节就变了,session 会直接 401。所以这里不读 body 语义、不动 JSON。
*
* <p>/ping、/whoami、/echo 仍然由 {@link MvpController} 自己处理(更具体的映射优先)。
*/
@RestController
@ConditionalOnProperty(name = "taobao.mode", havingValue = "forward")
public class TaobaoForwardController {
private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(3);
/** 云网关单请求 10 秒上限,留出余量。 */
private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(9);
private final HttpClient client = HttpClient.newBuilder()
.connectTimeout(CONNECT_TIMEOUT)
.followRedirects(HttpClient.Redirect.NEVER)
.build();
private final String forwardBase;
public TaobaoForwardController(@Value("${taobao.forward-base}") String forwardBase) {
this.forwardBase = forwardBase.endsWith("/")
? forwardBase.substring(0, forwardBase.length() - 1)
: forwardBase;
}
@RequestMapping("/api/taobao/{*path}")
public ResponseEntity<byte[]> forward(HttpServletRequest request) {
byte[] body = readBody(request);
String target = forwardBase + request.getRequestURI()
+ (request.getQueryString() == null ? "" : "?" + request.getQueryString());
HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(target))
.timeout(REQUEST_TIMEOUT)
.method(request.getMethod(), body.length == 0
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofByteArray(body));
copyHeader(request, builder, "Content-Type");
copyHeader(request, builder, "Authorization");
HttpResponse<byte[]> response;
try {
response = client.send(builder.build(), HttpResponse.BodyHandlers.ofByteArray());
} catch (IOException e) {
return envelope(502, "测肤服务连接失败");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return envelope(502, "测肤服务被中断");
}
// 对端正常返回时已经是客户端要的信封(h5/lib/taobao-routes.js:8),原样回传。
if (looksLikeEnvelope(response.body())) {
return ResponseEntity.status(response.statusCode())
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8")
.header(HttpHeaders.CACHE_CONTROL, "no-store")
.body(response.body());
}
// 对端没部署 / 返回 404 页面 / 网关错误:包成统一信封,避免小程序 JSON.parse 失败。
return envelope(502, "测肤服务暂不可用(上游 " + response.statusCode() + ")");
}
private void copyHeader(HttpServletRequest request, HttpRequest.Builder builder, String name) {
String value = request.getHeader(name);
if (value != null && !value.isBlank()) {
builder.header(name, value);
}
}
private static byte[] readBody(HttpServletRequest request) {
try {
return request.getInputStream().readAllBytes();
} catch (IOException e) {
return new byte[0];
}
}
private static boolean looksLikeEnvelope(byte[] body) {
if (body == null || body.length == 0) {
return false;
}
String text = new String(body, StandardCharsets.UTF_8).stripLeading();
return text.startsWith("{") && text.contains("\"success\"");
}
private static ResponseEntity<byte[]> envelope(int status, String message) {
String json = "{\"success\":true,\"data\":{\"ok\":false,\"http_status\":" + status
+ ",\"message\":\"" + message + "\"}}";
return ResponseEntity.status(200)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8")
.header(HttpHeaders.CACHE_CONTROL, "no-store")
.body(json.getBytes(StandardCharsets.UTF_8));
}
}
@@ -0,0 +1,154 @@
package com.juzi.mvp.api;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.HexFormat;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 淘宝小程序会话:一次性的 challenge + 30 天有效的 Bearer token。
*
* <p>参考实现 h5/lib/taobao-session.js 的取值:challenge 60 秒、单次消费、
* 每来源 30 次/分钟;token 32 字节随机十六进制、有效期 30 天。
*
* <p>注意:全部状态都在进程内存里,SAE 只能单副本运行;多副本/发布不丢会话
* 需要换 Redis 或 RDS。这一点和现有 Node 实现一致,不是新增的退化。
*/
@Component
public class TaobaoSessionService {
private static final long CHALLENGE_TTL_SECONDS = 60;
private static final long TOKEN_TTL_SECONDS = 30L * 24 * 60 * 60;
private static final int MAX_OUTSTANDING_CHALLENGES = 1000;
private static final int RATE_LIMIT_PER_MINUTE = 30;
private final SecureRandom random = new SecureRandom();
private final Map<String, Instant> challenges = new ConcurrentHashMap<>();
private final Map<String, Session> sessions = new ConcurrentHashMap<>();
private final Map<String, Rate> rates = new ConcurrentHashMap<>();
@Value("${taobao.app-key:}")
private String appKey;
private record Rate(Instant window, int count) {
}
public record Session(String userId, String openId, Instant expires) {
}
public record Token(String value, long expiresInSeconds, String userId) {
}
/**
* 限流计数用的来源标识。
*
* <p>云网关后面所有请求的来源 IP 都是同一个代理 IP,按 IP 计数会变成全站共享
* 30 次/分钟,所以在有 open_id 时优先按用户计数。
*/
public String rateKey(HttpServletRequest request) {
String openId = request.getParameter("open_id");
return openId != null && !openId.isBlank() ? "open:" + openId : "ip:" + request.getRemoteAddr();
}
/**
* 发放一次性 challenge;超出频率或存量上限时返回 null(调用方回 429)。
*/
public String challenge(String rateKey) {
sweep();
Instant now = Instant.now();
Rate rate = rates.compute(rateKey, (key, current) ->
current == null || !current.window().plusSeconds(CHALLENGE_TTL_SECONDS).isAfter(now)
? new Rate(now, 1)
: new Rate(current.window(), current.count() + 1));
if (rate.count() > RATE_LIMIT_PER_MINUTE || challenges.size() >= MAX_OUTSTANDING_CHALLENGES) {
return null;
}
String id = randomHex(24);
challenges.put(id, now.plusSeconds(CHALLENGE_TTL_SECONDS));
return id;
}
/**
* 消费 challenge;remove 返回非空即视为抢到,天然只成功一次。
*/
public boolean consume(String challenge) {
if (challenge == null || challenge.isBlank()) {
return false;
}
Instant expires = challenges.remove(challenge);
return expires != null && expires.isAfter(Instant.now());
}
public Token issue(String openId) {
String userId = userId(openId);
String value = randomHex(32);
sessions.put(value, new Session(userId, openId, Instant.now().plusSeconds(TOKEN_TTL_SECONDS)));
return new Token(value, TOKEN_TTL_SECONDS, userId);
}
public Session find(String token) {
if (token == null || token.isBlank()) {
return null;
}
Session session = sessions.get(token);
if (session == null) {
return null;
}
if (!session.expires().isAfter(Instant.now())) {
sessions.remove(token);
return null;
}
return session;
}
public void revoke(String token) {
if (token != null) {
sessions.remove(token);
}
}
/**
* 稳定的用户 id:sha256(app_key + ':' + open_id),与 Node 端 provider_key 口径一致。
*/
public String userId(String openId) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
String seed = (appKey == null ? "" : appKey) + ":" + openId;
return HexFormat.of().formatHex(digest.digest(seed.getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
throw new IllegalStateException("SHA-256 unavailable", e);
}
}
/** 从 Authorization: Bearer xxx 里取 token。 */
public static String bearerToken(HttpServletRequest request) {
String header = request.getHeader("Authorization");
if (header == null) {
return null;
}
String prefix = "Bearer ";
return header.regionMatches(true, 0, prefix, 0, prefix.length())
? header.substring(prefix.length()).trim()
: null;
}
private void sweep() {
Instant now = Instant.now();
challenges.entrySet().removeIf(entry -> !entry.getValue().isAfter(now));
sessions.entrySet().removeIf(entry -> !entry.getValue().expires().isAfter(now));
rates.entrySet().removeIf(entry -> !entry.getValue().window().plusSeconds(CHALLENGE_TTL_SECONDS).isAfter(now));
}
private String randomHex(int bytes) {
byte[] buffer = new byte[bytes];
random.nextBytes(buffer);
return HexFormat.of().formatHex(buffer);
}
}
@@ -0,0 +1,21 @@
package com.juzi.mvp.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 仅用于本地浏览器/curl 调试。小程序原生请求不受 CORS 限制。
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "OPTIONS")
.allowedHeaders("*")
.maxAge(3600);
}
}
@@ -0,0 +1,126 @@
package com.juzi.mvp.gateway;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* 淘宝云网关签名校验的最小实现。
*
* 规则(与 h5/lib/taobao-auth.js 一致):
* 1. 取全部 query 参数,去掉 sign 和空值,按 key 升序,拼成 key1value1key2value2...
* 2. 末尾追加原始请求体(未经重新序列化的 JSON 字符串)
* 3. sign_method=md5 时:MD5(secret + content + secret)
* sign_method 为空时:MD5(content + secret)
* hmac/hmac_md5:HMAC-MD5(secret, content)
* hmac_sha256:HMAC-SHA256(secret, content)
* 4. 结果转大写十六进制,与 sign 比较
*
* 本地直连没有 app_key/sign,默认 verify-sign=false 直接放行;
* 云端部署时设置 TAOBAO_VERIFY_SIGN=true 和 TAOBAO_APP_KEY/TAOBAO_APP_SECRET。
*/
@Component
public class TaobaoSignatureVerifier {
@Value("${taobao.app-key:}")
private String appKey;
@Value("${taobao.app-secret:}")
private String appSecret;
@Value("${taobao.verify-sign:false}")
private boolean verifySign;
public record Result(boolean accepted, boolean verified, String message) {
}
public Result verify(HttpServletRequest request, String rawBody) {
if (!verifySign) {
return new Result(true, false, "verify-sign=false, skipped (local/direct mode)");
}
if (appKey == null || appKey.isBlank() || appSecret == null || appSecret.isBlank()) {
return new Result(false, false, "TAOBAO_APP_KEY / TAOBAO_APP_SECRET is not configured");
}
Map<String, String> params = new TreeMap<>();
for (Map.Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
String[] values = entry.getValue();
if (values == null || values.length == 0) {
continue;
}
if (values.length > 1) {
return new Result(false, false, "duplicate parameter: " + entry.getKey());
}
params.put(entry.getKey(), values[0]);
}
if (!appKey.equals(params.get("app_key"))) {
return new Result(false, false, "app_key mismatch");
}
String openId = params.getOrDefault("open_id", "");
if (!openId.matches("[A-Za-z0-9_-]{4,256}")) {
return new Result(false, false, "open_id missing or invalid");
}
String sign = params.getOrDefault("sign", "");
if (!sign.matches("[A-Fa-f0-9]{32,64}")) {
return new Result(false, false, "sign missing or invalid");
}
try {
String expected = sign(params, rawBody == null ? "" : rawBody, appSecret);
if (expected == null || expected.length() != sign.length()) {
return new Result(false, false, "unsupported sign_method");
}
boolean equal = MessageDigest.isEqual(
expected.getBytes(StandardCharsets.UTF_8),
sign.toUpperCase(Locale.ROOT).getBytes(StandardCharsets.UTF_8));
return equal ? new Result(true, true, "ok") : new Result(false, false, "signature mismatch");
} catch (Exception e) {
return new Result(false, false, "signature error: " + e.getMessage());
}
}
public static String sign(Map<String, String> params, String body, String secret) throws Exception {
// 真实网关请求不带 sign_method,缺省等价于 md5:
// MD5(secret + content + secret)。2026-11-01 用抓包参数离线复算确认。
String rawMethod = params.getOrDefault("sign_method", "");
String method = rawMethod.isEmpty() ? "md5" : rawMethod;
if (!Set.of("md5", "hmac", "hmac_md5", "hmac_sha256").contains(method)) {
return null;
}
StringBuilder content = new StringBuilder();
params.entrySet().stream()
.filter(e -> !"sign".equals(e.getKey()))
.filter(e -> e.getValue() != null && !e.getValue().isEmpty())
.sorted(Map.Entry.comparingByKey())
.forEach(e -> content.append(e.getKey()).append(e.getValue()));
content.append(body == null ? "" : body);
if ("hmac".equals(method) || "hmac_md5".equals(method) || "hmac_sha256".equals(method)) {
String algorithm = "hmac_sha256".equals(method) ? "HmacSHA256" : "HmacMD5";
Mac mac = Mac.getInstance(algorithm);
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), algorithm));
return hex(mac.doFinal(content.toString().getBytes(StandardCharsets.UTF_8)));
}
String plain = secret + content + secret;
return hex(MessageDigest.getInstance("MD5").digest(plain.getBytes(StandardCharsets.UTF_8)));
}
private static String hex(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(String.format("%02X", b & 0xFF));
}
return sb.toString();
}
}
@@ -0,0 +1,16 @@
# Temporary validation only. This file is gitignored and must not be committed.
# It is baked into the JAR so SAE does not need TAOBAO_* environment variables.
# For the first end-to-end validation, signature verification is disabled because
# the current AppSecret is still suspect. Enable it only after resetting the
# AppSecret for AppKey 35486228 and updating the value below.
taobao:
app-key: "35486228"
app-secret: "b8bd05ebce2f8a34d28026b443ba3ed6"
verify-sign: false
# 淘宝小程序的真实测肤仍在 1me.xajuzi.com 那套 Node 服务上(旷视调用、评分、报告都在那边)。
# SAE 这边只做云网关适配层:把 /api/taobao/* 字节级透传过去。
# forward-base 对应 nginx 里的 location /tb/(只允许聚石塔 NAT 出口 IP 访问)。
mode: forward
forward-base: "https://1me.xajuzi.com/tb"
@@ -0,0 +1,35 @@
server:
port: 8080
spring:
application:
name: taobao-mvp-backend
profiles:
active: local
jackson:
time-zone: Asia/Shanghai
default-property-inclusion: non_null
management:
endpoints:
web:
exposure:
include: health,info
endpoint:
health:
show-details: always
taobao:
# forward: /api/taobao/* 业务接口字节级透传给外层 H5 后端(Node),Java 只做网关适配层
# native: Java 自己实现(challenge/session/me/logout);测肤接口尚未实现
mode: ${TAOBAO_MODE:native}
forward-base: ${TAOBAO_FORWARD_BASE:https://1me.xajuzi.com/state}
# 云端部署时通过 SAE 环境变量注入;本地留空即可。
app-key: ${TAOBAO_APP_KEY:}
app-secret: ${TAOBAO_APP_SECRET:}
# 本地直连没有网关签名,保持 false;云端改成 true。
verify-sign: ${TAOBAO_VERIFY_SIGN:false}
logging:
level:
com.juzi.mvp: INFO
@@ -0,0 +1,62 @@
package com.juzi.mvp;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class MvpApplicationTests {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate rest;
private String url(String path) {
return "http://127.0.0.1:" + port + path;
}
@Test
@SuppressWarnings("unchecked")
void pingReturnsCloudGatewayEnvelope() {
ResponseEntity<Map> response = rest.postForEntity(url("/api/taobao/ping"), Map.of(), Map.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).containsEntry("success", true);
Map<String, Object> data = (Map<String, Object>) response.getBody().get("data");
assertThat(data).containsEntry("server", "java").containsEntry("pong", true);
}
@Test
@SuppressWarnings("unchecked")
void echoReturnsRequestBody() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
ResponseEntity<Map> response = rest.postForEntity(
url("/api/taobao/echo"),
new HttpEntity<>("{\"hello\":\"world\"}", headers),
Map.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
Map<String, Object> data = (Map<String, Object>) response.getBody().get("data");
Map<String, Object> body = (Map<String, Object>) data.get("body");
assertThat(body).containsEntry("hello", "world");
}
@Test
void actuatorHealthIsUp() {
ResponseEntity<String> response = rest.getForEntity(url("/actuator/health"), String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).contains("\"status\":\"UP\"");
}
}
@@ -0,0 +1,139 @@
package com.juzi.mvp;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
/**
* 登录链路(challenge → session → me → logout)的端到端契约测试。
*
* <p>本地/测试环境 verify-sign=false,所以用 ?open_id= 模拟云网关注入的身份;
* 打开 verify-sign 后,同样的请求需要带正确的 sign。
*/
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
// 部署用的 application-local.yml 里写的是 mode: forward(生产走透传)。
// 这个用例测的是 Java 自己实现登录的那条路,所以显式钉住 native。
properties = "taobao.mode=native")
class TaobaoAuthFlowTests {
private static final String GATEWAY_QUERY =
"?app_key=35486228&open_id=AAG8-test-open-id&sign_method=hmac_sha256";
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate rest;
private String url(String path) {
return "http://127.0.0.1:" + port + path;
}
private HttpEntity<String> json(String body) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return new HttpEntity<>(body, headers);
}
private HttpEntity<String> json(String body, String token) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
if (token != null) {
headers.set("Authorization", "Bearer " + token);
}
return new HttpEntity<>(body, headers);
}
@SuppressWarnings("unchecked")
private Map<String, Object> data(ResponseEntity<Map> response) {
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).containsEntry("success", true);
return (Map<String, Object>) response.getBody().get("data");
}
@SuppressWarnings("unchecked")
private String login() {
Map<String, Object> challenge = data(rest.postForEntity(
url("/api/taobao/challenge" + GATEWAY_QUERY), json("{}"), Map.class));
assertThat(challenge).containsEntry("ok", true).containsEntry("http_status", 200);
String challengeId = (String) challenge.get("challenge");
assertThat(challengeId).hasSize(48);
Map<String, Object> session = data(rest.postForEntity(
url("/api/taobao/session" + GATEWAY_QUERY),
json("{\"consent\":true,\"challenge\":\"" + challengeId + "\"}"),
Map.class));
assertThat(session).containsEntry("ok", true).containsEntry("http_status", 200);
assertThat((String) session.get("token")).hasSize(64);
assertThat(((Number) session.get("expires_in")).longValue()).isEqualTo(2592000L);
Map<String, Object> user = (Map<String, Object>) session.get("user");
assertThat(user).containsEntry("kind", "taobao");
assertThat((String) user.get("id")).hasSize(64);
return (String) session.get("token");
}
@Test
@SuppressWarnings("unchecked")
void challengeSessionMeLogoutRoundTrip() {
String token = login();
Map<String, Object> me = data(rest.postForEntity(
url("/api/taobao/me"), json("{}", token), Map.class));
assertThat(me).containsEntry("ok", true);
assertThat(((Map<String, Object>) me.get("user"))).containsEntry("kind", "taobao");
assertThat(data(rest.postForEntity(
url("/api/taobao/logout"), json("{}", token), Map.class))).containsEntry("ok", true);
Map<String, Object> afterLogout = data(rest.postForEntity(
url("/api/taobao/me"), json("{}", token), Map.class));
assertThat(afterLogout).containsEntry("ok", false).containsEntry("http_status", 401);
}
@Test
void sessionRejectsMissingConsent() {
Map<String, Object> challenge = data(rest.postForEntity(
url("/api/taobao/challenge" + GATEWAY_QUERY), json("{}"), Map.class));
Map<String, Object> session = data(rest.postForEntity(
url("/api/taobao/session" + GATEWAY_QUERY),
json("{\"consent\":false,\"challenge\":\"" + challenge.get("challenge") + "\"}"),
Map.class));
assertThat(session).containsEntry("ok", false).containsEntry("http_status", 400);
assertThat(session.get("message")).isEqualTo("请先阅读并同意隐私协议");
}
@Test
void challengeCannotBeReused() {
Map<String, Object> challenge = data(rest.postForEntity(
url("/api/taobao/challenge" + GATEWAY_QUERY), json("{}"), Map.class));
String body = "{\"consent\":true,\"challenge\":\"" + challenge.get("challenge") + "\"}";
assertThat(data(rest.postForEntity(url("/api/taobao/session" + GATEWAY_QUERY), json(body), Map.class)))
.containsEntry("ok", true);
Map<String, Object> replay = data(rest.postForEntity(
url("/api/taobao/session" + GATEWAY_QUERY), json(body), Map.class));
assertThat(replay).containsEntry("ok", false).containsEntry("http_status", 401);
}
@Test
void meWithoutTokenIsUnauthorized() {
Map<String, Object> me = data(rest.postForEntity(
url("/api/taobao/me"), json("{}"), Map.class));
assertThat(me).containsEntry("ok", false).containsEntry("http_status", 401);
assertThat(me.get("message")).isEqualTo("检测会话已失效,请重新登录");
}
}
@@ -0,0 +1,123 @@
package com.juzi.mvp;
import com.sun.net.httpserver.HttpServer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
/**
* taobao.mode=forward 时,Java 必须把请求「字节级」转发给外层 H5 后端:
* 原始 query string、原始 body、Authorization 都要原样过去,回来的信封也要原样返回。
*
* <p>用一个本地 stub 冒充 Node 后端来验证这件事。
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class TaobaoForwardTests {
private static final HttpServer STUB;
private static volatile String lastQuery;
private static volatile String lastBody;
private static volatile String lastAuthorization;
private static volatile String lastPath;
static {
try {
STUB = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
STUB.createContext("/api/taobao/", exchange -> {
lastPath = exchange.getRequestURI().getPath();
lastQuery = exchange.getRequestURI().getRawQuery();
lastAuthorization = exchange.getRequestHeaders().getFirst("Authorization");
try (InputStream in = exchange.getRequestBody()) {
lastBody = new String(in.readAllBytes(), StandardCharsets.UTF_8);
}
byte[] payload = ("{\"success\":true,\"data\":{\"ok\":true,\"http_status\":200,"
+ "\"echo_path\":\"" + lastPath + "\","
+ "\"echo_query\":\"" + lastQuery + "\","
+ "\"echo_body\":" + (lastBody.isEmpty() ? "\"\"" : lastBody) + "}}")
.getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().add("Content-Type", "application/json;charset=utf-8");
exchange.sendResponseHeaders(200, payload.length);
exchange.getResponseBody().write(payload);
exchange.close();
});
STUB.start();
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
@DynamicPropertySource
static void properties(DynamicPropertyRegistry registry) {
registry.add("taobao.mode", () -> "forward");
registry.add("taobao.forward-base", () -> "http://127.0.0.1:" + STUB.getAddress().getPort());
}
@AfterAll
static void stopStub() {
STUB.stop(0);
}
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate rest;
private String url(String path) {
return "http://127.0.0.1:" + port + path;
}
@Test
@SuppressWarnings("unchecked")
void forwardsBodyQueryAndAuthorizationVerbatim() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer abc123");
// 故意带空格和不常见字段顺序:任何重新序列化都会改变这里的字节
String body = "{\"consent\":true, \"challenge\":\"x-1\", \"note\":\"带空格\"}";
ResponseEntity<Map> response = rest.postForEntity(
url("/api/taobao/session?app_key=35486228&open_id=op-x&sign_method=hmac_sha256&sign=ABC"),
new HttpEntity<>(body, headers),
Map.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).containsEntry("success", true);
assertThat(lastPath).isEqualTo("/api/taobao/session");
assertThat(lastQuery).isEqualTo("app_key=35486228&open_id=op-x&sign_method=hmac_sha256&sign=ABC");
assertThat(lastBody).isEqualTo(body);
assertThat(lastAuthorization).isEqualTo("Bearer abc123");
Map<String, Object> data = (Map<String, Object>) response.getBody().get("data");
assertThat(data).containsEntry("ok", true).containsEntry("echo_path", "/api/taobao/session");
}
@Test
void localDiagnosticsEndpointsStayLocal() {
ResponseEntity<Map> ping = rest.postForEntity(url("/api/taobao/ping"), Map.of(), Map.class);
assertThat(ping.getStatusCode()).isEqualTo(HttpStatus.OK);
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) ping.getBody().get("data");
assertThat(data).containsEntry("server", "java");
assertThat(lastPath).isNotEqualTo("/api/taobao/ping");
}
}
@@ -0,0 +1,40 @@
package com.juzi.mvp.gateway;
import org.junit.jupiter.api.Test;
import java.util.Map;
import java.util.TreeMap;
import static org.assertj.core.api.Assertions.assertThat;
class TaobaoSignatureVerifierTest {
@Test
void md5SignatureMatchesExistingNodeImplementation() throws Exception {
Map<String, String> params = new TreeMap<>();
params.put("app_key", "app");
params.put("open_id", "open-account");
params.put("sign_method", "md5");
params.put("env", "online");
String body = "{\"consent\":true,\"challenge\":\"nonce\"}";
// 由 taobao/test/runtime.test.cjs 的同一组参数计算得出。
assertThat(TaobaoSignatureVerifier.sign(params, body, "secret"))
.isEqualTo("E673310E672C9EB7E918857C6C64D247");
}
@Test
void missingSignMethodUsesTheMd5Form() throws Exception {
// 真实网关请求不带 sign_method(2026-11-01 抓包确认),缺省等价于 md5(secret+content+secret)。
Map<String, String> params = new TreeMap<>();
params.put("app_key", "app");
params.put("open_id", "open-account");
params.put("env", "online");
String body = "{\"consent\":true,\"challenge\":\"nonce\"}";
assertThat(TaobaoSignatureVerifier.sign(params, body, "secret"))
.isEqualTo("DD2B95886A5E8F61D6111A5659E4FBD5");
}
}
@@ -0,0 +1 @@
https://1me.xajuzi.com/media/videos/261026-shuangcang5-mobile-v2.html
@@ -0,0 +1 @@
https://1me.xajuzi.com/media/videos/261026-shuangcang5-46ae3089.html
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1 @@
实际淘宝 IDE 模拟器运行截图(非真机截图)。login-centered.png 从完整 IDE 窗口截图中裁取模拟器屏幕,未修改页面内容。
Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

@@ -0,0 +1,11 @@
# 报告侧脸大图对照
- mini-contour.png:当前小程序轮廓大图的浏览器投影。
- mini-wrinkles.png:当前小程序皱纹大图的浏览器投影。
- h5-contour.png:同一项目示例照片经 H5 reportZoomSource 实际函数生成的大图。
这些是使用项目示例照片及实际页面代码生成的浏览器预览,并非淘宝真机截图,也不能作为提审运行截图。原生图片组件最终显示效果仍需手机验证。
当前实现将 H5 裁切规则通过报告专用 canvas 生成独立位图,放大层只对位图 aspectFit,不再使用原生图片组件模拟裁切。预览脚本执行实际 report-side-zoom 模块的绘制命令。导出失败时等比显示完整原照片。
最新调整:按用户要求,mini-contour.png 和 mini-wrinkles.png 图片填充宽高均缩小至前一版的 80%,居中后向下移动缩放后图片高度的 5%;h5-contour.png 保留 H5 原规则作为历史对照,因此不再代表两者应当完全相同。
@@ -0,0 +1,5 @@
同一张项目示例照片的浏览器对照预览,不是淘宝真机运行截图,不用于冒充提审截图。
mini-contour.png:真实小程序报告放大图 AXML/ACSS 与控制器数据的浏览器投影。
mini-wrinkles.png:皱纹视图同一放大规则的浏览器投影(示例数据)。
h5-contour.png:使用线上 H5 reportZoomSource 原始函数及本地对应 CSS 生成的对照。
修复:不再在尺寸获取失败时按 3:4 拉伸;复用已读取的自然尺寸,未知尺寸使用 aspectFit,图片 onLoad 后补齐真实几何。
Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

@@ -0,0 +1,4 @@
2 张淘宝 IDE 模拟器实际运行截图,未使用 H5 投影、Mock 报告或重绘界面。仅裁去 IDE 外框,未修改界面内容。
01:登录页。02:未勾选协议点击游客体验后,真实显示“请先阅读并同意用户协议和隐私政策”。
格式 PNG,单张 239×423,均小于 4MB,满足用户提供的最大尺寸 2160×3840。
后续主页与档案运行截图需要先手动完成模拟器中的淘宝登录验证。
Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

+148
View File
@@ -0,0 +1,148 @@
# deploy —— 1me.xajuzi.com 上的「淘宝专用」Node 实例
这份目录存放**不在 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 上限)
→ 聚石塔云应用 1me-net(62829) → 私网 CLB 192.168.65.185:80
→ SAE 应用 nnn(taobao-mvp-backend-0.0.4-forward.jar,mode=forward,纯透传)
→ 聚石塔 NAT 网关出网(EIP 47.92.113.176)
→ https://1me.xajuzi.com/tb/ (nginx 白名单只放行上面这个 EIP)
→ juzi-taobao.service(Node,127.0.0.1:18081,本文档这套配置)
→ 旷视 Face++ skinanalyze_pro
```
生产 H5(`/state/`)和红书渠道(`/rednote/`)跑在同机另外两个 systemd 服务上,**8080/8081 端口,和本实例完全隔离**。
## 三个服务的分工
| 服务 | 端口 | 代码目录 | 用途 |
|---|---|---|---|
| `juzi-h5.service` | 8080 | `/home/ubuntu/juzi/h5` | 生产 H5(微信登录、报告、后台) |
| `juzi-rednote.service` | 8081 | `/home/ubuntu/juzi-rednote/current/h5` | 小红书渠道隔离实例 |
| `juzi-taobao.service` | 18081 | `/home/ubuntu/juzi-tb/h5` | **本目录新增**:淘宝小程序专用 |
## 重建步骤
### 1. 代码
`/home/ubuntu/juzi-tb` 是**独立的一份 h5 代码**(不是生产目录的软链),来自本仓库的 `h5/`。
部署方式(本地执行,把仓库里的 h5 打包传上去):
```powershell
# 本地仓库根目录
tar -czf temp/juzi-h5.tgz --exclude=h5/data --exclude=h5/test --exclude=h5/scripts -C . h5
scp temp/juzi-h5.tgz ubuntu@1me.xajuzi.com:/tmp/
```
```bash
# 服务器上
sudo mkdir -p /home/ubuntu/juzi-tb /home/ubuntu/juzi-tb-data
sudo chown -R ubuntu:ubuntu /home/ubuntu/juzi-tb /home/ubuntu/juzi-tb-data
chmod 700 /home/ubuntu/juzi-tb-data
tar -xzf /tmp/juzi-h5.tgz -C /home/ubuntu/juzi-tb
# 旷视凭据复用生产那份(gitignored,不随仓库走)
mkdir -p /home/ubuntu/juzi-tb/reference/facepp-skinanalyze
cp /home/ubuntu/juzi/reference/facepp-skinanalyze/credentials.env \
/home/ubuntu/juzi-tb/reference/facepp-skinanalyze/credentials.env
chmod 600 /home/ubuntu/juzi-tb/reference/facepp-skinanalyze/credentials.env
```
> `h5/server.js` 启动时如果找不到旷视 key 会直接 `exit(1)`,所以这一步不能漏。
### 2. 淘宝凭据
```bash
sudo mkdir -p /etc/juzi
sudo tee /etc/juzi/taobao.env >/dev/null <<'EOF'
TAOBAO_APP_KEY=35486228
TAOBAO_APP_SECRET=<35486228 的 AppSecret,向项目负责人索取>
EOF
sudo chmod 600 /etc/juzi/taobao.env && sudo chown root:root /etc/juzi/taobao.env
```
这个文件被 `h5/lib/taobao-auth.js` 读取,用于校验云网关签名。**没有它,`/session` 一律 401。**
### 3. systemd
```bash
scp deploy/systemd/juzi-taobao.service ubuntu@1me.xajuzi.com:/tmp/
ssh ubuntu@1me.xajuzi.com "sudo install -m 644 -o root -g root /tmp/juzi-taobao.service /etc/systemd/system/ \
&& sudo systemctl daemon-reload && sudo systemctl enable --now juzi-taobao && systemctl is-active juzi-taobao"
```
### 4. nginx
```bash
# 本地
scp deploy/add-tb-nginx.sh deploy/nginx/tb-location.conf ubuntu@1me.xajuzi.com:/tmp/
# 服务器
ssh ubuntu@1me.xajuzi.com "sed -i 's/\r\$//' /tmp/add-tb-nginx.sh && bash /tmp/add-tb-nginx.sh /tmp/tb-location.conf"
```
脚本是幂等的(检测到 `location /tb/` 就跳过),会先备份成 `juzi.bak-tb-<时间戳>`。
### 5. 验收
```bash
# 服务器本机(127.0.0.1 在白名单里)
curl -sk -H 'Host: 1me.xajuzi.com' https://127.0.0.1/tb/api/health
# 期望 {"ok":true}
# 公网(应被白名单挡掉)
curl -s -o /dev/null -w '%{http_code}\n' https://1me.xajuzi.com/tb/api/health
# 期望 403
# 生产 H5 不受影响
curl -s -o /dev/null -w '%{http_code}\n' https://1me.xajuzi.com/state/api/health
# 期望 200
```
最后在聚石塔 SAE 容器里再验一次(这是唯一走 NAT 的路径):
```bash
curl -s https://1me.xajuzi.com/tb/api/health # 期望 {"ok":true}
```
## 回滚
```bash
sudo systemctl disable --now juzi-taobao
sudo cp /etc/nginx/sites-available/juzi.bak-tb-<时间戳> /etc/nginx/sites-available/juzi
sudo nginx -t && sudo systemctl reload nginx
```
生产两个服务完全不受影响(端口、目录、数据都是独立的)。
## 数据
- 运行数据:`/home/ubuntu/juzi-tb-data`(700)—— 照片 `photos/`、原始返回 `results/`、记录 `records.jsonl`、账号 `accounts/`
- 与生产的 `/home/ubuntu/juzi/h5/data` 完全分开,两边用户/报告不互通
- **没有任何自动清理**:每次检测约 10–12MB 常驻磁盘,长期要换 OSS + RDS
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash
# 幂等地把 deploy/nginx/tb-location.conf 插入 /etc/nginx/sites-available/juzi。
# 只做「插入一段 location」,不改动任何已有 location;先备份,nginx -t 通过才 reload。
#
# 用法(在 1me.xajuzi.com 上):
# scp deploy/add-tb-nginx.sh deploy/nginx/tb-location.conf ubuntu@1me.xajuzi.com:/tmp/
# ssh ubuntu@1me.xajuzi.com "sed -i 's/\r$//' /tmp/add-tb-nginx.sh && bash /tmp/add-tb-nginx.sh"
set -euo pipefail
CONF=/etc/nginx/sites-available/juzi
SNIPPET=${1:-/tmp/tb-location.conf}
STAMP=$(date +%Y%m%d%H%M%S)
BACKUP="$CONF.bak-tb-$STAMP"
if grep -q 'location /tb/' "$CONF"; then
echo "SKIP: /tb/ 已存在,不重复插入"
exit 0
fi
if [ ! -f "$SNIPPET" ]; then
echo "缺少片段文件: $SNIPPET" >&2
exit 1
fi
sudo cp "$CONF" "$BACKUP"
echo "BACKUP: $BACKUP"
sudo python3 - "$CONF" "$SNIPPET" <<'PY'
import sys
conf_path, snippet_path = sys.argv[1], sys.argv[2]
text = open(conf_path, encoding='utf-8').read()
snippet = open(snippet_path, encoding='utf-8').read()
block = "# BEGIN taobao-debug 2026-11-01\n" + snippet + "# END taobao-debug 2026-11-01\n\n"
marker = " location / {\n"
if marker not in text:
sys.exit("marker 'location / {' not found")
text = text.replace(marker, block + marker, 1)
open(conf_path, 'w', encoding='utf-8').write(text)
print("INSERTED")
PY
echo "--- nginx -t ---"
sudo nginx -t
echo "--- reload ---"
sudo systemctl reload nginx
echo "RELOADED"
+25
View File
@@ -0,0 +1,25 @@
# 插入到 /etc/nginx/sites-available/juzi 的 HTTPS server { } 块内。
# 作用:给淘宝小程序云网关一个专用入口 /tb/,反代到 juzi-taobao.service(127.0.0.1:18081)。
#
# 为什么要 allow 白名单:/tb/ 暴露在公网,后面是真实旷视计费调用。
# 只允许聚石塔 NAT 的出口 EIP(以及本机自测)访问,其余一律 403。
#
# 注意:nginx 会剥掉 /tb/ 前缀,所以 /tb/api/taobao/session → Node 收到 /api/taobao/session,
# 与 h5/server.js 的路由完全一致。
location = /tb {
return 301 /tb/;
}
location /tb/ {
# 聚石塔 NAT 网关绑定的 EIP。换环境时改这一行。
allow 47.92.113.176;
# 本机自测(curl -H 'Host: 1me.xajuzi.com' https://127.0.0.1/tb/api/health)
allow 127.0.0.1;
deny all;
proxy_pass http://127.0.0.1:18081/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 150s;
}
+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()
+29
View File
@@ -0,0 +1,29 @@
[Unit]
Description=1'me taobao mini program backend (Node, /tb/ debug instance)
After=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/juzi-tb/h5
# 独立于生产 juzi-h5(8080)/juzi-rednote(8081):独立端口、独立数据目录、独立凭据文件
Environment=JUZI_ENTRY_MODE=state
Environment=NODE_ENV=production
Environment=HOST=127.0.0.1
Environment=PORT=18081
Environment=JUZI_DATA_ROOT=/home/ubuntu/juzi-tb-data
Environment=TRUST_LOOPBACK_PROXY=1
# 内容格式:
# TAOBAO_APP_KEY=35486228
# TAOBAO_APP_SECRET=<35486228 的 AppSecret>
# 权限 600 root,绝不入库。
EnvironmentFile=/etc/juzi/taobao.env
ExecStart=/usr/bin/node /home/ubuntu/juzi-tb/h5/server.js
Restart=on-failure
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
UMask=0077
[Install]
WantedBy=multi-user.target
+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()
@@ -0,0 +1,59 @@
{
"description": "Smaller product-card delivery images. Original PSD crops preserved.",
"retained": "Background/scan WebPs are smaller than JPEG; transparent icons/masks remain PNG.",
"assets": [
{
"source": "h5/assets/img/ui-20261012/product-25.webp",
"source_sha256": "c18827342d23bbff2de04c3a2ecbeeae9b16eb96f3a9f7e30f2a66cdc21beddc",
"source_bytes": 276778,
"file": "h5/assets/img/ui-20261012/product-25.jpg",
"sha256": "2c399cd6bd7bd783f698a91b9203a7ba42b92295d58bbeb0b1c2a884e862f00b",
"bytes": 63959,
"size": [
504,
503
],
"quality": 90,
"subsampling": 0,
"progressive": true,
"matte": "#FFFFFF",
"psnr_db": 40.98
},
{
"source": "h5/assets/img/ui-20261012/product-35.webp",
"source_sha256": "c1dc455c8072da17ac06cee386248b358947da65bd190b714f2ccb25360afea4",
"source_bytes": 267094,
"file": "h5/assets/img/ui-20261012/product-35.jpg",
"sha256": "ab4c5623235020b1129f564b733fe843c539088e7b62b2d38ed42a204e4af113",
"bytes": 66077,
"size": [
504,
503
],
"quality": 90,
"subsampling": 0,
"progressive": true,
"matte": "#FFFFFF",
"psnr_db": 41.47
},
{
"source": "h5/assets/img/ui-20261012/product-50.webp",
"source_sha256": "86647c8882caf62c43eafb1efe495bad3289126cab73a88c8963b7ad64ffa084",
"source_bytes": 331924,
"file": "h5/assets/img/ui-20261012/product-50.jpg",
"sha256": "78687985751afbf60c374f2605dee372ac64ffc5271aa54ddca7897443de3047",
"bytes": 153787,
"size": [
504,
503
],
"quality": 96,
"subsampling": 0,
"progressive": true,
"matte": "#FFFFFF",
"psnr_db": 41.94
}
],
"source_bytes": 875796,
"delivery_bytes": 283823
}
+501
View File
@@ -0,0 +1,501 @@
{
"source": "UI外发版10.09.psd",
"source_sha256": "c7698f5900adc7476286818450d928e8fa2ce2e58cda09375ff2b861629fd58f",
"document_size": [
7330,
16411
],
"native_header_px": 248,
"design_scale": 2.88,
"assets": [
{
"file": "h5/assets/img/ui-20261012/splash-slogan.png",
"source_layers": [
"0/5",
"0/4"
],
"bbox": [
1415,
391,
2133,
608
],
"size": [
718,
217
],
"bytes": 12781,
"sha256": "c9dee693eb3c1d566bb08d3770e7dd2f0c22e7d4e0b81bbe5e10ad8219b26044"
},
{
"file": "h5/assets/img/ui-20261012/brand-lockup.png",
"source_layers": [
"0/1"
],
"bbox": [
1515,
1861,
2032,
1920
],
"size": [
517,
59
],
"bytes": 7013,
"sha256": "3cb697d6c5542562a9cea20554b0772f2c0137b9c3977ff258908024d5a8e9ed"
},
{
"file": "h5/assets/img/ui-20261012/button-start-label.png",
"source_layers": [
"0/3/1"
],
"bbox": [
1696,
2005,
1857,
2045
],
"size": [
161,
40
],
"bytes": 2163,
"sha256": "fd88b174f120845d8045a78a36bc7665516d91011f7ea0ebc7fe200271e17738"
},
{
"file": "h5/assets/img/ui-20261012/button-history-label.png",
"source_layers": [
"0/2/2/2"
],
"bbox": [
1696,
2252,
1859,
2291
],
"size": [
163,
39
],
"bytes": 2024,
"sha256": "0253d761f63bda89301c1eec9df84352a2f5dd1aa97884e27ebf91092c0816d8"
},
{
"file": "h5/assets/img/ui-20261012/product-25.webp",
"source_layers": [
"8/1/0"
],
"bbox": [
1765,
14677,
2269,
15180
],
"size": [
504,
503
],
"bytes": 276778,
"sha256": "c18827342d23bbff2de04c3a2ecbeeae9b16eb96f3a9f7e30f2a66cdc21beddc"
},
{
"file": "h5/assets/img/ui-20261012/product-35.webp",
"source_layers": [
"8/1/1"
],
"bbox": [
1765,
15228,
2269,
15731
],
"size": [
504,
503
],
"bytes": 267094,
"sha256": "c1dc455c8072da17ac06cee386248b358947da65bd190b714f2ccb25360afea4"
},
{
"file": "h5/assets/img/ui-20261012/product-50.webp",
"source_layers": [
"8/1/2"
],
"bbox": [
1765,
15778,
2269,
16281
],
"size": [
504,
503
],
"bytes": 331924,
"sha256": "86647c8882caf62c43eafb1efe495bad3289126cab73a88c8963b7ad64ffa084"
},
{
"file": "h5/assets/img/ui-20261012/scan-waiting.webp",
"source_layers": [
"9"
],
"bbox": [
6250,
5500,
7330,
7591
],
"size": [
1080,
2091
],
"bytes": 11690,
"sha256": "6b661f3d454e9b696b70ca8846fed11243d3f53b95506c2d78b11b6b1a6d9f77"
}
],
"text_styles": [
{
"path": "0/4",
"text": "以我为始 自有表现",
"bbox": [
1502,
545,
2045,
608
],
"transform": [
0.7386429422796229,
0.0,
-1.0090361144648896e-16,
0.7386429422796231,
1772.6682649161658,
602.0113961671652
],
"runs": [
{
"font": "MiSans-Demibold",
"font_size": 90.25561,
"leading": 104.16666
},
{
"font": "MiSans-Demibold",
"font_size": 90.25561,
"leading": 104.16666
}
]
},
{
"path": "0/5",
"text": " ONE ME,\rFROM THE INSIDE OUT",
"bbox": [
1415,
391,
2133,
520
],
"transform": [
0.7386429422796229,
0.0,
-1.0090361144648896e-16,
0.7386429422796231,
1771.9296219738862,
442.37366417300353
],
"runs": [
{
"font": "MiSans-Regular",
"font_size": 90.25561,
"leading": 104.16666
},
{
"font": "MiSans-Regular",
"font_size": 90.25561,
"leading": 104.16666
}
]
},
{
"path": "0/1/1",
"text": "肌肤状态检测",
"bbox": [
1664,
1862,
2032,
1920
],
"transform": [
1.0,
0.0,
0.0,
0.9999999999999999,
1662.281376176642,
1868.3475610977566
],
"runs": [
{
"font": "MiSans-Demibold",
"font_size": 62.5,
"leading": null
},
{
"font": "MiSans-Demibold",
"font_size": 62.5,
"leading": null
}
]
},
{
"path": "1/3/0",
"text": "欢迎体验1'me\r肌肤状态评估服务!",
"bbox": [
2664,
832,
3218,
979
],
"transform": [
1.0,
0.0,
0.0,
0.9999999999999999,
2662.281376176642,
840.3475610977557
],
"runs": [
{
"font": "MiSans-Demibold",
"font_size": 66.66666,
"leading": 83.33334
},
{
"font": "MiSans-Demibold",
"font_size": 66.66666,
"leading": 83.33334
}
]
},
{
"path": "1/3/1",
"text": "我们将采集您的面部影像,为您生成专属肌肤分析与状态管理方案。所有数据严格保密,仅用于本次检测分析。",
"bbox": [
2663,
1045,
3361,
1177
],
"transform": [
0.9999999999999999,
0.0,
0.0,
1.0,
2662.281376176642,
1049.3475610977548
],
"runs": [
{
"font": "MiSans-Regular",
"font_size": 33.33333,
"leading": 50.0
},
{
"font": "MiSans-Regular",
"font_size": 33.33333,
"leading": 50.0
}
]
},
{
"path": "4/7",
"text": "请拍摄或上传\r正面及左右侧面照片",
"bbox": [
1442,
6160,
2036,
6307
],
"transform": [
1.0,
0.0,
0.0,
0.9999999999999999,
1439.281376176642,
6167.347561097756
],
"runs": [
{
"font": "MiSans-Demibold",
"font_size": 66.66666,
"leading": 83.33334
},
{
"font": "MiSans-Demibold",
"font_size": 66.66666,
"leading": 83.33334
}
]
},
{
"path": "4/8",
"text": "为使测试结果更准确,请关闭美颜/滤镜、并确保光线充足均匀。",
"bbox": [
1443,
6372,
2119,
6454
],
"transform": [
0.9999999999999999,
0.0,
0.0,
1.0,
1442.281376176642,
6376.347561097755
],
"runs": [
{
"font": "MiSans-Regular",
"font_size": 33.33333,
"leading": 50.0
},
{
"font": "MiSans-Regular",
"font_size": 33.33333,
"leading": 50.0
}
]
},
{
"path": "8/1/0/5",
"text": "以我本然有光蓄活\r双仓精华液",
"bbox": [
1362,
14850,
1627,
14932
],
"transform": [
1.7691030436028,
0.0,
0.0,
1.769103043602818,
1361.2813761766417,
14854.052217587123
],
"runs": [
{
"font": "MiSans-Semibold",
"font_size": 18.84194,
"leading": 28.26291
},
{
"font": "MiSans-Semibold",
"font_size": 18.84194,
"leading": 28.26291
}
]
},
{
"path": "8/1/0/6",
"text": "1'me 主场精华 I",
"bbox": [
1363,
14779,
1653,
14820
],
"transform": [
1.5358216667040816,
0.0,
0.0,
1.5358216667040878,
1361.2813761766417,
14784.058678621632
],
"runs": [
{
"font": "MiSans-Semibold",
"font_size": 27.12989,
"leading": 28.26291
},
{
"font": "MiSans-Semibold",
"font_size": 27.12989,
"leading": 28.26291
}
]
}
],
"shape_metrics": [
{
"path": "0/3/0",
"bbox": [
1516,
1971,
2038,
2083
],
"corner_radius_psd_px": 20.000065
},
{
"path": "0/2/0",
"bbox": [
1516,
2104,
2038,
2216
],
"corner_radius_psd_px": 20.000065
},
{
"path": "1/2/0",
"bbox": [
2753,
1410,
3275,
1522
],
"corner_radius_psd_px": 20.000065
},
{
"path": "4/2/0",
"bbox": [
1517,
6604,
2039,
6716
],
"corner_radius_psd_px": 20.000065
},
{
"path": "8/1/0/0",
"bbox": [
1285,
14677,
2269,
15180
],
"corner_radius_psd_px": 15.000158
},
{
"path": "1/1",
"bbox": [
2571,
719,
3458,
1621
],
"corner_radius_psd_px": 24.999972
},
{
"path": "4/1",
"bbox": [
1334,
5971,
2221,
6873
],
"corner_radius_psd_px": 24.999972
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

@@ -0,0 +1,40 @@
{
"source": "new_feedback/第三轮反馈20261012/UI外发版10.09.psd",
"source_sha256": "c7698f5900adc7476286818450d928e8fa2ce2e58cda09375ff2b861629fd58f",
"source_page": "9",
"hidden_scan_layer": "9/2/2",
"viewport": [
6250,
5500,
7330,
7591
],
"face_bbox": [
155,
426,
926,
1417
],
"cycle_ms": 1000,
"yellow": "#F5F078",
"assets": [
{
"file": "h5/assets/img/ui-20261012/scan-base.webp",
"size": [
1080,
2091
],
"bytes": 6688,
"sha256": "08f8e75634691d4329210188bd56472892d8152de0d194c9358f82fda6197ddf"
},
{
"file": "h5/assets/img/ui-20261012/scan-face-mask.png",
"size": [
1080,
2091
],
"bytes": 16417,
"sha256": "d9c729600b6141a3977e38a121f680c20c6b543a6828b1d38cd5f9878d311008"
}
]
}
+273
View File
@@ -0,0 +1,273 @@
# 交接文档:淘宝小程序 + 聚石塔 SAE + Java MVP
> 日期:2026-11-01 凌晨
> 给下一个 agent:先读「一句话状态」和「明天先做什么」,再按需读后面。
## 一句话状态
**云端链路已经打通**:开放平台服务测试 `POST /api/taobao/ping` 返回:
```json
{
"success": true,
"data": {
"server": "java",
"signMessage": "verify-sign=false, skipped (local/direct mode)",
"appKey": "35486228",
"transport": "gateway",
"ok": true,
"pong": true,
"env": "test",
"signVerified": false
},
"errorCode": null,
"errorMessage": null
}
```
SAE `nnn` 上跑的是 `0.0.2` JAR,SAE 环境变量已清空,JAR 内置 `verify-sign:false`,所以 `signVerified:false` 是**预期**。
**还没做** IDE 真机预览;**签名校验尚未启用**(之前的 `signature mismatch` 未解决,先绕过)。
## 明天先做什么
1. **IDE 真机预览**,验证小程序 → 云网关 → 聚石塔云应用 → 私网 CLB → SAE 整条链路(第 5.1 节)。
2. 真机通过后,再决定是否开签名:重置 35486228 的 AppSecret → 更新 `application-local.yml` → `verify-sign:true` → 打 `0.0.3` 上传(第 5.3 节)。
3. 最后提交这轮未提交的代码(**不要提交 `application-local.yml`**)并 push。
## 当前链路
```text
淘宝小程序(taobao/)
→ @tbmp/mp-cloud-sdk(cloudEnv: test)
→ 淘宝云网关(AppKey 35486228)
→ 聚石塔云应用 1me-net(ID 62829,默认云应用)
→ 私网 CLB lb-k2j324dwux1svaf2akwqx / 192.168.65.185:80
→ SAE 应用 nnn(namespace cn-zhangjiakou:my-namespace)
→ Java Spring Boot 0.0.2,容器 8080
```
## 关键资源 / ID
| 资源 | 值 |
|---|---|
| 开放平台应用(当前正确) | `1'me 肌肤状态检测`,AppKey **35486228** |
| 旧应用(不再使用) | `1'me肌肤检测`,AppKey 35479221;旧 Secret 不要再用 |
| 聚石塔云应用 | `1me-net`,ID **62829**,默认应用;生产/测试 SLB 都是 `192.168.65.185:80` |
| 聚石塔云应用(未用) | `app-new`,ID 62861;没接 SAE/CLB,别用 |
| SAE 应用 | `nnn`,ID `7c9f2d2a-95d0-4879-afad-323d30f85d30` |
| SAE 命名空间 | `cn-zhangjiakou:my-namespace` |
| VPC | `vpc-8vb5mcvenp8jwa1roqcsa`(`JST_cn-zhangjiakou_vpc-new`) |
| vSwitch | `vsw-8vbna5zb2rq03gaeiymch`(可用区 A) |
| 安全组 | `sg-8vbh8c96h0kotpu7y26r`(“聚石塔官方创建ssh”),需放行 8080 |
| 实例规格 | 1 Core 2 GiB |
| 代码包 | `taobao-mvp-backend-0.0.2.jar`,版本号 `1793466846250` |
| SAE 环境变量 | **已清空**(重要,见“不要做什么”) |
## 已验证 / 未验证
### 已验证
- 后端本地:`mvn test` 4/4(0.0.1 时;版本改 0.0.2 后只 `-DskipTests` 打包,未重跑)。
- 前端:`npm test` 17/17;`taobao/scripts/mvp-local-check.cjs` 本地直连 Java 通过。
- JAR 本地启动:`/actuator/health` UP,`/api/taobao/ping` 正常。
- SAE `nnn` 部署 `0.0.2` 成功,服务测试返回上面的 JSON。
- `signVerified:false` 是预期;`signMessage` 是固定文案,不代表直连;`transport:"gateway"` 证明走了网关。
### 未验证
- IDE **真机预览**(云应用 SDK 模拟器不支持,必须真机)。
- 开启签名后的 `signVerified:true`。
- 真实测肤接口(challenge/session/photo/skin-analyze/analysis/reports/logout)仍在 Node,Java 只有 `ping/whoami/echo`。
## 代码 / 配置状态
### Git
- 已推送:`ac0b7ca feat(sae): add Java MVP backend and Taobao connectivity page`(origin/main 同步)。
- 工作区未提交改动(11 个文件,`git diff --stat`):
- `.gitignore`(忽略 `application-local.yml`)
- `README.md`(交接文档入口)
- `backend-java/README.md`
- `backend-java/pom.xml`(version `0.0.2`)
- `backend-java/src/main/resources/application.yml`(`spring.profiles.active: local`)
- `docs/SAE-Java-MVP-操作步骤.md`(0.0.2 + 4.3.1 更新 JAR)
- `taobao/config.js`(云端 test)
- `taobao/pages/mvp/mvp.js`(显示 cloudType)
- `taobao/pages/splash/splash.js`(云端 test 也显示 MVP 入口)
- `taobao/test/transport.test.cjs`(tower 测试)
- `taobao/utils/transport.js`(tower 模式)
**`application-local.yml` 在 `.gitignore` 里,未跟踪,不要提交。** 它包含 AppSecret;JAR 里也包含,临时方案。
### 后端
- `backend-java/`,Spring Boot 3.5.6 / Java 17。
- `application.yml`:`spring.profiles.active: local`。
- `application-local.yml`:
- `taobao.app-key: "35486228"`
- `taobao.app-secret: <当前 Secret>`
- `taobao.verify-sign: false`
- JAR:`backend-java/target/taobao-mvp-backend-0.0.2.jar`(23.6MB),包含 `BOOT-INF/classes/application-local.yml`。
- 云端已上传该 JAR;SAE 环境变量已清空(否则覆盖 JAR 配置)。
- 接口:`GET/POST /api/taobao/ping`、`POST /api/taobao/whoami`、`POST /api/taobao/echo`、`GET /actuator/health`。
- 签名校验:`TaobaoSignatureVerifier`,算法与 `h5/lib/taobao-auth.js` 一致;`verify-sign:false` 时跳过。
### 前端
- `taobao/config.js` 当前是云端 test:
- `environment: 'development'`
- `transport: 'cloud'`
- `cloudType: 'tower'`
- `cloudEnv: 'test'`
- `cloudAppId: ''`(默认云应用;若 SDK 报默认不存在,填 `62829`)
- `mvpLocal: false`
- `baseUrl: 'https://miniappcloud.taobao.com'`(tower 模式仅占位,不参与请求)
- `assetBase: 'http://127.0.0.1:18180/assets/'`(真机上底图可能加载失败,不影响 MVP 入口)
- `taobao/utils/transport.js`:`cloudType === 'tower'` 时不传 `domain`、不要求 HTTPS、`cloudAppId` 可空;`cloudType === 'empty'` 保持旧行为(空应用/外部服务)。
- `taobao/pages/mvp/mvp`:MVP 页面;`mvpLocal` 时可强制直连 8080,云端时走 transport cloud。
- `taobao/pages/splash/splash.js`:`showMvpDebug = mvpLocal || (transport === 'cloud' && environment !== 'production')`;当前 development+cloud,入口显示。
- `taobao/app.json` 已注册 `pages/mvp/mvp`。
- `taobao/scripts/mvp-local-check.cjs`:本地直连契约测试(独立配置,不依赖 `config.js`)。
- `taobao/test/transport.test.cjs`:17 个测试含 tower 模式。
### 工具 / 环境
- JDK 17:`D:\juzi\tools\jdk17\jdk-17.0.21.0.2+1`
- Maven 3.9.9:`D:\juzi\tools\maven\apache-maven-3.9.9`
- Node 22.23.2:`C:\Program Files\nodejs`
- PowerShell 执行策略 Restricted;先:
```powershell
Set-ExecutionPolicy -Scope Process Bypass
. D:\juzi\tools\env-java.ps1
```
- Taobao IDE 的 `wmic` 兼容 shim:`C:\Users\youmi\.local\bin\wmic.cmd`;重建脚本 `D:\juzi\tools\fix-taobao-ide-wmic.ps1`。IDE 已能打开。
## 5. 明天详细步骤
### 5.1 IDE 真机预览(第一优先级)
1. 确认 IDE 当前小程序是 **35486228**(`1'me 肌肤状态检测`),不是 35479221。
2. 重新编译。
3. 点顶部「预览」,手机淘宝扫码(**不要用模拟器**;`cloud.application.httpRequest` 模拟器不支持)。
4. 启动页右上角点「SAE MVP 连通性」。
5. 页面自动调 `/api/taobao/ping`,预期:
- `transport: "gateway"`
- `server: "java"`
- `signVerified: false`(当前 JAR 配置)
- `appKey: "35486228"`
6. 如果成功,MVP 链路验收完成;记录截图。
### 5.2 真机常见错误
| 现象 | 排查 |
|---|---|
| `cloud init failed` | IDE 当前 App 不是 35486228;云应用没设默认;`cloudEnv` 不是 test |
| `cloudAppId` 默认不存在 | `taobao/config.js` 的 `cloudAppId` 填 `62829` |
| `Cannot find module '@tbmp/mp-cloud-sdk'` | `npm.cmd ci`,IDE 里「构建 npm」 |
| 模拟器报 SDK 不支持 | 正常,改用真机预览 |
| `请先配置淘宝云应用和 HTTPS 服务地址` | `cloudType` 不是 tower 或 transport 不是 cloud |
| 真机底图不显示 | `assetBase` 是 127.0.0.1,正常;MVP 入口仍可点 |
| `SIGN_INVALID` | 只会在 `verify-sign:true` 时出现;当前 false,不应出现 |
### 5.3 开签名(真机通过后再做)
1. 开放平台打开 **35486228** 的应用,重置 AppSecret,复制新值。
2. 更新 `backend-java/src/main/resources/application-local.yml`:
- `app-secret: 新值`
- `verify-sign: true`
3. 改 `pom.xml` 版本为 `0.0.3`,重新打包:
```powershell
Set-ExecutionPolicy -Scope Process Bypass
. D:\juzi\tools\env-java.ps1
cd D:\juzi\backend-java
mvn clean -DskipTests package
```
4. 上传 `target\taobao-mvp-backend-0.0.3.jar` 到 SAE `nnn`,部署。
5. 服务测试 `POST /api/taobao/ping`,期望 `appKey:35486228`、`signVerified:true`。
6. 如果仍 `signature mismatch`:
- 确认 SAE 没有残留 `TAOBAO_*` 环境变量(它们会覆盖 JAR);
- 确认 Secret 是 35486228 的当前值,没有空格/换行;
- 点服务测试「请求详情」,核对 `sign_method`、URL query、body;
- 需要时按 `docs/SAE-Java-MVP-操作步骤.md` 第 6 节算法手工复算(仓库里目前没有 `check-gateway-sign.cjs`,之前回答提到过但未创建)。
### 5.4 提交代码
真机通过后:
- 提交这 11 个改动(`pom.xml` 0.0.2、config、transport、splash、mvp、test、docs、两个 README、.gitignore、application.yml),外加本文档 `docs/HANDOFF-2026-11-01.md`。
- **不要** `git add application-local.yml`;它在 `.gitignore` 里。
- push 到 `origin/main`;远端证书已续期(notAfter 2027-01-29),凭据有效。
## 6. 不要做什么 / 已知坑
1. **不要再创建“云托管/新建云应用”**:那是云应用套件,会新建 CPU/容器并要求开通「容器镜像服务个人版」,和现有 SAE `nnn` 不是一回事。用户已提工单确认过。
2. **不要创建聚石塔容器集群/ECS**:入淘引导的「创建集群」是旧经典流程;SAE 不需要。
3. **不要用 `app-new` 62861**:没接 SAE/CLB;用 `1me-net` 62829。
4. **不要用 35479221 的 AppSecret**:当前是 35486228;旧 Secret 不通用。
5. **不要再加 `TAOBAO_VERIFY_SIGN=true` 环境变量**:会覆盖 JAR 的 `verify-sign:false`,回到 `SIGN_INVALID`。要么全删环境变量用 JAR,要么三者都配且 Secret 正确。
6. **不要在模拟器测云应用**:必须真机预览。
7. **不要把 AppSecret 写进小程序代码、git、聊天**:`application-local.yml` 已 gitignore;JAR 里带 Secret 只是临时验证,验证完重置。
8. **主应用其他页面现在不能云测**:Java 只有 ping/whoami/echo;challenge/session/photo/skin-analyze 等还在 Node。只测 MVP 页。
9. **`signMessage:"verify-sign=false..."` 不是直连**:固定文案;看 `transport`。
10. **SAE 应用 `nnn` 的“更多”里没有部署入口时**:用完整 SAE 控制台 `https://saenext.console.aliyun.com/cn-zhangjiakou/app-list/micro`,或应用列表行 `nnn` 的 ⋮;上传走「部署应用 → 代码包部署」。
11. **云托管入淘引导的「关联云应用」灰按钮**:不要按它去创建集群;已经通过「云应用列表 → 关联云应用」关联成功(1me-net 默认)。
12. **`new-new` SAE 应用/自动 VPC**:之前失败的应用,未使用;可删,别拿它当后端。
13. **`application-local.yml` 是临时方案**:正式环境改回环境变量/保密字典,重置 Secret;JAR 不要外发。
## 7. 常用命令
```powershell
# 环境
Set-ExecutionPolicy -Scope Process Bypass
. D:\juzi\tools\env-java.ps1
# 后端构建
cd D:\juzi\backend-java
mvn clean -DskipTests package
# 本地启动
java -jar target\taobao-mvp-backend-0.0.2.jar
# 本地健康/接口
curl.exe http://127.0.0.1:8080/actuator/health
curl.exe -X POST -H "Content-Type: application/json" -d "{}" http://127.0.0.1:8080/api/taobao/ping
# 前端测试
cd D:\juzi\taobao
npm.cmd test
node scripts\mvp-local-check.cjs
```
## 8. 下一阶段(MVP 验收后)
1. 把 `h5/lib/taobao-routes.js` 的接口迁到 Java:
- `POST /api/taobao/challenge`
- `POST /api/taobao/session`
- `POST /api/taobao/me`
- `POST /api/taobao/photo`(64KiB 分片)
- `POST /api/taobao/skin-analyze`(异步任务)
- `POST /api/taobao/analysis`(轮询)
- `POST /api/taobao/reports`
- `POST /api/taobao/logout`
2. 所有 `/api/taobao/**` 统一信封 + 签名;128KB/10s 限制。
3. 账号/历史/图片存 RDS/OSS,不放容器本地盘。
4. 保留 H5 Node 路由不变,Java 只接淘宝;以现有 Node 实现为契约。
5. 接 SLS 日志、Actuator 健康检查、告警,再切正式流量。
## 9. 交接检查清单
- [ ] 真机预览 `transport:"gateway"`、`server:"java"`
- [ ] 记录真机截图/返回
- [ ] 决定是否开签名;若开,重置 Secret、0.0.3、`signVerified:true`
- [ ] 提交 10 个改动,push
- [ ] 清理 `new-new`/`app-new`/旧 35479221(确认无用后)
- [ ] 重置 AppSecret,移除 JAR 内 Secret,改回环境变量/保密字典
- [ ] 开始迁移真实接口
---
**给下一个 agent 的第一句话**:当前服务测试已通过(`transport:"gateway"`、`server:"java"`、`signVerified:false`),先做 IDE 真机预览;不要开签名、不要创建云托管/集群,先读第 6 节“不要做什么”。
+313
View File
@@ -0,0 +1,313 @@
# 开发交接 —— 换一台开发机继续做(2026-11-02)
> **写给**:在新 Windows 机器上接手的人(或未来的 agent)
> **不涉及**:服务器 / 聚石塔 / SAE 都不用重建,云端资源全部现成可用
> **上一份**:[HANDOFF-2026-11-01.md](HANDOFF-2026-11-01.md)(那一轮只到「链路打通 + Java MVP」,UI 和真机测肤是这一轮做的)
---
## 0. 一句话状态
**真机全链路已跑通**:淘宝小程序 → 云网关(AppKey 35486228)→ 聚石塔云应用 1me-net → 私网 CLB → SAE `nnn`(只做透传)→ 聚石塔 NAT 出网(EIP `47.92.113.176`)→ `https://1me.xajuzi.com/tb/` → 现有 H5 的 Node 服务 → 旷视 Face++,**三视图拍摄后报告正常返回**。
小程序五个页面的 UI 已按 H5 重做(像素级对齐,见下)。剩下的是相机组件验证和一批工程化工作,清单在第 8 节。
---
## 1. 新机器准备
### 1.1 必装
| 工具 | 版本 | 用途 |
|---|---|---|
| Git | 任意近期版本 | 拉代码 |
| Node.js | **18+(推荐 20/22)** | `taobao/` 测试、`h5/` 本地跑、零依赖 |
| JDK | **17** | `backend-java/` 编译与测试 |
| Maven | 3.9.x | 同上 |
| 淘宝小程序开发者工具 | 最新 | 真机预览(**模拟器不支持云应用 SDK**) |
| PowerShell 7(可选) | 7.x | 仓库里的脚本按 pwsh 语法写 |
### 1.2 拉代码
```powershell
git clone https://git.xiangsilian.com/xsl/juzi.git
cd juzi
```
> 远端证书有效期到 2027-01-29;凭据失效时重新配置 Git 凭据管理器。
### 1.3 小程序依赖(必做,否则真机报 `Cannot find module`)
```powershell
cd taobao
npm ci
```
`@tbmp/mp-cloud-sdk` 固定 1.5.10,只有这一个运行期依赖。
### 1.4 工具链
仓库自带 `tools/env-java.ps1`,查找顺序:仓库内便携工具链 → 环境变量 → PATH。
```powershell
Set-ExecutionPolicy -Scope Process Bypass
. .\tools\env-java.ps1 # 输出 JAVA_HOME / MAVEN_HOME / java -version
```
- 用系统安装的 JDK 17 + Maven 即可,**不需要**再下便携版
- 想复原老机器的布局:把 JDK 放 `tools/jdk17/`、Maven 放 `tools/maven/apache-maven-3.9.9/`(这两个目录 gitignore)
### 1.5 机密文件 —— **已随仓库提交(测试阶段例外)**
> 2026-11-02 决定:测试验证阶段优先「换机不用手工拷贝」,下面两个文件**已入库**。
> 代价是密钥进入了 git 历史,正式上线前必须按本节末尾的清单清理。
| 文件(仓库内相对路径) | 内容 | 用途 | 缺了会怎样 |
|---|---|---|---|
| `backend-java/src/main/resources/application-local.yml` | 淘宝 AppKey/AppSecret + `verify-sign` + `mode: forward` + `forward-base` | 打 JAR 时把配置塞进包里(SAE 不需要环境变量) | JAR 没有 AppKey/Secret,`/session` 一定 401;只跑 `/ping`、不部署则不影响 |
| `reference/facepp-skinanalyze/credentials.env` | `FACEPP_API_KEY` / `FACEPP_API_SECRET` / `FACEPP_SKINANALYZE_URL` | `h5/server.js` 启动时读取旷视凭据 | 本机跑 H5 后端时启动即 `exit(1)`;只做小程序端开发不需要 |
克隆下来就都有了,**新机器不需要再手工拷这两个文件**。
**唯一还要自己配的是 SSH 密钥**(用于部署 Node 侧、看日志、跑 `deploy/` 里的脚本):
```powershell
ssh-keygen -t ed25519 -C "dev@newmachine"
# 把 ~/.ssh/id_ed25519.pub 内容追加到服务器 ~/.ssh/authorized_keys
ssh ubuntu@1me.xajuzi.com "systemctl is-active juzi-taobao"
```
**共享仓库的约束**(`.gitignore` 里也写了):
1. 仅限内网私有仓库 `git.xiangsilian.com/xsl/juzi`,**不要 fork 到公开仓库 / 推送到 GitHub**;
2. 不要把这些值复制到聊天、文档、工单、issue;
3. 正式上线前必须做三件事(列在待办第 6 项):
- 在开放平台**重置** AppSecret、在旷视控制台**重置** API Key
- 用 `git filter-repo` 把这两个文件从历史里清除,并 `push --force`
- 改回「环境变量 / 保密字典 / KMS」注入(`application.yml` 里已经支持 `TAOBAO_*` 与 Spring 占位符)
### 1.6 淘宝 IDE 的一个已知坑(Windows)
老机器上 IDE 曾因 `wmic` 缺失打不开,用 `tools/fix-taobao-ide-wmic.ps1` 生成过一个 shim(`C:\Users\<你>\.local\bin\wmic.cmd`)。新机器如果 IDE 报 `install miniapp extension fail: ... wmic ...`,跑这个脚本。
---
## 2. 仓库地图
```
juzi/
├── taobao/ 淘宝小程序(当前主战场)
│ ├── config.js ★ 环境/传输配置(cloud+tower+test、assetBase)
│ ├── app.acss ★ 全局设计 token(对齐 H5 的 .btn-primary/.card/.chev/.popup)
│ ├── pages/
│ │ ├── splash/ 启动页(登录、隐私弹窗、开发版入口)
│ │ ├── profile/ 档案填写(出生/性别/地区/肤质轮播)
│ │ ├── camera/ 拍照页(蒙版取景 or 系统相机兜底)
│ │ ├── report/ 报告页(环形分、7 视角、叠加层、标记点)
│ │ ├── history/ 历史报告
│ │ ├── mvp/ 连通性调试页(ping/whoami/echo)
│ │ └── camtest/ ★ 相机组件冒烟页(只放 <camera>,判断组件是否可用)
│ ├── utils/
│ │ ├── transport.js 云网关/direct 两种传输 + 信封解包
│ │ ├── api.js 9 个接口封装(challenge/session/me/photo/skin-analyze/…)
│ │ ├── permissions.js ★ 权限(scope 前缀、诊断 diagnose())
│ │ ├── image.js 1280 长边 + JPEG q0.88 压缩
│ │ ├── voice.js 语音播报(异常静默)
│ │ └── report-model.js payload → 报告页 data
│ └── test/ 20 个 node:test 用例
├── h5/ ★ 真正的测肤后端(Node,零依赖)
│ ├── server.js 入口:Face++ 调用、评分、报告、admin
│ ├── lib/taobao-*.js ★ 淘宝云网关路由/验签/会话(关键算法在这里)
│ ├── js/skin-score.js 评分引擎(6 类 → 3 维,4:3:3 加权)
│ └── test/ 一堆独立测试脚本(没有统一 runner)
├── backend-java/ SAE 上的透传层(Spring Boot 3.5.6 / Java 17)
│ ├── src/main/java/com/juzi/mvp/api/
│ │ ├── TaobaoForwardController.java ★ mode=forward 时字节级透传
│ │ ├── TaobaoEnvelope.java 客户端要的 data.ok + data.http_status
│ │ ├── TaobaoAuthController.java native 模式的登录接口(当前未启用)
│ │ └── TaobaoSessionService.java challenge + Bearer token
│ └── src/main/java/com/juzi/mvp/gateway/TaobaoSignatureVerifier.java 验签算法
├── deploy/ ★ 服务器侧配置(systemd / nginx / 安装脚本)
├── docs/ 交接与实施文档(本文件所在)
└── tools/ 构建与素材脚本
```
---
## 3. 三套测试
```powershell
# 1) 小程序:20 个用例
cd taobao ; npm test
# 2) Java:11 个用例(含验签向量的回归测试)
cd backend-java ; mvn test
# 3) H5 单测(独立脚本,逐个跑)
node h5/test/taobao-auth.js # 网关签名,必须 PASS
```
**改动提交前请保证**:`taobao` 20/20 + `backend-java` 11/11。老机器最后一次全绿是在 `fed5293`。
> `taobao/test/frontend.test.cjs` 会用 vm 真跑页面 JS,页面 data 字段改名要同步改测试。
---
## 4. 本地怎么跑
### 4.1 H5 后端(Node,测肤真正的实现)
```powershell
cd h5
$env:PORT=8099 ; $env:HOST='127.0.0.1' ; $env:JUZI_ENTRY_MODE='state'
$env:JUZI_DATA_ROOT='..\temp\h5-data'
node server.js
# http://127.0.0.1:8099/api/health → {"ok":true}
```
启动时缺旷视 key 会直接退出。只想调 UI/接口时给假值即可(真实检测会失败,但路由可用):
```powershell
$env:SKIN_API_KEY='dummy' ; $env:SKIN_API_SECRET='dummy'
```
### 4.2 Java(SAE 那一层)
```powershell
. .\tools\env-java.ps1
cd backend-java
mvn spring-boot:run # 或 mvn -DskipTests package 后 java -jar target\*.jar
curl.exe -X POST -H "Content-Type: application/json" -d "{}" http://127.0.0.1:8080/api/taobao/ping
```
- 默认 `mode: native`(Java 自己实现登录,便于单独调接口)
- 部署用的 `application-local.yml` 里写的是 `mode: forward`
- 本地想看透传:`$env:TAOBAO_MODE='forward'` + `$env:TAOBAO_FORWARD_BASE='http://127.0.0.1:8099'`
### 4.3 小程序
1. 淘宝 IDE 导入仓库里的 `taobao/`(`mini.project.json` 的 `appid` 留空,以 IDE 账号为准)
2. **确认 IDE 当前小程序是 35486228(`1'me 肌肤状态检测`)**
3. `config.js` 保持 `transport: 'cloud'` / `cloudType: 'tower'` / `cloudEnv: 'test'`
4. 顶部「预览」→ **手机淘宝扫码**(模拟器不支持 `cloud.application.httpRequest`)
5. 启动页右上角有三个**开发版入口**(`environment !== 'production'` 时才显示):
| 入口 | 作用 |
|---|---|
| 权限诊断 | 打印 `my.canIUse`、`getSetting` 的 authSetting、`authorize` 的原始返回(真机上唯一能看到这些的地方) |
| 相机组件测试 | 整页只有一个 `<camera>`,用来判断组件是否可用 |
| SAE MVP 连通性 | `ping/whoami/echo`,看 `transport:"gateway"`、`server:"java"` |
---
## 5. 云端资源(只读,无需重建)
| 资源 | 值 |
|---|---|
| 开放平台应用 | `1'me 肌肤状态检测`,AppKey **35486228** |
| 旧应用(废弃) | `1'me肌肤检测` 35479221 —— **不要再用它的 Secret** |
| 聚石塔云应用 | `1me-net`,ID **62829**,默认应用 |
| 私网 CLB | `lb-k2j324dwux1svaf2akwqx` / `192.168.65.185:80` |
| SAE 应用 | `nnn`,`cn-zhangjiakou:my-namespace` |
| 代码包 | `taobao-mvp-backend-0.0.4-forward.jar`(**环境变量全空**,配置都在包里) |
| VPC / vSwitch | `vpc-8vb5mcvenp8jwa1roqcsa` / `vsw-8vbna5zb2rq03gaeiymch`(可用区 A) |
| NAT 出口 EIP | **47.92.113.176**(nginx `/tb/` 白名单按它放行) |
| 服务器 | `ubuntu@1me.xajuzi.com`,Node v20.20.2 |
| 生产 H5 | `https://1me.xajuzi.com/state/` → `juzi-h5.service`(8080) |
| 红书渠道 | `https://1me.xajuzi.com/rednote/` → `juzi-rednote.service`(8081) |
| 淘宝专用 | `https://1me.xajuzi.com/tb/` → `juzi-taobao.service`(18081) |
---
## 6. 改完怎么上线
### 6.1 小程序
IDE 编译 → 预览(自测)→ 「上传」发版本。**不需要碰服务器。**
### 6.2 Java(SAE)
```powershell
. .\tools\env-java.ps1 ; cd backend-java
mvn clean package # 测试会自动跑
# 产物 target\taobao-mvp-backend-0.0.4-forward.jar
```
SAE 控制台 → `nnn` → 部署应用 → 代码包部署 → 上传 JAR(**版本号要和上次不同**,否则会被当成同一个版本)→ 环境变量保持全空。
### 6.3 Node(`/tb` 实例)
见 [deploy/README.md](../deploy/README.md) 第 1 节。核心三步:
```powershell
tar -czf temp/juzi-h5.tgz --exclude=h5/data --exclude=h5/test -C . h5
scp temp/juzi-h5.tgz ubuntu@1me.xajuzi.com:/tmp/
```
```bash
ssh ubuntu@1me.xajuzi.com "tar -xzf /tmp/juzi-h5.tgz -C /home/ubuntu/juzi-tb && sudo systemctl restart juzi-taobao"
```
> ⚠️ **不要动** `/home/ubuntu/juzi`(生产 H5)和 `/home/ubuntu/juzi-rednote`。我们那份是独立拷贝 `/home/ubuntu/juzi-tb`。
---
## 7. 这一轮的关键结论(踩过的坑,别重复踩)
1. **云网关签名算法**:真实请求**不带 `sign_method`**,缺省等价于 `MD5(secret + content + secret)`(`content` = 排序后的 `key+value` 拼接 + **原始 body 字节**)。之前按 `MD5(content + secret)` 实现,是 10 月底那个「signature mismatch」的真因。改在 [h5/lib/taobao-auth.js](../h5/lib/taobao-auth.js)。
2. **淘宝的 scope 是带前缀的** `scope.camera` / `scope.album`;传裸名 `camera` 返回 `{"errorCode":"5022","errorMessage":"Scope:camera信息不存在"}`。
3. **`<camera>` 组件在真机上会让整页崩溃**,且 `my.canIUse('camera')` 返回 `yes` 是**假阳性**。当前默认走系统相机(`config.nativeCamera = false`)。怀疑原因是相机里嵌了**微信专有的 `<cover-view>`**,已改成兄弟节点 + 普通 view 覆盖,并加了 `pages/camtest` 冒烟页——**这个还没在真机验证,是新机器的第一件事**。
4. **网关限制**:单请求 **10 秒 / 128KB**。所以 `skin-analyze` 必须 **202 + 1 秒轮询(最长 90 秒)**,不能同步等旷视。
5. **旷视 Face++**:正式 key 有并发窗口,必须**串行 + 4 秒间隔**;60 秒超时,失败/5xx/并发超限重试 1 次;返回值含 `face_maps`,4000px 素材时约 3.2MB(真实链路客户端压到 1280,约 200–300KB)。
6. **`/api/taobao/reports` 详情带 3 张 base64 照片**,历史报告页有超时风险(网关大响应未验证)。
7. **会话/任务全在内存**:SAE 只能单副本;`juzi-taobao` 重启后登录态与进行中的任务都会丢。
8. **没有任何清理策略**:每次检测在磁盘留 10–12MB,长期必须换 OSS。
9. **转发必须字节级透传**:签名覆盖原始 body 和 query,Java 一旦重新序列化 JSON 就会 401。
10. **生产那台 Node 是旧代码**,没有 `/api/taobao/*`;我们另外起 `/home/ubuntu/juzi-tb`(18081)跑新代码,用 nginx `/tb/` 分流。
11. **图片资源**走 `https://1me.xajuzi.com/state/assets/`(`config.assetBase`)。之前是 `127.0.0.1:18180`,真机上全部加载失败。
---
## 8. 待办(建议顺序)
| # | 事项 | 说明 |
|---|---|---|
| 1 | **真机验证 `pages/camtest`** | 能出实时画面 → 把 `config.nativeCamera` 改 `true`,拍照页立刻有蒙版取景;仍然崩 → 维持系统相机 |
| 2 | `/reports` 详情瘦身 | 现在透传 3 张 base64 原图(约 0.5MB+),改缩略图或 OSS URL |
| 3 | 存储工程化 | 照片/原始 JSON/记录 → OSS + RDS;现在全在容器/单机磁盘,无清理 |
| 4 | 旷视请求裁剪 | `return_maps` 请求 10 层但报告只用 6 层,可少传 4 层省带宽与落盘 |
| 5 | WebP 转码(可选) | 实测:6 层图 WebP q80 省 54%,叠 50% 缩放省 84%;**先真机验证 `<image>` 支持 WebP data URL** |
| 6 | 密钥治理(**上线前必做**) | 两个密钥文件目前随仓库提交(见 1.5)。上线前:重置 AppSecret 与旷视 Key → `git filter-repo` 从历史清除 → 改回环境变量/保密字典;服务器 `/etc/juzi/taobao.env` 与 JAR 内嵌 Secret 一并处理 |
| 7 | Java 端可选迁移 | `mode=native` 的登录接口已写好并测过(4 个用例),可把 auth 从 Node 搬到 Java;测肤仍留在 Node |
| 8 | 仓库收拾 | `sdk-java-35479221/`、三个 `.pptx`、`tools/build_taobao_proposal_*.py` 仍未跟踪,建议加 ignore 或删除 |
| 9 | 启用签名校验(Java 侧) | 当前 forward 模式下由 Node 验签;若切 `native` 或要 `/ping` 也验签,把 `verify-sign` 打开即可(算法已修复) |
---
## 9. 故障对照表
| 现象 | 原因 | 处理 |
|---|---|---|
| 真机 `cloud init failed` | IDE 当前 app 不是 35486228 / 云应用没设默认 | 确认 IDE 账号与 AppKey |
| `淘宝身份校验失败,请重新授权登录` | 云网关签名不匹配 | 检查服务器 `/etc/juzi/taobao.env` 的 AppSecret;算法见第 7 节第 1 条 |
| `请先配置淘宝云应用和 HTTPS 服务地址` | `cloudType` 不是 `tower` 或 `transport` 不是 `cloud` | 看 `config.js` |
| 页面一片灰没有图 | `assetBase` 指向 127.0.0.1 | 应为 `https://1me.xajuzi.com/state/assets/` |
| 点拍照整页崩(「系统遇到一点问题」) | `<camera>` 组件 | 保持 `nativeCamera: false`,用系统相机/相册 |
| 报告页白屏 | `payload` 为空或 `report-model` 抛错 | 看启动页「SAE MVP 连通性」是否 `server:"java"` |
| SAE 侧 `上游 403` | 容器出口 IP 不在 nginx `/tb/` 白名单 | `curl -s https://myip.ipip.net` 取新 IP,改 nginx 后 reload |
| `Cannot find module '@tbmp/mp-cloud-sdk'` | 没装依赖 / IDE 没构建 npm | `npm ci`,IDE「工具 → 构建 npm」 |
| Java 打包报找不到 `application-local.yml` | 机密文件没拷过来 | 见 1.5 |
---
## 10. 交接检查清单
- [ ] `npm ci`(taobao)跑过
- [ ] `tools/env-java.ps1` 能加载出 JDK17 + Maven
- [ ] `application-local.yml` 与 `credentials.env` 已在仓库里(clone 即有,无需手拷)
- [ ] `taobao` 20/20、`backend-java` 11/11、`node h5/test/taobao-auth.js` PASS
- [ ] IDE 打开 `taobao/` 并确认 AppKey 是 35486228
- [ ] 真机预览能看到图片(说明资产域名通)
- [ ] 真机点「权限诊断」→ `authorize(scope.camera)=ok`
- [ ] 真机点「相机组件测试」→ 记录结果,决定 `nativeCamera` 开关
- [ ] 能 `ssh ubuntu@1me.xajuzi.com`,`systemctl is-active juzi-taobao` 为 active
---
**给接手者的第一句话**:先按第 1 节把机器装好、把两个机密文件拷过来,跑通三套测试;然后去真机验 `camtest`——那是唯一还悬着的技术问题,也决定拍照页最终形态。
+39
View File
@@ -0,0 +1,39 @@
# 淘宝适配合入主分支的兼容性验证
日期:2026-11-04。主分支及 origin/main 基线为 381da60,淘宝适配提交为 774376f。
## 兼容边界
- H5 的 index.html、js/app.js、js/accounts.js、js/skin-score.js、js/report-ui.js 与 CSS 相对主分支没有改动。淘宝专用裁切、倒计时、商品链接与页眉位于 taobao/,未替换 H5 版本。
- 共享 accounts.js 仅新增临时淘宝游客 Bearer 会话;微信 Cookie/OAuth 及用户报告接口保留。淘宝登录与游客入口仍校验网关,游客不持久化账号和历史报告。
- 后台汇总按数据来源区分同 ID 记录,外部淘宝数据只读;实际检测及身份写入仍使用各实例自己的数据目录。
- 本次发现并修正本地后台 URL 的兼容差异:local 数据的详情、下载、重试、照片链接不追加 source=local,沿用旧路径;外部淘宝来源保留 source=taobao。
## 已通过验证
- node --test --test-reporter=dot taobao/test/*.test.cjs rednote/test/*.test.cjs
- node taobao/test/h5-compatibility.cjs:H5 游客隐私、HTTPS /state/ 下模拟微信 OAuth、同账号跨设备报告、不同账号隔离、Cookie 丢失及失败处理、注销和重新登录、旧报告不变、异步皱纹加载。
- node rednote/test/browser.cjs:/state/ 和 /rednote/ 双实例隔离、浏览器身份恢复、历史报告、后台照片和原始 JSON 路径、独立注销;红书操作不改变原 H5 数据。
- JUZI_ADMIN_BROWSER_QA=1 下 admin-channels、backend、rednote backend 测试:后台渠道筛选、详情、照片来源、导出、同 ID 隔离、外部只读保护、目录边界。
- 本地链接修正后,重新运行 H5 report-persistence 浏览器测试及上列红书/后台检查,均通过。
- node taobao/scripts/sync-shared.cjs --check:评分、报告文案和共享拍摄规则与 H5 一致。
- git diff --check 通过。
测试使用隔离数据目录、模拟微信上游和检测结果,不写线上用户数据、不调用收费检测。
## 主分支已有的旧测试问题
对当前候选与 git archive main 导出的隔离基线执行同一组 31 个 h5/test/*.js 脚本,两者均为 27 通过、4 失败,失败集合和原因一致,无新增失败:
- factual-copy.js:旧轮廓文案预期与当前主分支文案不同。
- tone-dark-circle.js:旧预期 9.1,当前主分支计算值为 9.25。
- scan-wait-timeout.js:抽取代码的 VM 测试上下文缺 savedAccount,导致测试未进入报告页。
- report-copy-cases.js:需要通过 H5_COPY_CASE_FILE 提供仓库外的私有案例;当前未配置。
未为通过这些旧测试而改动 H5 业务文案或评分。原始运行日志保存在 temp/merge-compatibility-20261104/,不入库。
## 验证限制与合并方式
微信真实平台授权、淘宝真机相机/页眉/分享最终效果仍需发布前手机验收,本地模拟不等同实际平台验收。本次仅合并本地 Git,不部署服务器、不发布小程序、不推送远程。
合并前主分支备份:codex/main-before-taobao-20261104(381da60)。主分支可快进到适配分支,不需覆盖已有提交或改写历史。
+465
View File
@@ -0,0 +1,465 @@
# 淘宝小程序 + Java + SAE 最小 MVP 操作步骤
> 目标:先用一个最小页面,把 **淘宝小程序 → 淘宝云网关(或本地直连)→ Java 后端 → 统一 envelope 返回** 这条链路跑通。
>
> 本文不包含:真实测肤算法、Face++、账号体系、图片分片上传。这些等链路验证通过后,再按现有 Node 实现的接口契约迁到 Java。
>
> 当前时间:2026-10。淘宝开放平台在 2026-05 把聚石塔的部署运维迁到了 ACK/SAE,网上 2023 年以前的“容器部署平台”教程不要照抄。
---
## 0. 本次交付物
| 路径 | 作用 |
|---|---|
| `backend-java/` | Spring Boot 3.5.6 + Java 17 的最小后端,含 `ping/whoami/echo` |
| `backend-java/src/main/java/com/juzi/mvp/gateway/TaobaoSignatureVerifier.java` | 云网关签名校验,算法与现有 Node 实现一致 |
| `taobao/pages/mvp/` | 淘宝小程序连通性页面,直接复用现有 `utils/transport.js` |
| `taobao/config.js` | 新增 `mvpLocal` / `mvpBaseUrl`,不影响主应用原来的 18180 配置 |
| `taobao/app.json` | 注册 `pages/mvp/mvp` |
| `tools/env-java.ps1` | 一键加载 JDK 17 + Maven 3.9.9 |
| `backend-java/README.md` | 后端速查 |
| `docs/SAE-Java-MVP-操作步骤.md` | 本文 |
现有 H5、Node 后端、淘宝主流程页面均未改动;只新增 MVP 页面和两个配置字段。
---
## 1. 环境安装
本机已有:
- Node.js `v22.23.2`:`C:\Program Files\nodejs`
- JDK 17:`D:\juzi\tools\jdk17`
- Maven 3.9.9:`D:\juzi\tools\maven\apache-maven-3.9.9`
PowerShell 默认执行策略是 `Restricted`,直接运行 `.ps1` 或 `npm.ps1` 会被拦。每次新开窗口先执行:
```powershell
Set-ExecutionPolicy -Scope Process Bypass
. D:\juzi\tools\env-java.ps1
```
然后确认:
```powershell
java -version
mvn -version
node -v
npm.cmd -v
```
预期:
- `java version "17.x"`
- `Apache Maven 3.9.9`
- `v22.23.2`
- `10.9.x`
> 如果 `env-java.ps1` 报 “JDK 17 not found”,检查 `D:\juzi\tools\jdk17\<jdk目录>\bin\java.exe` 是否存在;脚本会自动向下找一层目录。
>
> 不要用系统里的 JDK 25 来跑这个 MVP;SAE 运行时选 17,编译也用 17,避免 `UnsupportedClassVersionError`。
---
## 2. 后端本地启动
```powershell
Set-ExecutionPolicy -Scope Process Bypass
. D:\juzi\tools\env-java.ps1
cd D:\juzi\backend-java
mvn -DskipTests package
java -jar target\taobao-mvp-backend-0.0.2.jar
```
第一次 `mvn package` 会下载 Spring Boot 依赖,耐心等几分钟。看到类似日志即成功:
```text
Tomcat started on port 8080 (http)
Started MvpApplication in ...
```
想跑自动化测试(签名算法 + envelope + health):`mvn test`。
### 2.1 另开一个窗口验证
```powershell
curl.exe http://127.0.0.1:8080/actuator/health
curl.exe -X POST -H "Content-Type: application/json" -d "{}" http://127.0.0.1:8080/api/taobao/ping
Invoke-RestMethod -Method Post -Uri http://127.0.0.1:8080/api/taobao/echo -ContentType 'application/json' -Body '{"hello":"world"}' | ConvertTo-Json -Depth 5
```
> PowerShell 调原生 `curl.exe` 时,`-d` 里的双引号可能被吞掉(会看到 `{hello:world}` 解析失败);发 JSON 建议用 `Invoke-RestMethod`,或者把 body 写进文件后用 `--data-binary "@body.json"`。
预期 `ping`:
```json
{
"success": true,
"data": {
"ok": true,
"pong": true,
"server": "java",
"time": "2026-10-30T...+08:00",
"transport": "direct",
"appKey": "",
"openId": "",
"env": "",
"signVerified": false,
"signMessage": "verify-sign=false, skipped (local/direct mode)"
}
}
```
`echo` 预期能看到 `body.hello = "world"`。
> 所有 `/api/taobao/**` 返回的都是云网关要求的 envelope;`/actuator/health` 是给 SAE 健康检查用的普通 JSON,不是 envelope。
### 2.2 不装 IDE 也能跑的前端契约测试
```powershell
cd D:\juzi
node taobao\scripts\mvp-local-check.cjs
```
这个脚本用 Node 模拟 `my.request`,加载真实的 `taobao/utils/transport.js`,直连 8080,验证 `ping/echo/whoami` 和 `unwrap` 解包。预期输出 `PASS`,其中 `ping.server = "java"`。如果这里都不过,先别开 IDE。
---
## 3. 前端本地联调(淘宝 IDE)
### 3.1 安装依赖
```powershell
cd D:\juzi\taobao
npm.cmd ci
```
如果 `npm.cmd` 不在 PATH,用全路径:
```powershell
& "C:\Program Files\nodejs\npm.cmd" ci
```
`@tbmp/mp-cloud-sdk` 已经是 `1.5.10`,与当前 npm 最新版一致。
### 3.2 IDE 导入与启动页
1. 打开淘宝小程序开发者工具,导入 `D:\juzi\taobao`。
2. 填入真实小程序 AppID(`mini.project.json` 里的 `appid` 也可以留空,以 IDE 设置为准)。
3. 工具 → 构建 npm(把 `node_modules` 构建成小程序可用的包)。
4. 本地 `mvpLocal: true` 时,启动页右上角会出现「SAE MVP 连通性」入口,点它即可进入;也可以把启动页临时设为 `pages/mvp/mvp`。
5. 用**桌面模拟器**跑;手机上的 `127.0.0.1` 指向手机自己,连不到你电脑。
### 3.3 当前配置
`taobao/config.js` 新增了:
```js
mvpLocal: true,
mvpBaseUrl: 'http://127.0.0.1:8080',
```
`pages/mvp/mvp.js` 在 `onLoad` 时如果 `mvpLocal === true`,会临时把 `transport` 改成 `direct`、`baseUrl` 改成 `mvpBaseUrl`,离开页面时恢复。这样不会破坏主应用原来指向 `18180` 的 Node 后端配置。
进入页面后会自动调用一次 `/api/taobao/ping`,也可以点三个按钮:
- `POST /api/taobao/ping`
- `POST /api/taobao/whoami`
- `POST /api/taobao/echo`
预期页面显示 `server: "java"`、`pong: true`。
> 如果 IDE 拦截 localhost 请求,打开 IDE 的“不校验合法域名”/“开发环境不校验请求域名”开关(不同版本叫法不同)。桌面模拟器一般可以直接访问回环地址。
### 3.4 这一步验证了什么
- 小程序的 `utils/transport.js` 能走 `direct` 分支;
- `my.request` 能发 `POST application/json`;
- `transport.unwrap` 能正确解包 `{success,data}`;
- Java 后端端口、路径、返回结构都对。
---
## 4. 云端链路(SAE + 聚石塔云应用)
### 4.1 先理解三层关系
```text
淘宝小程序
└─ @tbmp/mp-cloud-sdk
└─ 淘宝云网关(校验 appkey/sign,注入 open_id/access_token)
└─ 聚石塔云应用(应用/环境/流量接入)
└─ SAE(真正的 Java 容器,跑 JAR)
```
- **SAE 只是算力层**,不是“建一个公网 URL 就能被小程序调用”。
- 小程序要调通,SAE 应用必须挂在**聚石塔云应用**下,或者走**空应用(不入塔)+ 域名白名单**。
- 2026-05-11 公告:原聚石塔“应用创建链路”2026-06-20 下线,部署运维迁到 ACK/SAE;聚石塔保留云应用管理、环境管理、流量接入。
- 参考:[迁移通知](https://api.alidayu.com/support/announcementDetail.htm?source=search&id=25839)、[新链路快速创建云应用](https://open.taobao.com/doc.htm?docId=122332&docType=1)、[云服务产品概述](https://miniapp.open.taobao.com/docV3.htm?docId=118541&docType=1)。
### 4.2 构建 JAR
```powershell
Set-ExecutionPolicy -Scope Process Bypass
. D:\juzi\tools\env-java.ps1
cd D:\juzi\backend-java
mvn -DskipTests clean package
```
产物:
```text
D:\juzi\backend-java\target\taobao-mvp-backend-0.0.2.jar
```
### 4.3 SAE 创建应用
在 SAE 控制台“创建应用 → 代码包部署 → 设置代码包部署”里按下面选:
| 配置项 | 选择 |
|---|---|
| 技术栈语言 | **Java** |
| 代码包类型 | **JAR 包部署** |
| 应用运行环境 | **标准 Java 应用运行环境** |
| Java 环境 | **Open JDK 17** |
| 文件上传方式 | 上传 JAR 包 |
| 启动命令 | 默认 `java -jar` |
| 应用端口 | **8080** |
| 健康检查 | HTTP `GET /actuator/health`,期望 200 |
| 实例数 | **2**(至少 2,避免单点) |
| 时区 | **Asia/Shanghai** |
| 地域/命名空间/VPC | 用聚石塔创建云应用时分配的那套,不要自己新建默认命名空间 |
**不要选**:
- WAR 包部署
- `apache-tomcat-*`
- `EDAS-Container-*`
- Open JDK 25
原因:WAR/Tomcat 是给外置 Tomcat 的老项目;EDAS-Container 是 HSF/Pandora Boot 专用;Open JDK 25 与 Spring Boot 3.5 的兼容性没有 17 稳。
环境变量(SAE → 应用设置 → 环境变量):
```text
TAOBAO_APP_KEY=你的AppKey
TAOBAO_APP_SECRET=你的AppSecret
TAOBAO_VERIFY_SIGN=true
```
> `TAOBAO_APP_SECRET` 只能放服务端,绝不能进小程序代码或 `config.js`。
建议 JVM 参数(按实例规格调整,堆小于容器内存):
```text
-Xms512m -Xmx1024m -Duser.timezone=Asia/Shanghai
```
### 4.3.1 更新代码包(重新上传 JAR)
应用创建之后,更新 JAR 的入口:
1. SAE 应用 `nnn` → 右上角「部署应用」/「变更」→「代码包部署」→ 上传新 JAR → 确定 → 部署。
2. 如果右上角没有,左侧「变更记录」→「创建变更」→ 代码包部署。
3. 聚石塔资源视图偶尔不显示部署按钮,可直接用完整 SAE 控制台:
```text
https://saenext.console.aliyun.com/cn-zhangjiakou/app-list/micro
```
选 `my-namespace` → `nnn` → 部署应用。
每次上传的 JAR 文件名/版本要不同,SAE 才会当成一个新版本。`pom.xml` 的 `<version>` 已改成 `0.0.2`,产物是:
```text
taobao-mvp-backend-0.0.2.jar
```
SAE 部署页的「版本」可以填 `0.0.2` 或 `20261101-2`。不要重复上传同名同版本的包。
### 4.4 流量接入
- 云应用走**内网私网 SLB**:聚石塔“流量接入”里绑定私网 SLB,后端端口 8080。
- 不要给云应用开公网入口;公网只用于你自己调试。
- 如果后端要调 TOP/奇门/外部 API,给 SAE 配 **NAT 网关或 EIP**,否则容器只入不出,调用会超时。
- 数据库在 RDS 时,把 SAE 的网段/安全组加进 RDS 白名单。
### 4.5 聚石塔/开放平台关联
1. 聚石塔控制台 → 云应用 → 新建云应用(按 2026-05 新链路向导)。
2. 部署方式选 SAE;创建测试环境、正式环境。
3. 在 SAE 里把 JAR 部署到对应应用/环境。
4. 聚石塔 → 流量接入 → 绑定私网 SLB(测试/正式各一套,或共用一个正式 SLB)。
5. 开放平台控制台 → 云托管 → 关联云应用 → 选择你的云应用和对应 SLB。
6. 在“服务测试”里调用 `POST /api/taobao/ping`,确认返回 `success: true`。
7. 在 `taobao/config.js` 里把 `mvpLocal` 改成 `false`,并配置:
```js
transport: 'cloud',
baseUrl: 'https://你的HTTPS域名',
cloudAppId: '控制台拿到的 cloudAppId',
cloudEnv: 'test', // 先 test,验收后改 online
```
8. 进入 `pages/mvp/mvp`,点 `ping`,看到 `transport: "gateway"`、`signVerified: true` 即通。
> 账号必须一致:创建云应用的聚石塔账号,要和开放平台小程序账号一致;子账号看不到云应用。小程序上线后默认云应用不能再改,选择时确认好。
### 4.6 空应用(不入塔)作为备选
如果这个 SAE 应用不在聚石塔,不能直接“关联云应用”。只能走空应用:
1. 开放平台 → 云托管 → 云应用管理 → 新建空应用。
2. 关联一个**没有敏感数据权限**的 AppKey。
3. 设置域名白名单,提交审批(HTTPS、已备案域名)。
4. 调用时 `exts.domain` 传你的域名;现有 `transport.js` 已经会传 `domain: config.baseUrl`。
注意:现有 `transport.js` 的 cloud 分支还要求 `config.cloudAppId` 非空。如果空应用控制台没有给 cloudAppId,需要先把这行校验放宽(或暂时先用聚石塔云应用)。
空应用只适合内容/工具/轻服务;电商交易、用户敏感数据类大概率不适合,还是得入塔。
---
## 5. 云网关接口契约(Java 后端必须遵守)
现有 Node 实现已经按这个契约写了,Java 迁移时照抄:
```json
{
"success": true,
"data": {},
"errorCode": "xxxx",
"errorMessage": "xxxx"
}
```
- 只支持 **GET/POST**;POST 必须 `application/json`。
- 请求体 **≤128KB**;超时默认 3s、最大 10s。
- 平台会往 URL 追加 `app_key`、`open_id`、`access_token`、`sign`、`env`、`mini_app_id` 等系统参数;自定义参数不要重名。
- 成功时小程序 SDK 只把 `data` 返回给页面;`errorCode/errorMessage` 在失败时返回。
- 发布前把 `cloudEnv` 从 `test` 改成 `online`;默认云应用上线后不能改。
- 图片/长任务不要塞进一个请求:现有实现是 64KiB 分片上传 + 异步任务 + 轮询。
参考:[云应用调用](https://open.m.taobao.com/docs/doc.htm?treeId=622&articleId=118538&docType=1)。
---
## 6. 签名校验
Java 的 `TaobaoSignatureVerifier` 与现有 `h5/lib/taobao-auth.js` 算法一致:
1. 取全部 query 参数,排除 `sign` 和空值,按 key 升序,拼成 `key1value1key2value2...`。
2. 末尾追加**原始 JSON body**(不要重新序列化)。
3. `sign_method=md5`:`MD5(secret + content + secret)`;`sign_method` 为空:`MD5(content + secret)`;`hmac*` 用 HMAC。
4. 结果转大写十六进制,与 `sign` 比较。
本地 `TAOBAO_VERIFY_SIGN=false`,没有 `app_key/sign`,直接放行,返回 `signVerified:false`。
云端 `TAOBAO_VERIFY_SIGN=true`:
- `app_key` 必须等于 `TAOBAO_APP_KEY`;
- `open_id` 必须符合 `[A-Za-z0-9_-]{4,256}`;
- `sign` 必须是 32–64 位十六进制;
- 重复 query 参数直接拒绝;
- 校验失败返回 `success:false, errorCode:"SIGN_INVALID"`。
如果云端一直 `SIGN_INVALID`,按顺序查:
1. SAE 环境变量是否真的注入(重启后生效)。
2. `TAOBAO_APP_KEY` 是否和云应用/小程序一致。
3. 网关用的 `sign_method` 与后端是否匹配。
4. body 是否被反向代理/框架重新序列化(必须保留原始字节)。
5. 是否有重复的 `open_id`/`app_key` 参数。
---
## 7. 常见问题
| 现象 | 排查 |
|---|---|
| `UnsupportedClassVersionError` | 编译 JDK 高于 SAE 运行时;统一用 17 |
| `404` | 路径不对;云应用 path 是否带 `/api/taobao`;WAR 的 context path |
| `502/504` | SAE 端口不是 8080、健康检查失败、SLB 后端端口不对 |
| `success:false` + `SIGN_INVALID` | 见第 6 节 |
| `cloud init failed` | IDE 未登录、AppID/cloudAppId 不匹配、cloudEnv 不对 |
| `请先配置淘宝云应用和 HTTPS 服务地址` | `transport:'cloud'` 但 `baseUrl` 不是 HTTPS 或 `cloudAppId` 为空 |
| `直连接口仅用于本机联调` | `transport:'direct'` 但 `baseUrl` 不是 localhost/127.0.0.1 |
| `Cannot find module '@tbmp/mp-cloud-sdk'` | `npm.cmd ci` 后,在 IDE 里执行“构建 npm” |
| 手机真机请求失败 | 127.0.0.1 是手机自己;真机必须走云端 HTTPS |
| 请求超过 10 秒 | 云网关最大 10s;改异步任务 + 轮询 |
| 返回体超过 128KB | 分页/裁剪字段,不要把大图 base64 放进响应 |
### 7.1 淘宝 IDE 报 `install miniapp extension fail: ... wmic ...`
Windows 11 24H2 起默认移除了 `wmic.exe`,而淘宝开发者工具在安装小程序扩展时会执行:
```text
wmic path win32_VideoController get caption
```
找不到 `wmic` 就报这个错。本机已经放了一个兼容 shim:
```text
C:\Users\youmi\.local\bin\wmic.cmd
```
修复步骤:
1. 完全退出淘宝开发者工具(任务管理器里不要留 `淘宝开发者工具.exe`)。
2. 重新打开 IDE,再导入 `D:\juzi\taobao`。
如果 shim 丢了,重新运行:
```powershell
powershell -ExecutionPolicy Bypass -File D:\juzi\tools\fix-taobao-ide-wmic.ps1
```
永久方案(需要管理员 PowerShell):
```powershell
DISM /Online /Add-Capability /CapabilityName:WMIC~~~~
```
装完后 `C:\Windows\System32\wmic.exe` 会优先于 shim,shim 可以删除。如果 DISM 提示找不到这个能力,说明系统镜像没带 WMIC FoD,继续用 shim 即可。
---
## 8. 下一步(MVP 通过后)
1. 把 `h5/lib/taobao-routes.js` 的接口逐个迁到 Java:
- `POST /api/taobao/challenge`
- `POST /api/taobao/session`
- `POST /api/taobao/me`
- `POST /api/taobao/photo`(64KiB 分片)
- `POST /api/taobao/skin-analyze`(异步任务)
- `POST /api/taobao/analysis`(轮询)
- `POST /api/taobao/reports`
- `POST /api/taobao/logout`
2. 所有 `/api/taobao/**` 统一走签名校验 + envelope。
3. 账号/历史/图片存到 RDS/OSS,不要放容器本地磁盘。
4. 保留 H5 的 Node 路由不变,淘宝走 Java;两边共用同一套业务规则时,先以现有 Node 实现为契约。
5. 接 SLS 日志、Actuator 健康检查、告警,再切正式流量。
---
## 9. 命令速查
```powershell
# 每个新窗口
Set-ExecutionPolicy -Scope Process Bypass
. D:\juzi\tools\env-java.ps1
# 后端
cd D:\juzi\backend-java
mvn -DskipTests clean package
java -jar target\taobao-mvp-backend-0.0.2.jar
# 本地验证
curl.exe http://127.0.0.1:8080/actuator/health
curl.exe -X POST -H "Content-Type: application/json" -d "{}" http://127.0.0.1:8080/api/taobao/ping
# 前端依赖
cd D:\juzi\taobao
npm.cmd ci
```
+453
View File
@@ -0,0 +1,453 @@
## 2026-11-04 登录页固定只显示原生标题
用户真机再次确认未登录页仍出现两个标题,说明不能根据 setNavigationBar 的成功回调认定原生首页标题已经清空。本轮登录页明确启用 nativeTitleOnly,首次 Page 数据即 showTitle=false,进入、返回、旋转和成功/失败回调都不再启用自绘标题。登录页 JSON 和运行时都保留 1'me肌肤状态检测 原生标题,公共白色页眉背景、安全区及页面高度不变。其余五个业务页仍使用居中标题,登录页采用淘宝原生对齐方式,不承诺居中。
143 项相关回归通过,新增“平台返回成功但实际未隐藏标题”、API 不存在、失败和生命周期重入场景,保证登录页不再次绘制第二个标题。IDE 重新编译后实际登录页只看到一个左侧原生标题;手机效果仍待新预览确认。共享逻辑检查同时发现 Git 在 Windows 检出 manifest.json 为 CRLF 后误报,已仅对清单文本比较归一化换行;实际五份共享 JS 仍逐字节校验,不放宽业务代码一致性。
## 2026-11-04 未登录首页重复标题的动态清理
用户反馈未登录登录页出现两个标题。此前仅 JSON defaultTitle=零宽空格,尚未在生命周期调用 setNavigationBar,不能证明首页平台标题已在真机隐藏。本轮在公共页眉包装器的 onLoad/onShow/onReady 调用 my.setNavigationBar({title:零宽空格}),不修改导航背景及返回/胶囊;保留原页面生命周期、Promise 返回值与业务登录流程。回调用独立序号及可见性隔离,离页及旧回调不能更改现页标题状态。
登录页额外启用 nativeTitleFallback:若平台明确拒绝清理(fail、抛错或 Promise reject),隐藏自定义标题文字,保留平台原生标题,避免叠加;该降级情况下不能承诺原生标题居中。非登录页沿用现有居中标题。保留背景品牌图片和登录按钮。新增页面进入/返回时的原生清理、旧回调隔离、拒绝/异常不阻断业务测试,相关回归通过。真机是否实际接受标题清理仍待新预览验证,未将模拟器结果等同真机。
## 2026-11-04 全部业务页白色页眉标题水平居中
用户要求所有页面顶部标题左右居中。官方 app.json window 与 setNavigationBar 文档未提供标题对齐参数,未使用猜测的 titleAlign 等字段。改为保留原生透明导航及其返回/分享/关闭控件,将原生 defaultTitle 设置为零宽空格,使用公共 components/brand-header.axml + app.acss 显示 1'me肌肤状态检测;标题容器左右对称预留 76px,按全屏中心对齐,白底黑字。
utils/page-header.js 包装业务页注册,保留并调用原生命周期,onLoad/onResize 根据 statusBarHeight(或 safeArea.top)及 titleBarHeight 计算页眉安全区。六页正文/取景根节点避让页眉;档案表单高度收缩使用扣除页眉后的高度,相机初始蒙版尺寸同样扣除页眉,最终仍由已挂载 .cam 真实尺寸校正。分享文字和页眉标题共用同一常量,避免原生空标题影响分享。
完整回归及共享 H5 校验通过,新增实际模板/样式的 320/375/414 三屏宽、20/59px 状态栏检查,覆盖全屏中心、安全区及生命周期保留。淘宝 IDE 编译完成,模拟器实际登录页确认白色页眉标题居中、系统时间和原生右侧胶囊保留且不重叠。截图 deliverables/centered-header-20261104/login-centered.png 为真实 IDE 模拟器运行截图,明确非真机;手机刘海/动态岛安全区效果仍需实测。
官方参考:https://developer.alibaba.com/docs/doc.htm?articleId=117457&docType=1&treeId=785
## 2026-11-04 分享卡片大标题显式统一
用户确认不带撇号出现在分享卡片大标题。原 app.js 全局 onShareAppMessage 已返回 1'me肌肤状态检测,不能将问题简单归因为源码漏字或断言全局钩子不受支持。官方文档 articleId=118909 明确支持 Page 分享及 App 全局回退。为明确各页输出,六个业务页面均新增页面级 onShareAppMessage,共用 utils/share.js,title 和 desc 均为 1'me肌肤状态检测,path 固定 pages/login/login,不携带报告/照片/档案/会话信息;App 全局钩子也共用此函数。
138 项前端、会话商品、页面交互、拍照测试通过,包括逐页检查分享配置与登录页标题一致、分享对象无私人参数及每次返回独立对象。此次修改不涉及开放平台登记的小程序名称。真机分享卡片最终标题仍需从新预览版本重新分享验证,不能根据代码测试直接声称客户端已修好。
## 2026-11-04 倒计时锁定,不再因识别变化取消
用户反馈稍动仍取消倒计时,要求移除这项判断。此前 countdownReady 仍检查单脸、源帧边界、分数与新鲜度,本轮移除倒计时阶段的全部姿态/位置/清晰度/人脸数复查。正式 native-face 启动倒计时前保持原检测和稳定门槛;开始后暂停新推理,在途结果不能改变倒计时,到零补光结束直接 takePhoto,不再进入 nativeCaptureWait。离页、切换镜头、身份状态变化、相机停止/故障仍通过会话与序号取消拍摄。移除已不用的 countdownReady 元数据。
专项回归覆盖无新识别结果仍拍摄、角度/位置/置信度/人脸消失不打断倒计时、在途结果不取消,以及离页/切换/故障取消。完整测试及共享 H5 校验通过。H5/备用 AR 判定未改动;手机效果仍需扫码实测。
## 2026-11-04 推荐商品切换直播间链接
用户提供新链接 https://e.tb.cn/h.jhEroZP98ha4Res,短链页面中的目标 item.taobao.com/item.htm 明确返回 id=1090510453077。更新 config.productItemId 及 productItemIds 三档统一目标,商品为用户提供的“【李佳琦直播间】1'me以我自在有势挺括双仓精华抗皱”。保留推荐评分、文案与 tb.openDetail 交互。106 项会话/商品/拍照适配测试通过,覆盖三档相同商品、重复点击、返回报告及游客直接进入商品。实际手机商品详情打开效果尚未实测。
## 2026-11-03 两处侧脸缩略图填充缩小 10%
用户要求轮廓/皱纹下面的两张小图脸部缩小。sideThumbnailStyle 的图片缩放由 cover*2.8 调整为 cover*2.52(原宽高的 90%),未读取尺寸时的 .side-img 兜底 transform 同步为 2.52;画框尺寸、纵向锚点及左右顺序不变。两处共用同一份 sides 数据及样式,原照片与皱纹标注同步缩放。大图仍为 cover*1.12、居中下移图片高度 5%,不受小图调整影响。64 项相关测试通过,缩略图覆盖横/竖/方照片与三个屏幕宽度,并验证相对原填充为 90%。
## 2026-11-03 侧脸大图缩小 20%,下移调整为 5%
用户要求减小主体:轮廓/皱纹侧脸大图原填充尺寸宽高均乘 0.8(原 cover*1.4 改为 cover*1.12),保持居中,再下移缩放后图片高度的 5%。大图窗口大小不变,原照片和标注同步使用该矩形;缩略图及拍照流程不变。64 项相关测试通过,包含尺寸为原 80%、5% 下移、跨图片比例及屏幕宽度检查。
## 2026-11-03 两处侧脸大图统一居中后下移 10%
用户要求皱纹页卡也采用居中裁切,并统一将大图填充区域向下移动图片高度的 10%。轮廓/皱纹侧脸大图共用 rectangle:left=(画框宽-缩放图片宽)/2,top=(画框高-缩放图片高)/2+缩放图片高*0.1。10% 取图片显示高度,不取画框高度;原照片和对应标注共用位置。保留倍率、缩略图和拍照逻辑。64 项相关测试通过,覆盖三种图片比例及三个屏幕宽度、实际 canvas 位移、标注对齐与回调隔离。
## 2026-11-03 轮廓侧脸放大改为图片居中
用户最新明确要求轮廓小图放大直接截图片中间,替代此前 H5 纵向偏移规则。轮廓大图使用 centerCrop,横纵位置均为(画框尺寸-缩放后图片尺寸)/2,取消向上部取样的偏移;保留原缩放倍率和等比展示。仅轮廓侧脸大图生效,缩略图、皱纹大图及拍照流程不变。新增横/竖/方图居中断言与实际轮廓 canvas 绘制中心验证,64 项报告相关回归通过。手机效果仍待重新编译后确认。
## 2026-11-03 侧脸放大改用独立位图
用户真机仍反馈轮廓侧脸大图只剩脑门和鼻子;此前浏览器几何一致不能证明淘宝原生组件显示一致,不能据此声称已修复真机问题。本轮按 H5 reportZoomSource 的绘制路径生成独立 JPEG:新增 utils/report-side-zoom.js 和报告专用离屏 canvas(与拍照 canvas 隔离),源照片/该侧标注使用相同矩形绘制;放大层只对生成图片 aspectFit,不再执行原生 overflow 图片裁切。读取尺寸优先 getImageInfo,回退原照片已知尺寸,导出图片的 onLoad 尺寸不能覆盖原图缓存。
生成期间或 API 不支持/导出失败,显示完整原照片 aspectFit,不再回退旧的激进原生裁切。导出有互斥、generation/账号/可见性守卫;旧结果不能重新打开大图。63 项相关测试通过,包括 H5 九种尺寸矩形对照、实际绘制命令与照片/标注对齐、关闭后的回调隔离。截图脚本改为执行实际 bake 模块绘制命令,输出仍明确为浏览器预览,不是真机截图。相机启动和识别本轮未修改;原生显示仍需用户重新编译后实测。
## 2026-11-03 倒计时后允许正常移动
用户要求取消倒计时后轻微移动导致无法拍摄的规则。正式 native-face 流程开始倒计时前仍按原角度/居中/距离与稳定帧判定;倒计时、补光、快门等待期间改用 countdownReady,只检查清晰的单人面部及其源画面边界,不再因角度、居中或距离偏离原阈值重新计时。新鲜帧 1.5 秒、性能故障停止、离页/切换镜头取消、账号隔离及原生拍照互斥均保留。
新增回归:启动后转头仍完成一次原生拍照、人脸消失取消;保留旧帧与在途识别等待测试。相机/质量/前端相关 97 项测试通过,共享 H5 文件校验通过。修改仅限小程序原生识别链路,未改共享 H5 判定或备用 AR 流程;手机实际效果仍待预览确认。
## 2026-11-03 扫描品牌黄与侧脸大图原生布局
用户确认相机目前恢复;本轮未修改识别、启动、角度或抓拍逻辑。实时扫描带 RGB 改为品牌黄 245/240/120,动画时序不变。
用户继续反馈侧脸大图裁切靠上。轮廓/皱纹大图改为最终屏幕尺寸的裁切窗口,图片和标注使用相同百分比定位,不再依赖小尺寸 overflow 容器整体 transform 放大。保留 H5 reportZoomSource 的 .5 fillScale 与 .25 contentDown 规则。新增浏览器几何对照覆盖 320/375/414 三宽及竖/横/方形照片九种组合,与 H5 drawImage 矩形误差小于 .15 CSS px;60 项相关测试及共享校验通过。浏览器截图已更新,淘宝真机裁切效果仍需核验,尚不能断言原生 transform 是唯一根因。
## 2026-11-03 侧脸小图点开后大图比例修正
用户反馈轮廓/皱纹侧脸大图扁平。检查发现原大图在 getImageInfo 不可用或失败时默认 3:4,而 image 使用 scaleToFill;当实际照片比例不同,会产生拉伸。实际手机是否命中此失败分支尚未采到日志,不据此断言全部真机根因。
现小图缓存自然宽高,放大优先复用;尺寸未知时显示 aspectFit,不再猜测 3:4,真实图片 onLoad 后补齐 H5 1.4 倍居中裁切及 .25 高度下移几何。大图基础照片始终等比;原照片与其标注层、左右顺序保持关联。尺寸回调带源图片及 zoom generation/账号校验,关闭或离页后不能重开。59 项相关测试及 H5 共享校验通过。
输出 deliverables/report-side-zoom-20261103/,以项目 contour-demo/left.jpg 和真实 AXML/ACSS/控制器数据生成浏览器投影,并用线上 reportZoomSource 函数生成 H5 对照;明确标记非淘宝真机截图。相机用户补充为恢复按钮常驻且点击也无法拍摄,需继续依据其错误提示定位,不能只归因于测试入口。
## 轮廓/皱纹侧脸小图裁切还原
已重新读取线上 `/state/css/ui-20261012.css` 和 `/state/js/app.js`。H5 两张侧脸缩略图使用居中 cover、translateY(-25%) scale(2.8),图框 104.35u×92.25u。此前小程序虽使用相同变换字符串,仍依赖原生 image 的 aspectFill/transform 执行效果,用户反馈实际不一致;尚不能确定具体原生差异机制。
现按照片自然尺寸显式计算 cover 后 2.8 倍的宽高,left=(208.7-imageWidth)/2,top=184.5/4-imageHeight/2(rpx),原生 image 使用 scaleToFill 和 transform:none。照片与侧脸皱纹图层共享同一 photoStyle;图片加载和 getImageInfo 均能更新自然尺寸,异步皱纹刷新保留已知几何,离页/旧照片/账号切换的回调不能写入。左右反向顺序、主图、放大图和拍照逻辑保持当前实现。
57 项相关测试通过,包含以真实 H5 CSS 在 Chromium 中计算 cover/transform 作为对照,320/375/414px 屏宽与竖图/横图/方图共 9 组显示几何误差均小于 0.15 CSS px(布局取整差异)。共享 H5 校验通过;淘宝真机实际效果仍待新版普通预览确认。
## 平台首屏性能检测(任务 20406447)
用户提供版本 0.0.1、2026-11-02 19:53:33 的平台报告:华为 P30/Android,得分 85,明确不通过项为首屏渲染 4565ms(标准 <2000ms);包体约 1099KiB,RSS 67MB、vmSize 242MB、FPS/CPU 指标均在报告阈值内。仅凭总耗时无法确定唯一瓶颈,不能将相机识别或语音确定为根因。
本轮只优化首屏:登录背景、品牌和游客图标三张 H5 原图以原始字节复制到 taobao/assets/startup(总 131632 字节),登录改用本地相对路径;App.onLaunch 移除无首屏用途的语音播放器创建,voice.play 的现有按需创建逻辑保留。没有修改相机代码、页面样式、登录校验或云环境。公网图片本机抽测约 0.29–0.37 秒,不代表平台 P30 网络性能。
新增回归验证首屏素材与 H5 字节一致、启动不加载语音模块/创建播放器;完整 node test/*.test.cjs 及共享 H5 校验通过。需要重新上传新的开发版本并重跑平台性能检测,尚无优化后真实首屏成绩,不能声称已达到 2000ms。
## 语音真机定位续查(16:46)
用户再次确认正式拍照仍无语音,拍照正常。IDE Console 仅存 VOICE-20261102-02 的 UNAVAILABLE/category=domain;不能据此断言域名白名单拒绝。VOICE-20261102-04 增加固定 operation 和受限 errorType,将 create、listen_ended/error/play/canplay、set_source、play 分开定位,不记录原始错误或 URL。补充 SDK appxDomain TypeError 回归测试;语音与拍照适配共 78 项测试通过。未修改相机、UI、交互;未声称声音已恢复。
IDE 已生成真机调试码版本 **0.1.2611021646.10**,待手机重新扫码进入正式拍照页,读取新版本错误日志后继续修复。服务器 MP3 MIME/Range 先前已修复,不再次修改云端设置。
# H5 → 淘宝小程序适配实施记录(2026-11-02)
## 报告返回手势、顶部安全区及列表核对
按用户要求,报告详情页配置 `gestureBack:"NO"`,保留原生左上角返回;同时开启报告/列表二级页原生导航栏 `navigationBarForceEnable:true`、`transparentTitle:"none"`。报告内容顶部原 56rpx 增至 72rpx,并以系统安全区为下限;历史列表保留 H5 的 56rpx 顶距并增加安全区下限。页卡横向手势及页面纵向滚动保持,不用全页 touchmove 拦截来禁用返回。需真机确认宿主执行手势设置及标题位置。
列表页对照 H5 的 history-content、account-status-history、history-count、identity-note、history-item、history-detail、history-retry 源码,主要布局和尺寸一致;淘宝身份和游客仅本次查看提示为既定渠道差异。当前不以“听到咔嚓”认定引导语音通过。用户另反馈调试版相机停在“开启实时相机”,已询问点击后的结果,尚不能判为相机功能回归。
真机 Console 已出现 VOICE-20261102-02 的 error/category=domain。此分类来自筛选后的错误文字,不能单独断言平台域名白名单问题;版本 VOICE-20261102-03 进一步区分 APPX_DOMAIN_CONTEXT、DOMAIN_REJECTED、JS_CONTEXT_ERROR 等安全错误指纹,需后续日志确定。播放器发声仍待验证。
## 真机仍无声:音频原生就绪事件与资源服务修正
用户已确认正式流程仍完全无声,手机媒体音量和静音模式已排除;此前的代码单测不能代替真机发声确认。核对安装的淘宝 SDK,音频上下文具有 `onCanPlay/onCanplay` 与 `onPlay` 事件。按[淘宝购物小程序音频规范](https://developer.alibaba.com/docs/doc.htm?articleId=121184&docType=1&treeId=776),移除淘宝不支持的 `obeyMuteSwitch` 属性设置;监听可播放事件后再调用 play,两个大小写版本均兼容,重复/迟到的就绪事件不能重播旧音频。继续使用每条新实例和单通道队列。
新增 `VOICE-20261102-02` 开发日志,区分 loading、canplay、play_requested、playing、error,记录公开语音 ID 和经筛选的错误代码/类别;不打印错误原对象、URL、照片或账号信息。不增加业务 UI 控件。播放器诊断可通过 `voice.diagnostics()` 获取安全快照。
已确认原 HTTPS MP3 返回 `application/octet-stream`,Range 请求也返回整文件 200。部署 `deploy/taobao-voice-media.py` 修正仅 `/state/assets/voice/` 静态路径,公开目录 `/var/www/juzi-voice-20261102/` 仅包含清单内 14 个 MP3 和 manifest.json,Nginx 负责 audio/mpeg、206 与 Range。配置备份 `/etc/nginx/sites-available/juzi.voice-20261102.bak`;已通过 nginx -t 并 reload,H5/小红书/淘宝业务服务进程均未重启。14 条音频分段请求均返回 206 和正确类型,原主页 200、后台 401、数据路径 404,保护规则不变。源 server.js 同步补充 MP3/AAC MIME,未部署整份 H5 业务代码。
完整 244 项回归通过,共享规则校验通过;同时补齐了 WebGL 离线测试包中新增亮度取样模块的显式允许列表。IDE 正在生成本次代码的真机调试码,待手机扫码后核对原生播放事件与实际声音,不把这些改动表述成真机已修复。
## 主流程人脸亮度检测
检查发现 H5 的光线规则已复用,但此前普通相机识别结果不含亮度,正式调用也未传入 luma,因此只有固定“光线充足”引导,实时明暗提示未生效。现复用本地人脸识别已复制、转换好的小尺寸 RGB 帧,在首个人脸框内以 16×16 网格取样,使用 H5 的 .299R + .587G + .114B 均值;不新增 Canvas、Tensor、整帧复制或网络上传。
识别结果携带仅一个聚合 luma 值,质量适配器接入 H5 既有规则,侧脸放宽判定也保留此值:低于 110 提示“建议到光线明亮处拍摄”,高于 220 提示“光线过强,建议避开强光”,成功姿态时按 H5 附加“保持不动即可拍摄”。明暗均为建议,不作为拍摄否决条件;单人、完整取景、角度和新鲜度限制不变。缺失/无脸/无效输入保持未知,不伪装为亮度 0。只评估人脸画面明暗,不代表物理照度、逆光或光照均匀度检测。
96 项相关回归通过,覆盖不同背景、人脸 ROI、RGB 权重、通道格式归一、真实模型亮度输出、前/侧脸提示,以及过暗/过亮时仍可自动取图;需重编译预览后确认手机实际提示。此次不调整界面布局或已有语音文件。
## 正式拍照语音排查与恢复
用户反馈拍照没有语音。线上 step1-front、step2-rightface、countdown-321 三个 MP3 均可通过 HTTPS 读取,返回 200;手机实际无声原因仍需新版真机确认。代码检查发现普通相机及 AR 姿态结果未接入 `VOICE.hint`,现按 H5 映射补齐正对、左右转头、距离及保持不动提示;同提示不逐帧重复播放,换步/倒计时清理过期提示。正式三步引导、拍摄完成、分析与失败语音保留。倒计时先等步骤引导收尾,最长等待 3 秒后继续,保留三秒视觉计时与取图逻辑;等待期间继续识别真实帧,姿态失效取消等待,旧回调不重启拍摄,语音不可用时不阻断流程。
播放器不再每次先 stop 同一个实例再复用。改为单通道、每条播放结束或打断后释放实例,下一条重新创建;旧实例迟到的 ended/error 不可清除新语音队列,同步回调错误不再导致空对象异常。可写属性设置播放音量,支持时关闭遵循静音开关,平台不支持的属性失败不会阻断拍照。测试页继续遵守此前的静音、无上传规则,不把专项测试页当作正式语音验收。
新增播放器和主流程语音回归,验证无 stop-before-first-play、停止后不复用、迟到回调隔离、排队/去重、异常资源释放及正式转头提示触发;未调整 UI 或语音文件内容。
本轮完整 236 项回归及共享 H5 校验通过,IDE 自动编译完成;手机实际听音仍待重新编译预览后的正式流程确认。
## H5 排版与交互还原
已通过只读 HTTPS 获取线上 `/state/` 的 index.html、style.css、ui-20261012.css、swipe-track.js;统一 CRLF/LF 后与本地内容一致。以这些源码为基准修正此前遗漏,不改变评分、报告文案、照片处理或推荐规则。
- 报告三组详情卡原先仅渲染当前面板、只能点击标签。改为全部面板挂载的横向轨道,检测图区域支持跟手拖动、短滑回弹、快划切换、边界回弹及标签联动,竖向手势仍用于阅读页面;文字区不绑定横滑。滑动中保持卡片高度,结束后按当前面板实测高度调整;侧脸标注异步完成后重新测量,避免裁切文案。
- 检测图和侧脸图新增与 H5 一样的点按放大/再次点击关闭。放大保留当前裁切、标记和图层;滑动释放不误触放大。离页、切换账号或缩放尺寸时取消未完成的手势/图片回调,避免私有图片重新弹出。
- 档案肤质轮播从“松手才跳页”补为 H5 跟手轨道,箭头、圆点同步,取消/纵向/多指手势不误选。
- 正常主页、登录和拍照页不再展示开发工具入口;需要专项验收时,开发环境显式启用 `cameraDebug:true` 可显示入口。主页游客状态恢复“游客模式”,去掉额外的游客操作按钮,保留 H5 的开始检测/我的报告;游客可从历史页进入淘宝登录,关闭小程序后不保存本次会话。报告页去掉 H5 没有的附加历史/结束体验按钮,历史入口统一从主页进入。
完整 227 项测试通过,新增 8 项针对手势、页卡高度、图层状态、放大与身份切换的行为回归。只读浏览器投影预览覆盖 320/375/414px 的主页、档案、报告布局,未见页面横向溢出,页卡宽高与当前面板吻合;预览脚本与截图保存在 `temp/check-h5-ui-layout.cjs`、`temp/h5-ui-restoration-20261102/`,使用项目测试样本,不能替代淘宝真机渲染/手势验收。共享 H5 校验通过。需重编译预览后核对真机横滑及放大体验。
## 正式主流程接入与回归
主流程沿用登录 → 启动页授权 → 档案 → 普通前置相机自动三连拍 → 压缩/上传 → 报告,入口为不带 debug 参数的 `/pages/camera/camera`。原页面与操作保持不变。首次进入、相机授权状态尚未确定时,自动衔接平台相机授权再开始取景;已明确拒绝时不反复请求,仍使用原权限提示与相册入口。
补充游客和淘宝账号两条多页面集成回归:运行实际登录、档案、拍照、上传客户端及报告页面代码,仅相机帧/JPEG、传输服务响应与平台 API 使用固定测试替身。验证游客不请求账号授权、不持久化档案/会话,正式账号保存自己的档案,普通业务三连拍完成后按 front/left/right 上传,只提交一个 request_id,渲染三组七项报告并打开 `1088839442533`;退出游客清除本次报告。完整 219 项回归通过,共享 H5 与本地模型产物校验通过;没有发起真实付费检测。云网关到真实服务和手机报告结果仍待验收。
## 当前状态:用户确认自动三连拍通过
用户已反馈“自动三连拍已经好了”,将最近交付的前置相机三步自动抓拍标记为当前手机真机通过;不推广为所有机型、后置或 AR 路线已通过。正式业务配置从临时上传优先恢复为 `photoInputMode:'camera'`,沿用同一普通相机识别与拍照实现;相册入口保留,页面布局和交互不变。正式业务离线回归使用实际配置验证三张照片只触发一次报告请求。
本轮再次确认远端 `juzi-taobao` 为 active,18081 `/api/health` 返回正常;这不代表小程序云网关已通。下一项仍是游客云服务访问、三图上传和手机真实报告,尚未完成端到端验收。
## 前置相机第二、第三张方向校准(LOCAL-FACE-20261102-09)
用户真机确认第二、第三张的角度方向相反:提示向左转头时,实际接受的是向右转头。普通前置相机的本地姿态判定现传入 `mirrored:true`,翻转 yaw 符号,等价于对调两个侧脸步骤接受的原始角度方向。步骤仍是正脸/右脸/左脸,原 UI、转头提示、遮罩、照片标签及提交顺序不变;后置相机仍用原映射,AR 对照及共享 H5 文件不变。保留 `08` 的放宽阈值,不以取消方向检查来放行。
相关 71 项测试通过,覆盖前置左右方向互换、错误方向提示、后置方向保留、小角度自动三连拍及照片输出顺序。随后用户已确认自动三连拍可用。
## 性别选择控件对齐 H5
档案页原先使用系统 `picker mode=selector`,现改为页面内底部弹层的单列 `picker-view`,复用已有出生年月/地区弹层容器。对照 `h5/js/app.js` 的 `genderPicker` 与 `h5/css/style.css` 的 mobile-select 样式:标题“性别”,选项“女/男”,左右“取消/确定”,50px 标题栏、220px 滚轮、44px 淡黄色选中框、90% 内容宽度及安全区留白。触发框沿用原样,滚动仅更新草稿,确定后更新性别,取消不更改字段;再次打开定位已保存选项。出生年月与地区的原实现保持不变。需重新编译预览后确认手机实际观感。
## 最新真机反馈与角度调整(LOCAL-FACE-20261102-08)
用户提供 `07` 真机诊断:WebGL 自测及预热通过,使用 `quad_strip`,本次识别约 70ms,已自动拍下一张;第二步仍为 `quality.reason=angle`。因此初始化及性能已在该设备本次测试通过,左右三连拍尚未完成。
按用户要求先放宽转头条件:仅在淘宝普通相机本地识别中,将侧脸 yaw 范围由 24–84° 放宽到 12–100°,侧脸 pitch 上限 29° → 40°、roll 上限 22° → 30°。只接受原单人、清晰度、完整取景和居中检查都通过后的侧脸角度失败;方向、正脸条件、450ms 稳定保持、倒计时及 1.5 秒新鲜度限制保留。共享 H5 文件、正式上传优先配置、页面布局与按钮交互不变。`08` 仍需真机确认能否完成三连拍。
## 最新约束:保持原有 UI 和交互,优先完成上传与报告
用户要求 UI 和交互与原来保持一致。本次撤回此前新增的上传面板、登录协议样式、动态按钮文案、入口版本标签和额外提示弹窗;恢复原相册/快门/切换镜头控制条、取景引导和步骤提示。保留接口修复及同步异常后解除按钮锁定的逻辑,不因排障新增业务页面或改变页面布局。
游客入口真机错误已确认为 `FAIL BIZ TARGET SERVICE_ERROR:.访问目标服务异常`。淘宝专用 Node 健康,最近日志未见对应新请求,仍需核对云应用、私网 CLB 和 SAE Java 转发服务;尚未确认云端根因,也未完成手机端报告验收。用户可通过此前提供的阿里云 SAE 控制台链接登录;代开浏览器的系统命令被自动权限检查拒绝,未绕过。
用户已调整优先级:实时抓拍继续保留,先验收三图上传闭环。`photoInputMode:'upload'` 在正式业务页跳过尚未通过真机验收的 GPU 初始化,沿用原相册图标选图、快门调用系统相机和三步引导,不显示独立上传面板。复用压缩、分片上传、同一 request_id 重试及报告跳转;游客身份与本次会话规则保持不变。开发版实时识别入口独立保留。
本轮联合测试 209 项通过;后续文案微调的相机回归 60 项通过,共享 H5 规则校验通过。已将匹配的 server.js、lib/*.js、js/*.js 更新到淘宝专用 `/home/ubuntu/juzi-tb/h5`,服务 `juzi-taobao`、端口 18081。备份为 `/home/ubuntu/juzi-tb-backup-upload-20261102.tar.gz`,数据目录 `/home/ubuntu/juzi-tb-data` 保留。发布检查曾因幂等标记查错文件触发自动回滚,修正后重新部署成功。健康检查、游客路由的同意门槛及 challenge 已在服务器通过;未调用真实付费检测,手机三图生成报告仍待验收。未部署 H5/小红书服务或统一后台。
手机验收:重新编译并预览 → 扫新码 → 正常游客体验或淘宝登录 → 开始检测并填写档案 → 依次选择三张照片 → 查看报告和商品跳转。此轮无需进入“相机测试”。以下相机版本记录为此前专项历史。
## 目标与基准
- 以 `https://1me.xajuzi.com/state/` 的页面、评分、七项报告、三视角拍摄和等待动画为基准。
- 源码起点 `381da60`,工作分支 `codex/taobao-h5-adaptation`。已重新 fetch;远端当前与此起点相同。
- 线上 HTML、评分及报告呈现文件已核对。本地 `app.js` 多了小红书渠道兼容,拍摄规则来自同一份 H5 源码。
- 淘宝账号替代微信;保留游客体验,游客报告仅本次可见。三档推荐文案保留,全部跳商品 `1088839442533`。
- 游客入口不需要淘宝账号登录/账号授权,不读取账号资料;仅在同意隐私告知后建立临时检测会话,相机权限在实际拍摄时申请。账号恢复不能阻塞游客按钮,迟到的旧账号结果不能覆盖游客流程。
- 最新用户选择:颜色校验仅保留为可选开发工具,不作为拍照/导出/报告门槛。缺少格式且严格匹配四通道的帧使用应用默认 RGBA;平台明确格式优先。
- **实时识别抓拍是首版验收要求。当前主线为普通前置相机;手机已识别人脸且姿态合格,CPU 优化版单帧从约 10.7 秒降至约 4.25 秒,仍未满足实时要求,首版尚未验收。正在验证原生 WebGL 加速候选。**
## 平台测肤接入:新增前置核对(2026-11-02)
已核对淘宝公开的[《肌肤测试》接入文档](https://developer.alibaba.com/docs/doc.htm?treeId=635&articleId=118965&docType=1)。页面更新于 2023-01-29,因此具体可用能力、申请入口和运营联系人仍需以目标应用后台及当前运营确认为准。
文档分别列出平台算法与自带算法两条路线。项目实际使用 Face++ 测肤服务及自有评分、报告逻辑,按“自带算法”路线准备;该路线仍有商家资质、ISV 调用资质、方案确认和测肤白名单流程。用户已反馈“尚未申请/目前不清楚”,当前按测肤白名单未申请、ISV 资质待确认处理;不能以已有 AppKey、可预览或普通相机可调用代替开通确认。
### 修正后的推进顺序
1. 核实天猫品牌/美容护肤主营类目、对应应用及 ISV 资质,确认自带算法测肤白名单进度。申请材料说明现有算法供应商、三视角采集、用途、数据流和游客仅本次查看规则;提交申请、联系运营由商家确认后执行。
2. 获得该应用实际开放的拍照组件及接入契约,确认能否实时取景、获得人脸点位或识别结果、自动触发抓拍、连续采集三视角并返回可送检照片。**首版 H5 式实时识别抓拍目标不变**;若组件限制影响目标,先确认平台支持的实现方式,再确定正式相机方案。
3. 增加拍摄前的品牌方人脸采集告知和明确授权,覆盖正式账号、游客及相机测试入口。当前登录页展示的是 Face++ 政策,不等于品牌方采脸弹窗。需要补齐品牌主体名称、完整面部图像用途、14 周岁承诺、肖像使用范围、实际第三方处理情况、品牌隐私链接及退出/删除联系方式;拒绝后不启动采集。官方模板的营销共享条款需按实际业务与当前审核口径确认,不能直接扩张实际共享用途。
4. 在平台要求的测肤结果边界适配一级 `skinType`:0 干性、1 中性、2 混合偏干、3 混合偏油、4 油性。先确认回传接口和现有算法结果的语义,再加适配;不直接改 H5 历史结构,也不用问卷答案冒充算法检测结果。品牌七项报告继续沿用。
5. 平台接入、采集授权与相机能力一起验收,再进行真实三连拍、报告、商品回跳及部署验收。
文档没有写明“所有普通相机调用一律禁止”,也没有证明原生 `ar-camera` 就是白名单开放的测肤拍照组件。目前的 AR 实现和测试页保留为技术验证;即使真机有帧、能导出 JPEG,也不能据此认定测肤业务已获平台准入。正式拍摄组件方案仍待上述信息确认。
### 对接说明草稿(尚未提交)
业务为 1'me 以我品牌的淘宝小程序肌肤检测及产品推荐,拟采用自带算法接入:第三方 Face++ 提供检测能力,品牌系统负责评分、七项报告及推荐,算法服务不是品牌完全自研。参考 H5 为 `https://1me.xajuzi.com/state/`,首版要求页内实时识别、稳定后自动抓拍正脸/右脸/左脸三张图,随后生成报告并跳转指定天猫商品。
拟使用平台对该应用开放的测肤拍照组件,保留 H5 的引导与报告规则。图像经品牌后端送算法服务;淘宝与 H5 的身份、报告存储隔离,管理后台可只读汇总。游客按当前实现仅在本次会话处理,品牌后端不将其照片和报告写入持久目录;这不代表已确认第三方算法服务的保留规则。正式账号的照片/报告保留及删除规则仍需确定,再同步到隐私告知。
商家需补充:应用名称及 AppID、品牌运营主体、店铺主体及主营类目、开发主体/ISV 信息、品牌隐私政策链接与用户联系渠道。申请时不附密钥或用户照片。
需向当前平台运营确认的问题:
- Face++ 加品牌自有评分的接入分类、所需资质及当前申请流程是什么?
- 开通后提供哪个插件/组件、版本和技术文档?是否支持三视角实时识别抓拍、自定义引导、合适的图像导出和自带算法送检?若由组件内置识别抓拍,如何取得结果和处理取消/失败?
- 现有原生 `ar-camera` 能否用于该业务,还是必须使用另行开放的测肤组件?不能把两者视为同一权限。
- 采脸授权的现行文案、品牌及第三方披露要求是什么?游客是否有额外接入规则?
- `skinType` 通过哪个接口/事件返回,是否要求同时回传其他结果字段?
### 白名单申请的执行路径
优先由店铺负责人联系已有的天猫美容护肤行业运营,说明要申请“自带算法测肤拍照组件白名单”,请其确认当前测肤产品运营及接入要求。没有对接人时,可登录[淘宝开放平台支持中心](https://open.taobao.com/supportCenter)创建接入咨询工单,请求转接对应业务负责人;[官方支持中心说明](https://developer.alibaba.com/docs/doc.htm?articleId=849&docType=1&treeId=456)确认开发者可在线提交问题/需求。工单用于咨询和转接,正式白名单仍按业务运营给出的流程申请。
建议材料包括店铺链接与类目、品牌和开发主体、ISV 资质状态、目标应用名称及 AppID/AppKey、Face++ 与品牌评分的数据流、三视角实时抓拍流程、采集授权和隐私规则。先取得方案确认,再向当前运营指定的渠道提交白名单申请;通过后核对获批应用、组件/接口范围及技术文档。
公开测肤文档中的运营花名来自 2023 年,未公布邮箱,不推测联系地址。文档“申请插件”链接指向[能力中心](https://open.taobao.com/ability),本次未能验证登录后的申请按钮及能力 ID 63 的开放状态。也可从[文档关联提问入口](https://developer.alibaba.com/support/create_or_edit_problem.htm?docId=118965&docType=1)登录后咨询。
可发给运营/工单的说明:
> 我们计划为 1'me 以我品牌接入淘宝小程序肌肤检测,使用第三方 Face++ 检测及品牌自有评分、报告,申请自带算法路线的测肤拍照组件白名单。首版需要页内实时识别、稳定后自动抓拍正脸/右脸/左脸三张图,并支持默认前置和前后置切换。请协助确认商家及 ISV 资质要求、当前申请入口/材料和业务对接人,并提供对应组件文档。应用名称/AppID/AppKey、品牌主体及开发主体待补充。
## 已实施
1. 独立登录首屏、淘宝/游客两入口、协议、失败重试、正式账号签名恢复和宿主换号隔离。
2. 游客临时 Bearer 仅内存保存;账号库、照片、原始结果、报告和侧脸缓存不落盘。商品页往返保留本次报告,主动结束或冷启动清理。
3. 通过网关完整签名与单用 challenge 建立游客,不要求 open_id,不接受客户端自报身份。正式 `/me` 对照当次签名身份与旧 Bearer,避免宿主换号后继续显示旧报告。
4. 上传和临时任务按 Bearer 会话隔离;稳定 request_id/photo_refs 复用原任务。队列满不消费照片,提交响应丢失重试不会重新收费;过期任务返回明确错误。
5. 拍照蒙版从 H5 原始 SVG 轮廓生成本地 PNG,包含三方向定位线;图标、完成卡、扫描等待画面与 H5 对齐。`scripts/build-camera-assets.cjs` 可重建。
6. 补齐原来缺失的 `compress-canvas`;补压缩超时、迟到回调隔离、上传/恢复阶段提示及错误重试。切后台后继续查询原任务。
7. 推荐跳转统一固定商品 ID,防重复点击,提供缺 API/失败反馈;新报告、游客报告、历史报告的内存状态保持。
8. 后台可按显式配置只读汇总 H5 与淘宝的数据源,支持渠道筛选、分页、统计、详情、照片、原始 JSON 和 CSV。外源重试禁止写入。详见 [后台部署说明](TAOBAO-ADMIN-CHANNELS.md)。
游客入口补充修复:登录页恢复旧账号期间允许直接选择游客;选择后立即失效旧账号恢复、清除旧 Bearer 和本地账号缓存。游客 challenge/guest-session 不携带旧账号 Bearer、不调用 `scope.userInfo` 授权;旧 `/me` 成功或 401、以及其迟到页面回调均不能重定向/覆盖游客流程,游客请求失败也不会自动恢复旧账号。用户未同意告知时仍保留原会话并提示同意,不提前切换身份。相关 API 时序与页面回归共 32 项通过。服务器游客接口仍需部署配套版本后才能手机验收。
## AR 实时相机
普通 `camera` 的实时帧监听在旧官方文档中标为暂不支持。本轮改用淘宝原生 `ar-camera`,模式 `faceDetection2D`,事件 `onARFrame` 返回人脸框及 106 点坐标。
### 默认前置与镜头切换(初始排查,最新映射见后续更新)
用户反馈页内实时相机实际打开后置。原先的 `devicePosition: 'front'` 只绑定普通 `camera`,没有传给正在使用的 `ar-camera`;AR 分支的切换按钮也只弹出提示。现已向 AR 组件显式绑定驼峰属性 `devicePosition`,默认请求前置;正式拍照页右下角的翻转图标增加“切换镜头”文字,测试页也提供同名按钮。
点击切换会卸载旧组件,再按新方向挂载;取消旧倒计时、清除旧帧,并按相机会话标记丢弃旧组件的帧/停止/错误事件。已拍好的照片和当前拍摄步骤保留。人脸原始坐标仍沿用现有约定,不仅凭镜头方向推断坐标是否镜像。
本次镜头改动的相机、测试页及前端相关回归共 **28 项通过**,覆盖切换取消倒计时、旧组件事件隔离、已拍照片保留,以及测试页切镜头/重开时共享画布导出串行。这些是逻辑验证,不代表真实镜头已切换。
证据边界:淘宝缓存文档只描述按 `mode` 自动选择相机,没有列出方向参数。`devicePosition` 的明确字段及默认后置来自 [Taro 官方 Alipay 适配源码](https://github.com/NervJS/taro/blob/main/packages/taro-platform-alipay/src/components.ts#L155-L164),因此本轮是基于共享组件线索的兼容修正,**尚未证实淘宝宿主接受该参数**。没有调用未证实的 AR 切镜头方法,也没有直接用另一套 `<ar>` 替换识别与图像导出链。必须在手机确认实际镜头改变,不能以页面状态变化或本地测试通过代替验收。
用户随后重新编译预览反馈“默认仍是后置”,所以当前前置请求补丁未达到目标,镜头控制问题仍未解决;不把请求方向状态当作实际镜头状态,也不认定未开白名单就是该故障的原因。下一步需要目标应用实际可用组件的方向控制契约及真机能力确认。
### 继续排查:已复现并修复的本地问题
1. 正式拍摄页原先在切后台时解除拍摄忙状态,但旧原生画布导出还可能继续。已用持久 Promise 队列串行化 AR 导出和普通三图压缩;队列跨后台/恢复保留,旧照片不能恢复到新会话。测试实际延迟 `canvasPutImageData` 回调,验证不会并发写同一画布。
2. 已复现补光后的 300ms 内相机报错/停止,定时器仍能采用旧帧进入下一步。现在会使未完成抓拍的序号失效、清除倒计时/补光/旧帧;已提交分析任务继续运行。
3. 三张照片拍完后,相机原先仍在等待动画背后运行。现在进入报告处理时卸载实时组件并清空帧引用;分析失败时保持关闭,主动“重新拍摄”才恢复。
4. 云 SDK 的 `init` 或请求 Promise 若不结束,原有轮询截止时间无法中断当前等待。传输层已新增独立超时:初始化 15 秒,每次云/本机请求 12 秒;迟到结果被忽略,初始化失败可重新建立。重试仍沿用原任务/request_id,不在超时处理里自动发起新检测。
上述是已复现逻辑缺陷的本地修复,不等于用户手机上的报告卡住根因全部确认,也未解决默认后置问题。
### 新的两组真机诊断
- 原 AR 测试页版本为 `AR-DIAG-20261102-01`,展示可读取的设备/淘宝/SDK 版本、实际 context 方法名、原生事件类型和白名单元信息。收到不符合标准结构的回调也会计数,避免将“有回调但解析失败”误认为没有帧。帧计数保存在 JS 中,页面与元信息扫描最多每 400ms 更新一次;点击“复制诊断”才写剪贴板,文字不含图像、landmarks 或身份。登录首屏入口已从左上角小字改为醒目的黄色“相机测试(开发版)”按钮,仍仅在开发环境显示。
- 从该页新增“普通相机对照测试”,对应 `pages/camnative/camnative`。默认请求普通 `camera` 的前置,支持前后置重挂,按本机淘宝 SDK 实现调用 `onCameraFrame(callback)` 返回的 listener 的 `start/stop`。区分“方法存在”“start 成功”和“实际收到尺寸、字节非空的原始帧”;3/10 秒无帧给出明确状态。手动拍照仅作本页临时预览,无账号/上传/业务存储依赖。
- 普通相机对照页只验证取景、原始帧与照片能力,**不作为实时人脸识别三连拍的替代方案**。有原始帧也不代表已有可用人脸关键点或已取得测肤业务准入。
### 淘宝专用后端只读核对
本次通过现有 SSH 授权只读连接 `ubuntu@1me.xajuzi.com`,确认 `juzi-taobao` 为 active,回环 `18081/api/health` 的 `ok=true`。读取 `/home/ubuntu/juzi-tb/h5` 四个业务文件的哈希/功能标记后,确认与本地版本不同:已部署路由没有 `guest-session`,会话模块没有本轮 `request_id` 幂等实现。没有读取凭据、用户照片或报告,也没有重启/部署/修改远端。
因此新版游客和完整幂等链路仍需匹配的 Node 版本才能进行手机验收。相机诊断页无需后端,可先独立验证。
本次代码完成后运行 **87 项自动测试全部通过**,共享 H5 规则校验通过;淘宝 IDE 再次本地编译完成,界面显示 0 错误、0 警告。没有调用真实 Face++ 检测。默认前置、真实切换和完整三连拍仍等待用户手机的两组诊断结果。
### 用户真机反馈与颜色校验(后续更新)
用户已确认测试页为 AR 相机真机验收,计数持续增加,默认后置但按钮可以切换。其提供的旧测试页文字显示:720×1280、一张人脸、几何姿态判断通过,`capturedImage` 为 3,686,400 字节的 ArrayBuffer,未带像素格式;JPEG 导出报“相机原始图像格式尚未确认”。这个长度等于宽×高×4,可确认四通道长度匹配,但不能据此决定 R/B 或 alpha 的位置。默认方向和导出问题仍单独处理,不直接反转枚举值。
已修正两个有本机淘宝 SDK 源码依据的画布兼容问题:JPEG 结果兼容直接字符串及 `dataURL`/`dataUrl`/`base64` 外壳,验证 JPEG MIME、标准 Base64 与 JPEG 标记;像素写入优先使用淘宝 `ctx.putImageData`,保留探测到的 `my.canvasPutImageData` 兼容路径。四通道原始像素恰巧以 JPEG/PNG 标记字节开头时,不能误按压缩图片解码。
AR 测试页现为 **AR-DIAG-20261102-04**:顶部固定“复制诊断信息”和“导出照片”,复制失败仍展示可长按选择的脱敏文字。按用户后续要求,默认直接导出,不再要求做颜色校验。`config.arPixelFormat='RGBA'` 只补缺少格式且严格匹配四通道的图像;平台明确格式、开发页显式预览格式及当前运行内可选确认的格式优先,显式未知格式和不匹配的字节长度仍报错。应用默认值不是已确认的平台通道规范,诊断分别记录 default 与 confirmed。
颜色校验移至正文高级可选区域:明确点击才冻结一次原图,不复制 anchors,并串行生成最长边 480 的 RGBA/BGRA/ARGB/ABGR 预览。可选确认仅留在本次运行、仅用于同一图像字段路径,不写 Storage、不上传;不做校验或冷启动后也可使用应用默认值。正式导出保留最长边 1280,跨机型颜色与方向仍需后续验收。
用户进一步确认,切到实际自拍画面时页面显示“请求后置镜头”。据此将当前 AR 的 `config.arFrontPosition` 设为 `back`,测试页默认请求该值;正式页分开逻辑镜头 `devicePosition` 与 AR 请求 `arDevicePosition`,默认逻辑前置对应此请求值,切换后使用另一请求值。轮廓和扫描线的镜像继续按逻辑镜头计算;普通 `camera` 仍按其标准 front/back 属性控制。该配置来自当前真机反馈,新版本首次启动是否直接自拍及其他机型映射仍需验证,不能视为淘宝所有设备统一反向。
普通相机对照页同步补充固定复制入口和可选择文字后备,版本为 **CAM-NATIVE-20261102-02**。本次相关修改后 **126 项自动测试全部通过**,共享规则校验通过。仍需真机验证候选颜色、方向和 JPEG;未部署 Node 或调用真实 Face++。
默认导出与 AR 请求映射修改后,再次运行 **131 项自动测试全部通过**,共享 H5 规则校验通过。新增覆盖未校验直接导出、默认值不覆盖平台格式或可选确认、正式三连拍采用默认格式,以及反向 AR 请求不改变逻辑蒙版/普通相机。此次仍未部署 Node 或进行真实检测。
手机更新预览时,需在 IDE“预览”弹窗生成新的预览二维码,再扫码核对上述版本号;只重新编译本地模拟器不能当作手机已更新。用户此前提供的页面尚没有新版本号/复制按钮,不以该旧页面验收新入口。
点位转换使用官方图示的语义对应:轮廓取 0、2…32,眉鼻眼取 33…63,嘴取 84…103。H5 的头部姿态、距离、居中及稳定性判断原样同步到 `vendor/`,由哈希校验,未将未注明单位的原生 Euler 值当作角度。
`useCapturedImage` 只在官方文档中承诺返回图像 ArrayBuffer,未完整注明字段和像素格式。实现读取实际帧结构,支持可识别 JPEG/PNG 和明确标注的 RGBA/BGRA/ARGB/ABGR 四通道图像;按用户最新选择,缺少格式且严格匹配四通道的图像可使用应用默认值直接导出。显式未知格式或尺寸/字节不匹配仍明确报错。没有调用未记载的 AR `takePhoto` 方法。
### 真机验收步骤
后续手机反馈已确认:AR 反向请求映射仍未实现默认自拍;普通 `camera` 独立诊断则默认前置、可切换、可拍照出图,并且原始帧计数持续增加。因此继续验证“普通前置相机 + H5 本地人脸模型 + 原生 JPEG 自动三连拍”路线,不能把原 AR 请求映射标为修复成功。
用户反馈复制按钮与系统时间重叠后,两个诊断页的固定工具栏均新增顶部安全间距:读取可用状态栏/安全区/标题栏高度,最少留出 112px,正文同步下移。普通相机版号更新为 **CAM-NATIVE-20261102-03**,AR 页为 **AR-DIAG-20261102-05**。同时修正普通诊断页版号在 `Page()` 后才赋值导致初始页面可能不显示版号的问题。两页相关 39 项本地回归通过;真实手机点击位置仍待新预览验证。用户已要求跳过颜色校验,不再把此操作作为本轮测试步骤。
### 普通相机本地识别接线(最新实现)
`config.captureMode` 已切为 `native-face` 开发候选:普通 `camera` 默认请求 front,单次实时帧推理后复用 H5 的姿态、居中、距离和稳定判断,再倒计时并用同一相机 `takePhoto` 获取 JPEG。正/右/左三张接入既有顺序压缩及单次任务提交;手机已提供普通帧宽高/字节数,实际本地识别、方向和耗时仍须手机验证。显式未知格式、额外行填充及旋转标记会报错,不静默改成手动拍照。
`utils/native-face.js` 使用从 H5 原有 face-api 和量化权重构建的本地 CPU 副本:无需 DOM、网络、Node 模块或 Face++。无 DOM 环境下以仓库本地参考图真实跑过检测及 68 点,专项 6 项通过;模型占 68 个 tensor、2,200,644 字节,最后一个引擎释放后回到 0。CPU 耗时受环境影响,本机直接 Node 连续参考帧约 369–552ms,隔离 VM 约 1s;均不代表手机表现。构建脚本去掉了库内会被 IDE 静态解析的 Node-only require 分支,保存原版权声明、许可与来源 SHA。
帧缓冲在返回宿主前同步缩采样为独立 RGB,避免原生复用缓冲改变待处理图像;并发推理前先跳过忙帧。倒计时末若检测未完成,最多等 1.5 秒复核新结果,仍从原始帧采集时刻判断新鲜度;连续 3 次识别超过 1.5 秒会明确停止。切后台、换镜头、相机异常及报告处理清理监听和倒计时,旧推理/旧快门回调不能推进新会话。
本轮最终 **163 项自动测试全部通过**,H5 共享规则逐字节校验及本地模型构建校验通过。没有调用真实检测或部署服务。手机仍需验收帧格式、速度、方向、自动三连拍及报告闭环;当前服务器缺少新版游客接口的部署事实不变。
后续用户提供 `AR-DIAG-20261102-05` 的 iPhone15,2/iOS 27.0.1/淘宝 10.68.10/SDK 2.9.4 诊断,`frameEvents=0`、context 未观测。该记录属于 AR 页,并非普通相机元信息;无帧不能推断能力不支持。为减少误入,登录与首页开发测试入口现直接进入普通相机,AR 页普通相机按钮移至固定工具栏。两测试模式用 `redirectTo` 切换,避免反复堆叠页面;原生拍照或 AR 导出进行中不允许切换。用户当前手机版本仍可使用正文旧“普通相机对照测试”入口,无需为取得元信息反复生成二维码。
### 普通相机真机帧已取得
用户随后复制 `CAM-NATIVE-20261102-03`:同一设备上 `requestedPosition=front`,`takePhoto/onCameraFrame/listener.start/stop` 均存在,监听启动成功,145 个回调均有有效原始字节。图像为 **540×960、2,073,600 字节**,等于宽×高×4,未报告 pixelFormat/format。这证明有紧密四通道长度的连续图像,不能仅凭长度认定通道排列;按用户要求沿用应用 RGBA 默认值,不要求颜色校验。`nativeReady=false` 只表示该事件未观测,正式流程不依赖此标记;当前这份诊断未发起手动拍照,照片能力仍来自此前用户真机反馈。
真实模型新增同尺寸竖屏回归:参考图等比居中放入 540×960,缺格式采用默认值,并在 detect 调用后立即复用清空原生缓冲。实际检测出 1 脸和 68 点,输出坐标合法,tensor 无增长;模型专项现 **7 项通过**。本机隔离环境约 937ms,仍不代表手机性能。
普通诊断页更新为 **CAM-NATIVE-20261102-04**,顶部增加“开始自动三连拍测试”。该入口使用独立 `debug=local-face` 开发模式复用正式相机的识别、稳定判定、倒计时、正/右/左自动抓拍和 JPEG 压缩,完成后仅本页临时预览,不要求账号/档案、不上传照片、不创建检测任务、不播放远程语音。已有手动 `debug=camera` 保持原来的本机直连开关。测试入口不等于正式报告验证;已有账号的应用全局前台恢复仍可能独立进行 `/me` 校验,因此不宣称整个应用全程零网络。
自动验收版本为 **LOCAL-FACE-20261102-01**,复制入口避开状态栏,包含安全的 stage/failureStage、模型耗时、人脸计数、拍照计数及独立 canIUse 信号。正式相机属性与已验证普通诊断页对齐为 `mode=normal`、`frame-size=medium`。新增隔离回归验证不调用账号 API/getApp/discardAnalysis,不改变其他正在进行的检测;旧模式开关和生产环境登录门槛不放宽。最终 **169 项自动测试全部通过**,共享 H5 规则和模型构建校验通过;没有真实检测或部署操作。
### 手机性能阻塞与后续优化
用户在 `LOCAL-FACE-20261102-01` 实测:`canIUse.camera/createCameraContext=yes`,6 次处理后在正脸阶段因 performance 停止;最后一帧 **10,734ms、1 脸、quality=ok**,源图仍为 540×960/2,073,600 字节。连续 3 个结果超过 1.5 秒后正确停止,照片计数 0。问题是当前识别链路耗时,不能以姿态合格认定抓拍可用,也不能把结果完成时间改作源帧时间或直接放宽门限。
新版诊断 `LOCAL-FACE-20261102-02` 分开记录原始回调、忙时跳过、实际检测次数及 snapshot/model/tensor/detector/landmark/total 耗时,区分配置请求值和引擎实际值,补运行时版本。正常推理期间的额外 100 个回调只跳过、不复制图像、不刷新 UI;桥接的大帧交付和 GC 仍需实测排查。
连续预览改用官方 [camera 的 small 帧尺寸](https://developer.alibaba.com/docs/doc.htm?articleId=120279&docType=1&treeId=776),降低原生到 JS 的图像传输量,原生正式拍照仍保留 high JPEG。轻量 68 点模型来自 [face-api 官方权重](https://github.com/justadudewhohacks/face-api.js/tree/master/weights),单独存于 `reference/native-face-models` 并记录来源/SHA,H5 原始模型不改。定位输入为 160、缩采样最长边保留 320;本地等比双脸图在 128 输入只检出 1 脸,故未用该更低尺寸作为默认值。
CPU 优化仅将旧 TFJS padding 的逐标量坐标换算改为连续 typed-array 块复制,数值运算顺序不变;1×1 卷积实验未取得稳定收益,未纳入发布候选。原 540×960 参考竖屏的同图隔离基准约 full256 934ms → tiny160+pad-copy 302ms(约 3.1 倍),模型张量内存约 2.20MB → 1.08MB。更低 maxSide 192 仅省约 24ms,未采用。上述是本地基准,手机的输入帧大小、计算时间及自动抓拍仍待 `LOCAL-FACE-20261102-02` 验收。
上一轮还完成 `temp/native-face-webgl-prototype.cjs` 离线原型:显式 WebGL1 注入可在屏蔽 DOM 的模块中通过矩阵乘法、卷积、读回及真实 68 点模型,数值自测确实执行 shader;没有网络请求。但这不证明淘宝手机原生 GL 已兼容。原型阶段尚未启用 enableSkia 或 GPU;在本轮 CPU 真机复验后,已继续实现下述 WebGL 候选。MNN 插件未接入。
本轮最终 **174 项自动测试全部通过**,共享 H5 规则与模型生成校验通过。独立复核额外对照 250 组 padding,覆盖 rank 1–5、空维、TypedArray 偏移和特殊常量;多模型失败恢复与最终释放均通过。源帧 1.5 秒新鲜度、连续 3 次慢结果停止、多人拒绝和调试页无上传规则保持不变。
### 原生 WebGL 候选(后续开发)
用户在 `LOCAL-FACE-20261102-02` 复验:small 帧已实际降为 **360×640/921,600 字节**,tiny/160/pad-copy 均生效;处理 3 帧后仍停在 performance,最后一帧总耗时 **4,253ms**,其中 snapshot 8、model 29、tensor 0、detector 2,373、landmark 1,843ms。人脸及姿态合格,照片计数仍为 0。这证明减少传输量已生效,而本轮主要瓶颈在模型 CPU 计算,不能继续用同一 CPU 方案宣称实时通过。
下一候选 `LOCAL-FACE-20261102-03` 使用官方 [同层/离屏 Canvas](https://developer.alibaba.com/docs/doc.htm?articleId=118999&docType=1&treeId=776) 的原生 WebGL1 入口。按文档启用 `app.json.window.enableSkia:"true"`,配置明确请求 WebGL;不能仅以 API 存在或后端名称验收。初始化先做真实矩阵、卷积和 typed-array 读回数值自测,然后预热模型;数值错误、缺能力或上下文丢失时明确停止,不自动回到已证实过慢的 CPU。手机兼容性及速度仍待真机验证。
本机 SDK 的 legacy `createCanvasContext` 有原生 Canvas 兼容路由,保留现有 JPEG 压缩方式,不引入未证实的 2D 导出接口;文件读取结果补 JPEG 标记和标准 Base64 验证,防止把 PNG/坏数据标成 JPEG。真实手机仍需验证开启 Skia 后三张 JPEG 压缩、方向与覆盖层。
图形引擎的离线真实 WebGL 回归已验证:数值自测确实执行 shader 并读回;tiny/full 可共享同一后端;GPU 存活时拒绝切换 CPU;真实参考图输出 1 脸和 68 点;初始化缺浮点能力、读回错误及上下文失效均明确失败。挂起真实 Tensor 读回期间销毁 owner 不会提前删除资源,操作结束后旧结果被拒绝,模型及 Buffer/Texture/Framebuffer/Shader/Program/Renderbuffer 均释放;新会话可以重建。以上使用隔离 Chromium 软件 GL,网络请求为 0,不能当作淘宝手机支持或速度的证据。
相机在初始化时显示“正在校验图形加速并预热模型…”,模型就绪后才监听真实帧,初始化 30 秒未结束会停止并保留可复制诊断。GPU 错误会使原引擎退役,待其初始化/推理结束后才能重建,避免复用失效的缓存或在旧回调仍运行时删除资源。JPEG 的回调与 Promise 双通道都受同一完成标记保护,原生 binding 的 Promise 拒绝会被处理,不引发未处理拒绝。
本轮联合回归 **190 项全部通过**,其中 4 项使用真实隔离 WebGL,覆盖数值自测、模型、失败恢复、共享后端、挂起销毁、GPU 资源释放及重建。H5 共享规则与模型生成校验通过。没有上传手机照片、进行真实 Face++ 调用或部署后端;手机 GPU 自测、实时速度和三张 JPEG 闭环尚待版本 `03` 验收。
### WebGL 真机数值兼容(后续开发)
用户在 `LOCAL-FACE-20261102-03` 返回:原生 Canvas/WebGL 上下文创建及后端注册已走通,矩阵自测执行了 4 次绘制,但严格数值校验失败,`MATMUL_FAILED`,没有开启真实帧监听或拍照。旧诊断把错误长度、非有限值和误差统一归此码;`pixelReads=0` 当时只在卷积成功后更新,**不能解读为完全没有像素读回**。当前不能认定手机支持可用 GPU 推理,也不能绕过自测继续抓拍。
离线兼容实验故意破坏 `readPixels(FLOAT)` 返回后,默认路径确实矩阵失败;关闭 `WEBGL_DOWNLOAD_FLOAT_ENABLED` 时,TFJS 1.7 的真实备用路径将浮点结果编码为 RGBA 字节并用 `UNSIGNED_BYTE` 读回,矩阵/卷积及完整 68 点模型可通过。同样可关闭 `WEBGL_PACK` 使用非打包 shader。依据为 [TFJS 1.7 官方后端实现](https://github.com/tensorflow/tfjs/blob/tfjs-core-v1.7.0/tfjs-core/src/backends/webgl/backend_webgl.ts) 与 [开关定义](https://github.com/tensorflow/tfjs/blob/tfjs-core-v1.7.0/tfjs-core/src/backends/webgl/flags_webgl.ts),未强造 HAS_WEBGL、WebGL 版本或浮点能力。
版本 `LOCAL-FACE-20261102-04` 按 `default` → `packed_byte`(仅禁用浮点下载)→ `unpacked_byte`(再禁用打包 shader)逐档执行原有严格合成矩阵/卷积/读回自测,每次失败先释放后端和图形对象,通过才加载模型及预热;三种均不通过时明确停止,不回退 CPU,不放宽 1.5 秒源帧新鲜度。新增诊断包括 profile、attemptCount、合成输入的长度、非有限值数、误差及校验和,以及 floatReads/byteReads。合成指标和兼容配置仅在初始化阶段缓存,后续模型检测返回不能覆盖;不复制照片或人脸坐标。
版本 `04` 的联合回归 **194 项全部通过**,含 6 项隔离真实 WebGL 测试:浮点读回损坏后进入字节读回、前两档失败后进入非打包字节读回、三档失败停止、两条兼容路径与 CPU 参考图的人脸框/置信度/68 点几何对照,以及每次换档前图形对象释放。共享 H5 规则、模型生成及差异格式校验通过,淘宝 IDE 本地重新编译完成(DevServer: Done)。这些本地结果不代表手机 GPU、速度或自动三连拍已通过。
### 原生 WebGL 读回检查(版本 05)
用户在 `LOCAL-FACE-20261102-04` 真机复验:三档均 `MATMUL_FAILED`,最后 `unpacked_byte` 记录 5 次绘制、1 次 `UNSIGNED_BYTE` 读回,输出长度 256、非有限值 0、244 项不匹配、最大绝对误差 22、预期校验和 2786/实际 0。帧监听和照片计数均为 0。日志呈全零特征;聚合数据不能单独证明每个元素都为零,更不能据此确定是绘制、上传还是读回出错。此前代码在数值不匹配后即抛错,尚未检查该次 GL 错误,不能将“未报告 GL 错误”解读为“没有 GL 错误”。
版本 **LOCAL-FACE-20261102-05** 根据[淘宝官方 Canvas 注意事项](https://developer.alibaba.com/docs/doc.htm?articleId=121761&docType=1&treeId=829)使用 `getContext('webgl',{enable_flip_y_after_read_pixels:false})`,关闭宿主默认附加的 flipY。隔离实验中,按较大 canvas 高度转换较小 framebuffer 的读取坐标可以产生全零且 `getError=0`;这说明该路径值得验证,**尚未证实手机内部存在这种坐标转换**。
TF 初始化前增加 2×2 RGBA8 清屏/读回、非对称 shader、字节纹理及浮点纹理上传采样检查,使用固定非零合成输入和预填目标哨兵值,区分未写回 TypedArray、错误像素、shader 编译/链接、纹理上传及 framebuffer 问题。基础检查失败即停止并释放资源;通过后仍需通过原有严格矩阵/卷积数值自测、兼容档选择及模型预热。没有降低数值阈值、放宽 1.5 秒新鲜度或自动回到 CPU。
新增诊断保留 `nativeProbe` 阶段与规范化错误、各基础检查结果、哨兵改写数、不匹配数、预期/实际校验和、读回返回值类型枚举、GL 错误和 framebuffer 状态。TF 自测在数值判定前采集 GL 状态;这些固定合成数据只在初始化时缓存,实际人脸检测不能覆盖,不复制照片、像素或人脸坐标。手机兼容性、根因、速度及自动三连拍仍待版本 `05` 验收。
本轮 `05` 联合回归 **199 项全部通过**,含 8 项隔离真实 WebGL 测试。新增测试验证默认坐标转换模拟的全零现象、关闭翻转后的非对称小 FBO 及完整人脸模型,并覆盖未修改读回出参、仅返回 ArrayBuffer/TypedArray/Promise、编译/链接返回 0、顶点上传、字节/浮点纹理故障共 9 类原生预检失败。失败时未注册 TF 后端、未加载模型,各类 GPU 对象释放。共享 H5 规则及模型构建校验通过,淘宝 IDE 本地重新编译完成(DevServer: Done)。实际手机结果仍待确认。
### 原生绘制与兼容选路(版本 06)
用户在 `LOCAL-FACE-20261102-05` 返回:原生清屏读回通过,非对称 shader 检查失败,`changedBytes=16`、3 项不匹配、预期校验和 2244/实际 2040,`getError=0`、FBO 状态 36053(完整)。实际校验和与原清屏颜色的 2040 相同。离线真实 GL 中,停止索引绘制、剔除所有三角形或把顶点上传清零均可复现这一组指标,因此它不能唯一确定绘制未覆盖、坐标、缓冲区上传或 GL 状态中的哪一项是手机根因。帧数及自动照片仍为 0。
版本 **LOCAL-FACE-20261102-06** 显式规范化 GL 绘制状态及颜色写入,在每次绘制前清黑以区分旧背景和实际绘制。先以不依赖 `gl_FragCoord` 的恒色 shader 分别验证 `drawArrays` 与索引绘制,再执行原非对称 shader、字节/浮点纹理检查。新增 `constantArrays`/`constantIndexed` 结果、可选顶点/索引缓冲区字节数及固定合成的四个 RGBA uint32 像素值;缓冲区元数据查询不额外作为硬门槛。
原生兼容策略按 `standard` → `buffer_copy`(将上传视图的字节紧密拷为标准 `bufferData` ArrayBuffer)→ `framebuffer_y`(仅为已绑定 FBO 补偿 viewport/scissor Y)→ `combined` 自测选择。仅原生 `VALUE_MISMATCH` 允许尝试下一路,其余错误明确停止;不能按手机型号认定某路可用。四路在同一个上下文上进行,每次检查结束清理本路图形对象;后续 TF 的 `default`/`packed_byte`/`unpacked_byte` 仍最多创建三个上下文。任何被选中的原生策略都须通过全部原生检查及后续原有严格矩阵/卷积数值校验和真实模型预热,保留 1.5 秒源帧新鲜度,不回退 CPU。
诊断同时保留 `nativeInitialStage`/`nativeInitialCode` 与最终 `nativeStrategy`,避免兼容通过后丢失最初失败信息。四像素、缓冲区及分层结果均来自固定合成输入,只在初始化时读取缓存,实际人脸检测不能覆盖。实际手机的选路、兼容性、速度及完整自动三连拍仍待版本 `06` 验收。
本轮 `06` 联合回归 **204 项全部通过**,其中 9 项为隔离真实 WebGL 测试。四种原生策略分别在第 1/2/3/4 次选中;模拟继承绘制状态、typed view 上传失效、viewport/scissor 坐标转换及组合情况后,实际模型和 CPU 人脸框/置信度/68 点几何对照通过。紧密 ArrayBuffer 拷贝保留子视图偏移和长度;索引绘制不执行时明确停在 `constant_indexed`,不会因 `drawArrays` 通过而准入。10 类原生故障保持注册后端/加载模型前停止,所有张量和跟踪 GPU 对象最终为 0。共享 H5 规则、模型生成及差异格式校验通过;淘宝 IDE 本地重新编译完成(DevServer: Done)。上述不能替代版本 `06` 的手机验收。
### 固定四边形的索引兼容(当前候选 07)
用户在 `06` 返回 `nativeInitialStage=constant_indexed`、`VALUE_MISMATCH`,由执行顺序可知标准路径清屏和普通恒色绘制已通过。索引缓冲为 12 字节,但长度不能证明其内容和原生索引执行正确。最后 `buffer_copy_framebuffer_y` 的 `constant_arrays` 黑色失败属于后续候选,不能覆盖初始故障。真实帧数、照片仍为 0。
`LOCAL-FACE-20261102-07` 在每次原生索引绘制前显式重新绑定索引缓冲。若标准普通绘制通过、索引数值失败,则在相同 native 策略下验证四顶点 `TRIANGLE_STRIP`,保持原非对称像素、字节/浮点纹理和矩阵/卷积严格自测及真实模型预热。依据固定四点与 `[0,1,2,2,1,3]` 的[上游 TFJS 数据](https://raw.githubusercontent.com/tensorflow/tfjs/tfjs-core-v1.7.0/tfjs-core/src/backends/webgl/gpgpu_util.ts),第二个 strip 三角形与原第二三角形同为 `(2,1,3)`;这只适用于自有固定 quad。
实际转换同时要求精确的 `TRIANGLES/6/UNSIGNED_SHORT/offset0` 参数,以及已追踪上传的 12 字节固定索引内容。视图只读取其字节范围;未知缓冲、不同索引、数字分配、部分更新或删除后不再准入,错误为 `DRAW_UNSUPPORTED`。诊断明确 `drawStrategy=indexed/quad_strip` 和实际 strip 绘制计数;替代路径的 `constantIndexed` 保持 `not_run`,另以 `constantQuad` 表示替代输出,不宣称原生索引接口通过。原生自测最多 8 次且共用一个上下文,逐路释放检查对象;1500ms 新鲜度和不回退 CPU 的规则不变。手机是否能完成初始化、速度及三连拍仍待 `07` 验收。
本轮 `07` 联合回归 **207 项全部通过**,其中 11 项为隔离真实 WebGL 测试。模拟原生索引绘制完全无效后,标准原生策略在第 2 次原生检查选中 `quad_strip`,原矩阵/卷积、模型预热、实际人脸及 68 点与 CPU 几何对照通过。另验证 10 类错误调用/索引/缓冲状态均不会发出替代绘制,正确的 typed-view 字节边界仍能识别;资源和张量最终为 0。共享 H5 规则、模型生成及差异格式校验通过,淘宝 IDE 本地重新编译完成(DevServer: Done)。手机版本 `07` 尚未验收。
1. 在电脑淘宝 IDE 顶部点“预览”,等待预览弹窗打开。
2. 在弹窗内点橙色“重新编译并预览”,等待生成新二维码;仅本地重新编译不会更新旧的手机预览。
3. 手机打开淘宝,使用“扫一扫”扫描这个新二维码,进入本次小程序预览。
4. 在手机登录首屏点黄色“相机测试(开发版)”;已进入首页时在开发工具区点同名入口,进入普通相机诊断。
5. 在“普通 camera · 独立诊断”确认版号 `CAM-NATIVE-20261102-04`,点顶部“开始自动三连拍测试”。
6. 进入后确认版号为 `LOCAL-FACE-20261102-07`。若出现“开启实时相机”,点一次,并由用户处理系统相机权限。先等待基础图形检查、数值校验和模型预热,约 30 秒内无需重复点击。
7. 将正脸居中并保持不动,等待自动倒计时及第一张拍摄;随后按页面提示分别露出右脸、左脸,完成三张自动拍摄和压缩。
8. 卡住、报错或完成后,点页面上方的“复制识别诊断”,回到聊天粘贴文字;无需发送照片或校验颜色。后续再验证镜头切换、方向、切后台恢复及拒绝授权。
9. 相机路线通过后,在配套服务版本下验收稳定识别 → 3 秒倒计时 → 三步完成 → 报告,分别测试游客和淘宝账号。再次核对左右脸语义与图层坐标。
## 已运行的验证
- 淘宝前后端、游客无落盘、幂等、账号隔离、AR 几何、相机生命周期及后台汇总共 **61 项本地自动测试全部通过**;共享 H5 评分/报告/拍摄规则哈希一致。
- H5 四组浏览器回归:游客不留存、HTTPS 微信登录/退出/跨设备、报告/后台/CSV、异步侧脸图层。
- Java Maven `clean package`,11 项测试通过;转发层原样支持新增路径,无需新增 Java 业务接口。
- IDE 曾发生桌面进程崩溃;已发现 Crashpad 转储,JavaScript 日志未记录对应原因。重新打开后再次编译成功(0 错误、0 警告),未新增转储;尚未确定首次闪退根因,不直接归为 AR/GPU。
## 部署顺序与边界
淘宝专用 Node 已按上方“最新目标”记录备份并更新游客、签名恢复和幂等代码;H5 及小红书服务尚未更新。本轮未迁移或删除业务数据,手机完整报告及真实网关仍待验收。
1. 先完成平台测肤资质/白名单及开放拍照组件核对,补齐采集授权;在平台支持的组件方案上通过实时识别、图像导出和三视角抓拍验收。现有 AR 验证只是其中一项技术验证。
2. 部署淘宝专用 Node,保留独立数据目录;验证新登录/游客/三图报告,不向 H5 生产数据目录写入。
3. 发布含本轮后台改动的 H5 管理服务,并明确配置只读 `JUZI_ADMIN_TAOBAO_DATA_ROOT`,保留原 nginx 管理员认证及 `/tb/` 网关入口限制。
4. 运行大报告/历史详情的真实网关与低内存手机测试。前台最近 5 份只是索引规则,后台照片和快照尚无自动清理;保留周期、备份、删除另行确定,不能直接合并两个文件目录。
5. 验收正式/测试环境隔离、资源域名、商品详情能力、新老报告及游客回跳;关闭开发入口。按现有交接要求处理测试凭据后再正式发布。
### 明确未完成
- 平台自带算法路线的商家/ISV 资质和白名单状态确认、正式拍照组件契约、品牌采脸授权及平台结果字段适配。
- 普通相机在手机上的本地模型耗时、转头方向、覆盖层和完整自动三连拍。AR 保留为可选对照,其默认自拍和图像导出仍未验收。
- 手机上的新游客接口和原系统相机报告故障最终复验(本地回归已覆盖,线上尚未更新)。
- 固定商品真机跳转、真实网关大响应及后台跨源上线配置。
- 照片保留/清理周期、数据库/对象存储迁移;本轮没有自动删除旧数据。
+40
View File
@@ -0,0 +1,40 @@
# H5 / 淘宝统一后台:数据源与部署
统一后台增加“全部渠道 / H5 / 淘宝小程序”筛选,覆盖记录、统计、失败记录及 CSV 导出。用户账号、照片、报告仍由各自业务服务保存,不做账号合并,也不改变 H5 用户历史。
## 默认行为
未配置额外数据源时,后台继续读取当前服务的 `JUZI_DATA_ROOT`,现有 CSV 列顺序和末尾“渠道”保持不变。新接口返回数据来源标识,不公开服务器目录。
## 接入淘宝只读数据源
在承载管理后台的 **H5 服务**环境中配置绝对路径,例如:
```ini
JUZI_ADMIN_TAOBAO_DATA_ROOT=/home/ubuntu/juzi-tb-data
```
需要部署本次修改的 `h5/server.js`、`h5/lib/admin-sources.js`、`h5/admin.html` 和 `h5/js/admin.js`,然后按原部署流程重启该 H5 服务。淘宝业务服务继续使用原数据目录。管理后台进程需要拥有该目录的只读权限;两个服务不得共用同一可写 JSON 目录。
该配置只增加管理读取,不修改淘宝账号库或检测文件,不迁移、不删除历史数据。配置的额外目录必须存在,与当前数据目录独立,并位于 H5 静态应用目录之外;配置错误时服务启动会明确失败。
后台仍经原有 nginx 管理员认证入口访问,保持业务服务监听回环地址;不要为汇总功能放开 `/tb/` 网关白名单或取消后台认证。小红书隔离实例不接入此额外数据源。
## 路由与数据口径
- 每条汇总记录包含 `source`、`admin_key` 和只读状态。两个数据源即使有相同记录 ID,也能分别打开正确详情、照片和原始 JSON。
- 详情和媒体请求通过后台已有路径的 `source=local` 或 `source=taobao` 选取已配置的数据源;HTTP 请求不能指定任意文件目录。
- 全局列表先合并再筛选、稳定排序和分页。CSV 沿用所有记录筛选条件,导出日期可另选;接入额外数据源后,仅在原有列末尾追加“数据来源、记录唯一键”。
- 按渠道统计检测记录及拍照流程;游客不留存的检测不会伪装成已保存报告。拍照漏斗是匿名客户端事件,不能视为人数或计费次数。淘宝尚未上报漏斗事件时显示零事件,不据此推断淘宝无检测。
- 外部数据源为只读:详情和失败页关闭重试按钮,服务端也拒绝外源重试。重试必须在原服务后台进行,避免写入错误渠道。
- “用户最近 5 份报告”是用户历史展示范围,不是后台磁盘保留期限。本次不会删除旧照片、原始结果、报告快照或检测记录;清理与备份周期需另行约定。
## 验证
```powershell
node --test taobao/test/admin-channels.test.cjs
$env:JUZI_ADMIN_BROWSER_QA = '1'
node --test taobao/test/admin-channels.test.cjs
```
测试使用独立临时数据目录和伪造图片,不请求 Face++,也不访问或修改线上数据。浏览器验证截图位于 `temp/taobao-adaptation/admin-channels.png`。
@@ -0,0 +1,24 @@
# 七项护理选三与关注优先
护理候选项按文档顺序为轮廓、皱纹、肤色、平滑度、水分、泛红、痘痘。使用未四舍五入的百分制分数,缺失/无效项剔除。
肤色独立采用color_score(扣斑后的色素80%+黑眼圈20%);水分独立采用已有交叉校验后的水分项。其他项使用各自现有P。肤色分析文案和护理分档同步使用独立肤色项,黑眼圈描述仍按真实检测程度;水分独立分档保持。小项P仅用于展示与护理选择,不调整维度或综合分。
1. 依小项P升序选择全局最低三项;同分时,当前优先关注大维度中的项优先,再按上述文档顺序固定选择。
2. 只在入选三项中,将属于优先关注大维度的项整体前置;组内P仍低分优先。不额外插入高分的关注项。
3. 各项按自己的分档取护理和生活方式全文拼接。低于80改善、80至低于90良好、90以上优秀;保留痘痘脓包/结节改善档、有皮损不优秀等现有文案约束。
4. 不足三项按有效数量展示,全部缺失显示原有无建议提示。
用户提供的本轮护理全文同步到reference和report-copy。原文比对发现皱纹改善档生活方式需删除旧版开头“保证睡眠,”,本次已修正,其余沿用用户提供文本。
新presentation新增careRule=seven-lowest-three-focus-first-v1;care条目保留key、name、dim、P、level、priority供核查,页面仅展示原有编号和正文。产品三选一、原始评分、历史快照和历史推荐不变;历史保存的护理文案不批量重生成。
## 真实案例回放
R20261015-111214-JKNS:关注紧塑力。肤色58.8、皱纹73、平滑度88.9为全局最低三项,最终皱纹→肤色→平滑度;水分97不入选,总分94不变。
R20261014-171215-CYLF:关注紧塑力。皱纹65、平滑度87.6、肤色88.8入选,最终顺序不变;水分93不入选,总分94不变。
## 验证
care-selection.js覆盖关注项两项/一项/零项、全部关注项、同分选取与固定顺序、原始小数、不足三项/全缺失、肤色水分独立档位、痘痘约束、全文一致与输入不变。同步复测肤色、水分、账号、持久化、历史快照、推荐、报告底部长文案布局和旧评分回归。
@@ -0,0 +1,13 @@
# 产品推荐按报告展示肌龄
规则版本 `2026-10-16-display-skin-age-v2`,替代原来的原始肌龄推荐依据。
推荐年龄必须与报告顶部显示的肌肤年龄一致:使用当前实际年龄 ±5 岁展示修正规则后的肤龄;没有实际年龄则显示原始肤龄,并使用同一个值推荐。原始肌龄缺失时展示值也缺失,不以实际年龄代替推测产品。
仅改变年龄输入,原有分档和边界调整条件保留:<35 岁基础 I、35~不足50 岁基础 II、≥50 岁基础 III;32~不足35 / 47~不足50 岁紧塑力低于8升一档;35~38 / 50~53 岁紧塑力至少9且透亮感或稳韧力任一至少9降一档;不足32固定 I;其余按展示肤龄。不改变评分、年龄显示规则或报告结论。
新报告保存新推荐。历史报告重新打开、后台列表/详情/筛选/产品分布及 CSV 按当前展示肌龄只读更新产品推荐;历史检测记录与原始报告快照不批量重写,分数、结论与护理顺序保留。后台明确标示「展示肌龄(推荐依据)」,原始肌龄仅供复核。
案例 R20261016-152835-5C0Q:实际44、原始肌龄34、展示肌龄39;按39岁推荐主场精华 II,不再按34岁推荐 I。
验证包括推荐边界/未舍入分数、真实案例等价数据、缺失年龄、新旧报告产品一致、前后台筛选和 CSV、原始数据不变以及异步皱纹浮层回归。不调用付费检测接口。
+2
View File
@@ -1,5 +1,7 @@
# 产品推荐规则:肤龄基准与边界调整 # 产品推荐规则:肤龄基准与边界调整
> 2026-10-16 更新:以下为原始肌龄依据的历史规则说明。当前已改为报告展示肌龄,分档和边界条件保留;以 [产品推荐改用展示肌龄-2026-10-16.md](产品推荐改用展示肌龄-2026-10-16.md) 为准。
状态:2026-10-12 用户已确认三个边界条件;已实现。规则版本:`2026-10-12-skin-age-v1`。 状态:2026-10-12 用户已确认三个边界条件;已实现。规则版本:`2026-10-12-skin-age-v1`。
## 已明确的要求 ## 已明确的要求
@@ -0,0 +1,13 @@
# 侧脸皱纹浮层异步加载
主测肤请求只等待主接口、评分及报告保存,不再同步等待左右侧脸的皱纹浮层。正面浮层随主接口返回。前端在报告切屏并经过两帧绘制后,才调用 `POST api/wrinkle-views`,获取该报告所属任务的左右侧脸标注。
加载期间保留两张侧脸原图,各在图片上覆盖半透明「皱纹标注 / 加载中…」提示,不在图片下方重复显示加载文字。完成或失败后自动移除提示浮层,图片尺寸、裁切和点击查看大图规则不变。完成后只更新侧脸图片,不重算分数、不改结论或护理建议。失败/超时仅退回原图提示,报告继续使用。扫描等待页 30 秒兜底只针对主报告,不受浮层任务影响;可选浮层请求前端预算 65 秒,服务端两次顺序请求仍各最多 20 秒、间隔 4 秒。
使用随机任务令牌并绑定服务器会话所属账号;不同账号无法获取他人的标注。重复/并发请求共用同一任务,成功或失败结果都缓存,不自动重新收费调用。微信任务元数据及完成结果持久化,补写原报告 payload、后台标注状态和原始结果,不修改 vm/presentation。服务重启后可继续尚未启动的任务;已标记运行但被重启打断的任务不自动重跑,避免重复调用。老报告无任务令牌时不自动补检。
游客任务、照片及结果仅存内存,五分钟后清除,不写游客身份或报告到磁盘。任务从未被报告页请求时,不调用额外侧脸检测。微信任务可从已有报告快照恢复照片,无需用户重新上传。离开报告或切换账号后,迟到结果不修改当前页面;返回同一报告可显示已加载数据。
后台 `cost_ms` 现在代表主报告检测链路,额外浮层耗时记录为 `wrinkle_cost_ms`。正常案例从进入报告的等待中移走固定的两次 4 秒间隔及两次侧脸请求耗时,但不承诺固定秒数,不减少已实际执行的接口调用数量。
验证使用隔离数据目录和模拟上游,不产生付费检测:主响应只有一次上游调用、报告显示后才请求侧脸、总共三次模拟调用、重复并发与重启缓存、不同账号隔离、游客无磁盘写入、离开/返回和失败降级,以及评分和报告结论不变。
@@ -0,0 +1,29 @@
# 原始肌龄显示与鸿蒙滚轮兼容
## 范围
- 生物可视年龄直接展示算法原始肌龄,取消实际年龄 ±3 的展示修正。原始肌龄缺失显示 `--`,不以实际年龄或旧展示值补齐,不额外四舍五入。
- 新报告 `skinAgeDisplay` 与 `skinAgeRaw` 相同,保留兼容字段 `ageClampNote` 但值为空。推荐规则、评分和其他话术不变。
- 历史报告回看优先读取快照 `vm.skinAgeRaw`;更老的快照缺该字段时,从保存的 `payload.result.skin_age` 读取。忽略旧的钳制展示值,不改写存量快照,不重算历史推荐/文案。后台列表、详情、CSV 的展示肌龄也改为读取原始肌龄,避免前后台不一致。
- `mobile-select@1.4.0` 原来通过 UA 字符串选择 touch/mouse,无法识别不带 Android 的 OpenHarmony/ArkWeb。保留上游版本与 MIT 声明,添加本地补丁:通过 `navigator.maxTouchPoints > 0` 或 `ontouchstart` 能力选择触摸事件,无触摸能力则使用鼠标事件;不再按系统名称判断。
- 同时按事件的 `touches/changedTouches` 读取坐标,不依赖全局 `TouchEvent` 构造函数,避免无该构造函数的鼠标环境报错。弹层挂载、页面禁滑和滚轮联动逻辑不变。
## 回归
```bash
node h5/test/raw-skin-age.js
node h5/js/skin-score.js
node h5/test/recommendation.js
node h5/test/regression.js
node h5/test/report-persistence.mjs
node h5/test/harmony-picker.mjs
node h5/test/profile-interaction.mjs
node h5/test/guest-privacy.mjs
node h5/test/report-touch-layout.mjs
```
鸿蒙专项使用 Chromium 的真实触摸/鼠标分发配合不同 UA 与能力配置,逐项验证年、月、省、市四轮、地区联动、确认回填、重开定位和表单提交。覆盖 Android 触摸、OpenHarmony 5、OpenHarmony 6+微信、桌面 UA 的触摸设备、无 TouchEvent 构造函数的桌面鼠标、Android UA 的无触摸环境。
这属于 UA/能力兼容回归,不等同于鸿蒙真机或 ArkWeb 内核验证。测试使用隔离临时目录或拦截账号请求,不写线上报告,不调用付费检测。历史报告测试故意保留旧展示值 57 岁,确认 H5/后台改显示原始肌龄且原文件字节不变。
用户已确认发版。本次与《微信退出登录-2026-10-13.md》的修改一起提交,采用 Git bundle 快进同步到 `/home/ubuntu/juzi`,重启 `juzi-h5.service` 后校验线上资源、健康与浏览器交互。不代用户推送 origin,不改写既有报告或调用线上付费检测。摄像头切换不在本次修改范围内。
+9
View File
@@ -0,0 +1,9 @@
# 取消每张照片拍后的二次检测
删除 `checkStill` 以及实时抓拍、系统相机/相册选图路径对它的调用。保存/读取照片成功后直接进入该张拍摄完成与下一步流程,不再检测已保存照片的人脸、角度、亮度或等待检测模型。
保留实时预览阶段的人脸识别、角度、位置与稳定性检测;光线仍仅提醒。保留 321 动画、补光、空帧/图片读取失败提示和取消/后台中断的过期回调保护。三张完成后的测肤报告接口调用不变,该接口仍可能因无法检测照片等原因返回失败。
拍摄观测诊断现在记录最后一次通过的预览帧,明确标记 `check_stage: preview`;系统相机/文件及手动降级无预览数据时标记 `check_stage: none`,不伪造拍后检测通过。策略版本 `2026-10-16-preview-only-v11`。
验证:真实本地模型自动三连拍时禁止调用静态照片检测;文件路径直接推进及取消保护;321 倒计时与流程漏斗回归。不调用付费接口。
+25
View File
@@ -0,0 +1,25 @@
# 后台北京时间统一
## 修改
- 后台记录列表、失败与异常、详情的创建/更新时间统一显示北京时间(UTC+8)。
- 日期筛选和CSV导出筛选按北京自然日计算,例如10月14日覆盖UTC的10月13日16:00至10月14日15:59:59.999。
- 完整带时区ISO查询仍按其实际时间点比较,完整结束时间包含该毫秒;不带时区的完整时间按北京时间解释。
- 今日检测量、近14日趋势按北京日期归类,不再直接截取UTC日期。
- CSV创建时间转换为北京时间,列标题明确时区;列表筛选、趋势和导出说明也标注北京时间。
- 前后端共享 `js/beijing-time.js`。原始记录、UTC时间字段及接口时间不做批量修改,评分和推荐不变。
- 无效日期及颠倒区间返回400,不产生误导结果。
## 验证
```sh
TZ=UTC node h5/test/beijing-time.js
TZ=America/New_York node h5/test/beijing-time.js
TZ=Asia/Shanghai node h5/test/beijing-time.js
node h5/test/admin-beijing-browser.mjs
node h5/test/report-persistence.mjs
```
独立测试服务使用临时记录目录,服务进程设为纽约时区,浏览器分别使用洛杉矶、UTC、上海时区。固定“当前时间”为北京时间凌晨00:30(UTC仍为前一天),验证北京午夜前后毫秒边界、单日/单边/带时区筛选、CSV、今日及每日统计、创建与更新时间显示、无效日期。测试后逐字比较原始UTC记录不变,无真实账号/报告或付费检测调用。
当前本地实现,尚未提交或部署。
@@ -0,0 +1,36 @@
# 基础信息记忆与肤质拖动
> 2026-10-13 调整:以下偏好缓存仅对微信登录生效。游客仅保留当前页面操作中的临时表单值,不写缓存、不回填历史;刷新后重新填写。详见《游客不留存与弹窗护理间距调整-2026-10-13.md》。
## 范围
- 出生年月、性别、地区每次点滚轮“确定”即保存,不必拍照或提交报告。
- 日常肤质切换后也保存。再次进入基础信息页,包括刷新、返回、重新登录同一账号时,默认使用本机上次已确认的值;滚轮重新打开也定位到原选择。
- 保存出生年月而非固定年龄,进入页面和提交前按当前年月重新计算实际年龄。
- 使用当前登录账号的 `kind + id` 隔离本机缓存。切换微信/游客或另一个账号时,页面值与滚轮位置一起重置/恢复,不串用其他账号信息。
- 这是本机浏览器偏好缓存,不新增服务端档案同步接口;同一微信账号换手机仍可看服务器报告,但本次新增的基础信息默认值不跨设备。清除浏览器数据后需重新填写。
- 老版本没有单独保存出生年月,无法准确反推出原选择;更新后需确认一次,从此开始记住。缓存被禁用或容量不足时保持当前页面可用,在该页面内记忆,不保证刷新后保留。
- 缓存只包含出生年月、性别、省市、肤质序号,不复制身份凭据、照片、报告。
## 肤质交互
- 水平方向拖动时,图片、名称与说明一起实时跟随手指,不再等松手才切换。
- 松手按距离/轻扫速度吸附到相应选项;短距离慢拖回原项。
- 首尾拖动有弹性阻力,松手回到有效选项;系统取消手势时复位。
- 横竖方向锁定,保留 `touch-action: pan-y`,不阻塞页面上下滚动;屏幕尺寸变化后保持选项对齐。
- 原有箭头、圆点保留;补充左右方向键支持。
- 不改变 PSD 素材、颜色、页面布局、拍摄约束、评分或推荐规则。
## 回归命令
```bash
node h5/test/profile-store.js
node h5/test/profile-interaction.mjs
H5_BASE_URL=https://1me.xajuzi.com/state/ node h5/test/profile-interaction.mjs
```
浏览器测试使用真实页面与 mobile-select 控件,以模拟账号和真实触摸事件检查恢复、账号隔离、跟手位移、吸附、短拖、轻扫、首尾阻力、系统取消、纵向手势以及不同屏宽。测试只跳过字体下载、拦截登录会话并暴露滚轮实例用于选择测试值;所有非 GET 请求均阻止,不会新增线上账号、报告或付费检测调用。
本地已通过上述两套新增测试,以及第四轮反馈浏览器闭环、登录加载与超时、报告持久化、账号/OAuth 隔离、真实本地模型自动三连拍与最终 JPEG 复验、推荐 126 用例及算法真值 86 断言。
部署目标为 `ubuntu@1me.xajuzi.com:/home/ubuntu/juzi`,基线 `c9352fe`。沿用 Git bundle 快进同步,不代用户推送 origin,不改服务器环境配置或历史数据。上线后核对静态文件哈希、服务健康、匿名访问保护,并执行同一套只读模拟账号浏览器测试。
@@ -0,0 +1,89 @@
# 第三轮反馈:PSD 对齐、验收及上线记录
日期:2026-10-12。依据 `new_feedback/第三轮反馈20261012` 的客户会话、四张批注及新版 `UI外发版10.09.psd`。本轮已完成本地开发、Git 提交及生产部署;按此前约定未推送 origin,由用户自行推送。
## 已完成
| 范围 | 实现 |
| --- | --- |
| 首页/引导背景 | 直接导出 PSD 中三行口号、Logo 与中文组合,保留原始字形、字重及可见高度比例;首页两个按钮的静态文字也从 PSD 导出,不再依赖手机系统字体。 |
| 介绍弹窗 | 完全采用用户确认的 PSD 原文,按原稿校准弹窗位置、标题、正文、勾选区及按钮;服务端保存和回看保持不变。 |
| 拍照引导 | 固定设计稿的两行断句、字号及文本栏;弹窗固定约 308×313 CSS px,320px 窄屏也能完整展示,不机械缩小文字。 |
| 产品卡 | 使用 Semibold 原字体,标题约 14.47px、名称约 11.57px;校准左侧约 26.74px 留白、品牌标识及纵向位置。右图按 PSD 白卡的实际可见区域导出,不再二次居中裁切。 |
| 生成报告等待 | 三张照片拍完后显示 PSD 原始黑底、白色人脸、黄色扫描区和「检测中…」。相册/翻转图标仅是图片内容,不可点击、不可聚焦;保留明确取消、返回、失败恢复及请求完成后的报告跳转。 |
产品仍只展示算法推荐的一款;没有修改评分、肤龄、推荐规则、三视角顺序或自动拍摄阈值。历史报告保留原推荐和原文案,仅按已有产品档位更新展示图片的裁切,不回写历史数据。
用户确认的弹窗原文:
> 我们将采集您的面部影像,为您生成专属肌肤分析与状态管理方案。所有数据严格保密,仅用于本次检测分析。
引导正文的 PSD 断点(包括「确/保」之间的换行):
```text
为使测试结果更准确,请关闭美颜/滤镜、并确
保光线充足均匀。
```
## 原稿与导出可追溯性
- 新 PSD 为 7330×16411、11 个顶层分组,新增第 9 号「扫描」,原「登录」变为第 10 号。旧导出器改为按唯一名称定位「登录」,拒绝缺失或重名,避免切图串页。
- 本轮选择性导出脚本:`tools/export_third_feedback_assets.py`。运行方式:
```bash
python3 tools/export_third_feedback_assets.py 'new_feedback/第三轮反馈20261012/UI外发版10.09.psd'
```
- 原 PSD SHA-256:`c7698f5900adc7476286818450d928e8fa2ce2e58cda09375ff2b861629fd58f`。
- `design/ui-20261012/manifest.json` 记录源文件校验、图层路径、原始坐标、字体及变换矩阵、输出尺寸和逐资源 SHA-256。测量包含文字图层的缩放,不直接把原始 FontSize 当最终字号。
- 另读取矢量路径确定原稿圆角:弹窗 25 个设计像素、按钮 20px、产品卡 15px(375px 基准分别约 8.68/6.94/5.21 CSS px),不是继续沿用旧版估计值;产品卡宽高按 984×503 的原稿比例校准。
- 八份前端资源位于 `h5/assets/img/ui-20261012/`;PNG/无损 WebP 均来自 PSD,不使用生成式替代图、不改人物或产品包装。原 10.09 资源和字体文件保留不动。
- 扫描页须整体合成,保留跨图层剪贴依赖,再裁掉 248px 微信原生顶栏;单独导出图标组会漏掉人脸。导出器及浏览器测试都检查白色人脸、黄色下半脸像素。
- 原稿预览保存在 `design/ui-20261012/page-{0,1,4,9}.webp`,仅用于核对,不将整个交互页面替换为背景截图。微信原生导航由宿主提供,不叠加进 H5。
- 等待画面保持原始长宽比,异形屏用黑色留白适配;额外的「取消」为保留已有功能的必要入口。不新增虚构百分比、扫描动画或固定长时间等待,沿用原有至少 1.2 秒防闪及 150 秒请求预算。
- 扫描图未加载或加载失败时仍显示「检测中…」文字并可取消,不留下无提示黑屏;正常加载后仅展示原稿文字,不重复叠字。
- 静态品牌图规避系统字体差异;其余动态文字使用完整原始 MiSans,保留 `font-display: swap`。小字号开启几何精度渲染,减少浏览器将小数像素字宽取整造成的断行偏差;不同系统的文字抗锯齿仍可能有细微区别。
## 本地验证
已通过以下检查,未调用收费 Face++ 或改动线上账号、照片、报告:
- 第三轮专项:源资源校验/PSD 字体与原文、320/375/390/414/480px 弹窗和产品排版,三款推荐产品、固定断行、字体失败降级、短视口及等待页六种尺寸;实际三张上传推进、装饰图标、键盘焦点、取消/迟到响应、失败重试、成功进入报告。
- 第二轮专项:全屏相机、三种圆角轮廓、五官辅助线、震动安全降级、450ms 自动抓拍条件、协议及报告实际字体、首屏无大字体/模型请求。
- 原反馈回归:报告图放大、三个维度独立滑动切换、上下滚动、七张真实数据可视化及皱纹/毛孔文案。
- 推荐规则 126 个用例、原算法 86 条断言,以及 150 秒超时/迟到回调单测。
- 隔离服务器的报告持久化:后台/H5/CSV 一致、最新五份报告、用户隔离、历史快照不回写,包含旧图片路径升级展示的验证。
- 登录及模拟微信:隔离 HTTPS `/state/` 回调、Secure cookie、账号切换、跨设备同账号和不同账号隔离;不是对真实手机授权的替代验收。
- 登录性能回归:150ms 延迟、约 2Mbps 下载及 4 倍 CPU 降速下冷启动约 **1.64 秒**、资源 **264610 字节**;新增扫描图约 11.7KB,在进入档案/相机后才加载,不竞争登录首屏。
- 切图映射 3 个单测、JS/Python 语法及 diff 空白检查。按项目约定执行 `graphify update .`(AST 更新,无付费语义提取)。
主要复验命令:
```bash
python3 tools/test_export_psd_assets.py
H5_ARTIFACT_DIR=/tmp/juzi-third-final node h5/test/customer-feedback-third.mjs
node h5/test/customer-feedback-second.js
node h5/test/customer-feedback-second.mjs
node h5/test/customer-feedback.js
node h5/test/customer-feedback.mjs
node h5/test/recommendation.js
node h5/test/regression.js
node h5/test/analyze-timeout.js
node h5/test/report-persistence.mjs
node h5/test/accounts.js
node h5/test/login-performance.mjs
node h5/test/wechat-browser.mjs
```
## 生产部署及只读验收
- 功能提交:`c7b085e795f21565a9802896041b447970f44ab9`(`feat(h5): align third feedback with latest PSD and scanning screen`)。
- 部署目标:`ubuntu@1me.xajuzi.com:/home/ubuntu/juzi`;页面为 <https://1me.xajuzi.com/state/>。部署前确认服务器工作区干净,与本地基线 `f3e7eac` 一致,通过 Git bundle 快进更新,不覆盖服务器私有配置或用户数据。
- 因后端启动时加载 `report-ui.js`,同步代码后重启 `juzi-h5.service`,保证前后端使用同一版本。服务 `active`、自动重启次数 `NRestarts=0`,健康接口返回 `ok:true`;检查时最近 15 分钟没有 systemd 错误级日志。
- 线上首页、两个 JS 模块、新 CSS 及八份新素材均返回 200,与本地逐文件 SHA-256 一致,并带有 `Cache-Control: no-cache`。
- 线上第三轮浏览器专项全部通过:五种宽度弹窗/断行/产品排版、三款产品、三张照片推进至扫描页、白色人脸和黄色扫描区、六种视口、装饰图标、取消/迟到响应、失败重试和成功报告、字体及扫描图失败降级;没有页面 JS 异常。检查了线上首页、等待画面和产品卡截图。
- 线上第二轮浏览器回归也通过:首屏不请求完整字体/人脸引擎、协议字体实际加载、全屏相机和三种圆角轮廓/五官辅助线、报告数字及标题的真实 PSD 字体、五种宽度无溢出;没有页面 JS 异常。
- 访问控制检查:未登录报告接口、后台页面和统计接口均返回 401;私有身份文件和 `server.js` 返回 404;微信登录配置仍启用。
- 生产浏览器回归使用内存中的登录/分析响应及本地示例图片,不提交真实检测、不调用收费 Face++、不创建或改动线上账号和报告;不能替代真实手机微信授权及摄像头验收。
仍需用户在真实手机微信中做最终视觉/相机验收。客户反馈原件及 PSD 未修改、未纳入提交;服务器凭据、环境配置、已有照片和报告保持不动。
@@ -0,0 +1,82 @@
# 12 日第二批:明确项修改
依据用户提供的 `12 日第二批.docx`(正文与 6 张截图)、随后补充的首页高清批注图及最新 `UI外发版10.09.psd`。原第 3 张首页截图仅 73×160 像素,未猜测;收到高清图后补齐下面两项。
## 已修改
### 首页高清批注
- 将三行口号中的中文「以我为始 自有表现」独立为 700 字重,英文继续使用 400 字重,保留三行断句与位置。PSD 对应中文 `0/4` 为 Demibold、英文 `0/5` 为 Regular,确实有粗细区别。
- 放大底部「肌肤状态检测」到 24 CSS px、700 字重,与原始 Logo 约 26 CSS px 的可见高度对齐;Logo 保持原图宽高比。Logo 与文字均固定 CSS px,不再因屏宽缩放 Logo 而让二者比例失衡。
- 首页继续使用系统字体表达字重区别,不提前下载约 5 MB 的完整 MiSans 字体;不改动或子集化字体。系统字体字形的细微差别仍依手机系统而定。
- 在 320、375、390、414、480 五种屏宽验证:品牌行不溢出,口号仍三行,登录状态不遮挡口号,首屏不新增字体或检测引擎请求。
### 拍照展示
- 去掉人脸外轮廓的白色描边,以及原来无脸呼吸/合格变绿的轮廓描边;保留明暗镂空、五官虚线、扫描提示和「保持不动」反馈。
- 视频由 85% 缩小预览恢复为铺满视口,避免外侧矩形取景边界;镜像仍保留。屏幕人脸中心坐标的缩放系数同步为 1,距离、姿态、光线判定阈值不变。
- 对 PSD 导出的折线轮廓做最大 2 CSS px 的局部圆角化,使用二次曲线、不过冲、不改变轮廓坐标系。镂空与扫描裁切使用同一份平滑轮廓。原始 `CameraGeometry` 不修改。
- 原始视频抓帧、上传图片、报告图片处理与评分/推荐逻辑不变。
### 准备捕捉提醒
原来仅在补光抓拍瞬间请求 30 ms 震动,改为首次满足检测条件、开始「保持不动」时请求 `[70, 40, 70]` 两次短震动。连续合格 450 ms 后仍按原流程自动抓拍,不增加倒计时。抖动重试时最多每两秒请求一次,每步重置。
实时相机的手动快门模式仍在点击抓拍时请求提醒;自动模式不会在抓拍瞬间再重复震动。
API 不存在、返回 false 或抛出异常时安全跳过,不能因震动失败卡住拍摄。此处没有接入未经验证的微信私有接口,也不声称所有 iPhone/微信环境均可震动。振动接口存在浏览器、用户激活和系统设置限制,实效必须在手机微信上测试;参见 [MDN Navigator.vibrate](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/vibrate)。保留原有文字、语音和补光反馈。
### 字重
直接读取 PSD 字体元数据,明确区分而非将所有文字一起加粗:
| 位置 | PSD 图层 | 原始字体 |
| --- | --- | --- |
| 协议弹窗标题 | `1/3/0` | MiSans-Demibold |
| 拍照引导弹窗标题 | `4/7` | MiSans-Demibold |
| 正/右/左拍照步骤 | `5/3`、`6/2`、`7/2` | MiSans-Demibold |
| 综合评分、肌龄 | `8/12/2`、`8/12/3` | MiSans-Demibold |
| 三维度数字 | `8/12/7/2`、`8/12/7/9`、`8/12/7/13` | MiSans-Demibold |
| 报告标题、三维度大标题 | `8/13`、`8/9/17`、`8/10/20`、`8/11/17` | MiSans-Semibold |
| 护理建议、产品区大标题 | `8/2/0/1/0`、`8/1/3` | MiSans-Semibold |
项目原来只有 Regular、Semibold,数字和部分提示还使用 Regular 字重。现从[小米官方字体包](https://hyperos.mi.com/font/zh/download/)增加完整的原始 Demibold 文件(4994364 字节),未转换、未子集化、未修改字形;SHA-256 为 `7afe3737efaf6a137db0ef4857fbe4033a8802756485ebc7f008e96a4bdedcf2`。保留字体归属声明。
Demibold 使用独立 CSS 字体族,仅应用于本轮明确需要它的位置,避免影响原有正文、Tab 和其他 Semibold 元素。报告大标题按原始 Semibold 字形显示,不能承诺所有原本已经正确加载 Semibold 的标题都会额外变粗。
字体样式注册提前到打开协议/拍照引导弹窗时,解决首次协议弹窗尚未注册项目字体的问题。继续 `font-display: swap`,不阻塞交互;登录、首页和账号恢复仍使用系统字体,不在首屏请求完整字体或模型。新增完整字体约 5 MB,在需要的页面下载并缓存;弱网下载完成前仍可能短暂显示回退字体。固定字号与产品两行断句保持不变。
## 验证与状态
```bash
node h5/test/customer-feedback-second.js
H5_ARTIFACT_DIR=/tmp/juzi-feedback-second-final-20261012 node h5/test/customer-feedback-second.mjs
node h5/test/customer-feedback.js
H5_ARTIFACT_DIR=/tmp/juzi-feedback-second-regression node h5/test/customer-feedback.mjs
node h5/test/recommendation.js
node h5/test/analyze-timeout.js
node h5/test/login-performance.mjs
node h5/test/e2e-browser.mjs
H5_ARTIFACT_DIR=/tmp/juzi-feedback-second-ui-20261012 node h5/test/ui-20261009.mjs
```
专项测试覆盖三种轮廓的有界曲线和无原始数据修改、异常输入回退、震动缺失/拒绝/抛错容错、抓拍前触发及频率限制、原有 450 ms 自动抓拍时机;浏览器核验实际渲染字体(不仅是 CSS 数值)、五种屏宽的全屏预览与固定字号、三种状态均无轮廓描边、五官引导保留、登录首屏无大字体/模型请求。专项浏览器测试拦截会话并使用本地合成视频流,不写入后端账号、报告,也不调用收费检测。
已完成本地开发及验证,首页高清图的两处批注也已补齐,并按用户要求提交 Git、部署公网。未推送 origin,由用户自行推送。真实手机震动实效仍待确认。
## 公网部署与复核(2026-10-12,北京时间)
- 功能提交:`fe9623a`;服务器 `/home/ubuntu/juzi` 通过 Git bundle 从 `abb1c4c` 快进部署到此版本。随后同步本节部署记录及公网字体检查的测试时序修正。
- 地址:`https://1me.xajuzi.com/state/`。本轮仅变更前端资源,服务按请求读取磁盘文件,因此无需重启 Node 或 nginx;`juzi-h5.service` 持续 active,进程 PID `3481826` 未变化。
- 公网 HTML、三份 CSS、`app.js`、新增 Demibold 字体均与本地 SHA-256 一致,资源保留 `Cache-Control: no-cache`,刷新会重新校验版本。
- 健康接口 200/ok,微信配置 `wechat: true`;匿名报告接口和后台页面/统计接口均 401,源码与私有账号数据路径均 404。未修改微信私有配置、真实账号、照片和报告,也未调用收费检测。
- 专项浏览器测试分别在本地及公网通过,确认五种屏宽首页字重与视觉高度、协议/报告实际渲染字体、全屏取景/无轮廓描边/圆角与五官引导、无 JS 错误。公网测试仅拦截会话、使用浏览器本地合成视频和模拟报告,不向后端写账号或报告。
- 发现并修正测试本身的时序问题:`<link>` 插入 DOM 不代表字体声明已加载;测试先等待 `document.fonts` 注册后再检查字体。未因此更改页面行为。
- 登录性能回归通过:模拟 150 ms 延迟、约 2 Mbps 下载、4 倍 CPU 降速,冷启动约 1.66 秒、资源约 264 KB(测试环境数据,不代表所有手机实测);登录及首页没有新增字体/模型下载。
- 126 项推荐规则测试、抓拍提醒/容错测试、原反馈及超时测试通过;隔离 HTTPS 环境的模拟微信授权、跨设备报告/账号隔离和切换登录回归通过。本轮未替代真实手机微信授权或震动验收。
公网专项验证命令:
```bash
H5_BASE_URL=https://1me.xajuzi.com/state/ H5_ARTIFACT_DIR=/tmp/juzi-second-deploy-online-20261012 node h5/test/customer-feedback-second.mjs
```
@@ -0,0 +1,88 @@
# 第四轮反馈修改与验收
依据:`feedback/第四轮反馈` 的客户会话、6 张批注图及傅艳在本线程中的逐项确认,包含随后指定的皱纹三档原文。按用户要求提交及部署,不代替用户推送 origin。
## 已确认并实现
1. 登录按钮、登录成功弹窗、首页/我的报告状态及相关引导统一使用「游客登录/游客已登录」,内部仍为原 `browser` 身份,不另建账号、不丢失已有报告。明确说明游客报告仅能在当前浏览器查看、不能跨手机同步。
2. 拍摄质量在前端收紧,不新增服务端质量拦截、重拍要求或额外质量检查接口。服务端原有业务错误处理不变;仍按现有算法剔除无效数据。成功报告不显示侧脸角度无效、建议重拍等备注,后台保存完整备注及结构化质量标记。
3. 皱纹结论使用傅艳最后指定的「改善/良好/优秀」三档原文(下附)。客户正文不再附加 12 分区/9 指标长文;分区原始指标保存在结构化快照,并在受保护后台单独展示。历史报告仅显示原有结论、不再展示分区附文,不回写历史快照。评分、肤龄、推荐规则及照片视角归属不改。
4. PSD 原始画面拆为白脸底图和透明人脸遮罩,黄色扫描区约 1 秒完成一次上下往返。成功返回至少等到实际 CSS 动画完成一轮;响应慢时不额外延长等待,取消/失败不为动画延迟。相册和翻转图标依旧仅装饰、不可操作,取消入口保留。
5. 三维度评分下方恢复浅灰提示区及黄色竖线,固定两行结构:`近期优先关注:{维度名}` 与客户指定建议。根据已保存的原始维度 P 值选最低项,不使用前端一位小数排序;并列依次紧塑力、透亮感、稳韧力,缺失不作 0、全部缺失则隐藏。历史报告也按其保存分数展示,不重算、不回写历史快照。
| 最低维度 | 建议原文 |
| --- | --- |
| 紧塑力 | 强化紧塑力,提拉轮廓抚平细纹。 |
| 透亮感 | 提升透亮感,补水提亮加强防晒。 |
| 稳韧力 | 增强稳韧力,舒缓泛红强韧屏障。 |
### 皱纹(紧塑力)原文
- 【改善】全脸共检测到皱纹 {wr_cnt} 条、细纹 {fl_cnt} 条,面中、口周、眼周等区域已出现静态纹倾向,纹路已呈现固化趋势,并非临时性干纹。
- 【良好】全脸共检测到皱纹 {wr_cnt} 条、细纹 {fl_cnt} 条,面部整体平整,仅眼周或口周有细微的干纹,无明显静态深纹,纹路面积占比相对较低。
- 【优秀】面中、口周、眼周等各区域均无明显皱纹沟壑,无静态深纹与粗纹,各类纹路面积均处于极低水平,面部整体平整细腻。
缺失的计数仍标记为未获取,不伪造为 0;分档计算保持不变。
## 前端参数及限制
参数版本 `2026-10-12-fourth-v1`,集中在 `h5/js/capture-quality.js` 和 `h5/js/head-pose.js`。
| 条件 | 之前 | 本轮 |
| --- | --- | --- |
| 人脸检测置信度 | ≥0.3,取单一人脸 | ≥0.5,检查检测到的人脸数量为 1 |
| 框高/原始帧高 | 30%~68% | 34%~64% |
| 水平居中 | 偏离≤18% 屏宽 | 偏离≤12% 屏宽 |
| 完整入镜 | 没有单独边缘门限 | 检测框距原始帧边缘≥2% |
| 正脸 yaw/pitch | ≤15°/18° | ≤10°/12° |
| 侧脸幅度/pitch | 28°~75°/≤20° | 35°~65°/≤15° |
| 歪头 roll | 不判断 | ≤10° |
| 脸部平均亮度 | 65~235 | 75~225 |
| 稳定累计 | 450ms,短暂漏检不打断 | ≥650ms 且≥3 合格帧;任何漏检/异常打断,帧间隔>2000ms 打断 |
| 稳定期间漂移 | 未单独判断 | 相对起始合格帧 yaw/pitch≤5°,中心 x/y≤原帧 3.5%,框高占比变化≤4.5 个百分点 |
| 最终 JPEG | 实时抓拍后直接推进 | 抓拍后本地复验通过才推进 |
系统相机/相册文件使用同样的单人、完整入镜、距离、角度、光线门限;由于系统照片可能已镜像,沿用不判断左右方向的兼容方式,并记录 `direction_checked:false`。模型加载中最多等待 8 秒;模型确实不可用时保留原有手动/文件降级通路,并记录 `unchecked/model-unavailable`,不伪称已校验。
这些角度是 68 点几何经验估计,不是服务端 `jawline_quality` 的直接替代;只能尽量提高合格率,不能承诺手机实拍 100% 通过。仍需提测收集不同机型、光线和转头方式的样本后校准。未修改取景轮廓、自动拍摄三步顺序或语音资源。
真实本地识别模型的自动流程测试发现:无硬件加速时每帧推理约 1.05 秒,若将间隔上限设置为 1 秒会一直停留在「保持不动」。因此最终上限采用 2 秒;650ms/至少 3 帧及全部质量/漂移约束不变,慢设备按实际推理耗时累计,不为抢速度跳过合格帧。
## 后台校准留痕
- 现有测肤请求携带参数版本、三视角的本地观测(yaw/pitch/roll、置信度、亮度、框比例及中心位置),不新增网络往返、不向前端下发服务端私钥。
- 后端仅接收限定字段和有界数值,明确标记 `client-untrusted`。当前参数版本对应的配置快照一同保留,避免以后调整参数后无法追溯。
- `capture_quality` 单独保存接口左右侧脸的 quality、下颌线角度、valid/invalid/unavailable 状态;quality 不为 1 或角度缺失只做标记,不改变原成功响应、不要求用户重拍。
- 后台列表在拍摄状态下标注异常;详情增加「拍摄质量与前端校准」;CSV 增加质量状态及参数版本。旧记录不批量补写,仍可查看原有计算备注/原始返回。
- 校准时结合后台原图、前端参数/观测及接口质量结果分析,不将客户端自报当作可信质量结论;本轮不引入自动学习或自动修改线上参数。
## PSD 来源
`tools/export_fourth_feedback_assets.py` 从原 PSD 第 9 页隐藏原扫描效果图层 `9/2/2`,保留原人脸、圆角边框、文字及装饰图标;生成 `scan-base.webp` 和具有真正 alpha 通道的 `scan-face-mask.png`。不修改原 PSD,不使用 AI 重新绘制人脸。
`design/ui-20261012/scan-motion-manifest.json` 记录原 PSD SHA-256、裁切范围、图层路径、遮罩范围、动画周期、色号及资源 SHA-256。新底图/遮罩只在进入档案/相机后加载,登录首屏不请求扫描素材、完整字体或人脸识别引擎。
## 验证
- `node h5/test/customer-feedback-fourth.js`:本地角度/光线/距离/多脸、稳定时长/帧数/漏检/漂移、关注维度/并列/缺失、历史皱纹快照不回写、后台诊断字段清理。
- `node h5/test/customer-feedback-fourth.mjs`:隔离服务器和模拟上游;quality=3 仍成功保存,仅后台标记;后台/CSV/历史报告不回写;游客登录及状态;五种屏宽;快速成功响应等待实际动画一轮。
- 第三轮专项回归:PSD 原文/字体、弹窗、三款产品、装饰图标/键盘、取消/迟到响应/失败恢复及六种等待视口;新动效替代原静态图断言。
- 第二轮、原客户反馈浏览器回归,以及报告持久化/账号隔离/模拟微信回调均通过;推荐规则 126 个用例、原算法 86 条断言、请求超时测试通过。
- 登录性能:150ms 延迟、约 2Mbps 下载及 4 倍 CPU 降速下最后一次冷启动约 1.67 秒,资源 267784 字节;未将识别引擎或扫描图放到首屏。
- 使用本地真实模型对 C3_180、C3_-135、C3_135 三张样例进行推理,当前收紧门限均通过(yaw 约 4.28°/-44.77°/42.59°)。这不是对真实手机拍摄合格率的保证。
- `node h5/test/capture-quality-browser.mjs`:真实识别引擎和原始模型,使用保持长宽比、居中后的三视角样例模拟视频流,实际自动拍摄三张并复验 JPEG;正脸不能冒充侧脸、反方向侧脸不能直接推进,诊断对应正确视角,相机成功后释放。不是手动触发快门,也不是模拟识别结果。
全部写入测试仅发生在独立临时数据目录,上游测肤以本地样例替代;不调用收费 Face++,不修改生产账号、照片、报告或配置。真实手机微信/相机仍需用户最终验收。
## 生产部署及只读验收
- 功能提交:`92a3965b7d637f19ec3d31389f142447339ff2a6`(`feat(h5): apply fourth feedback and tighten local capture validation`)。
- 服务器:`ubuntu@1me.xajuzi.com:/home/ubuntu/juzi`;入口:<https://1me.xajuzi.com/state/>。部署前服务器工作区干净、HEAD 与 origin/main 均为 `e53c809`;通过 Git bundle 快进更新,不代替用户 push、不覆盖服务器私有配置或已有用户数据。
- 更新后重启 `juzi-h5.service`,让服务端评分展示模块、质量诊断模块及前端同版。服务 active/running,`NRestarts=0`,健康接口 `ok:true`;检查时最近 10 分钟错误级日志为 0。
- 公网首页、app/capture-quality/head-pose/report-ui 四个脚本、CSS、两份扫描素材全部 200;逐文件 SHA-256 与本地一致,均为 `Cache-Control: no-cache`。
- 公网未登录报告、后台页面及统计接口均 401;私有身份文件、server.js 均 404;微信配置仍启用,不修改微信凭据或流程。
- 公网真实模型的自动三连拍测试通过:三个实际合格视角自动抓拍、最终 JPEG 本地复验、实际识别到的正脸/错误方向不能冒充目标侧脸、正确诊断归属、报告后释放摄像头;登录与测肤响应仅浏览器内拦截,不产生生产账号或报告、不调用收费测肤。
- 公网第三轮专项串行复验通过:五种屏宽的弹窗断行/产品版式、三款产品、扫描动效与成功进入报告、六种等待视口、装饰图标、取消/迟到响应/失败恢复及字体/扫描图失败降级;无页面 JS 异常。已检查公网首页、等待画面及产品截图。
- 公网第二轮回归串行复验通过:首页口号/Logo 比例、实际 PSD 字体、首屏不加载完整字体和识别引擎、全屏相机、三种圆角轮廓/五官辅助线、五种宽度无溢出;无页面 JS 异常。
- 公网报告交互串行复验通过:三个维度独立滑动/键盘切换、垂直滚动不误切换、图片放大、七张检测图、三档产品排版;皱纹正文显示指定结论、不再包含分区技术附文。无页面 JS 异常。
- 同时启动多套冷浏览器时曾出现字体/资源下载超时;串行重试自动拍摄流程通过。字体仍按原 PSD 使用原始 MiSans,并不将此网络压力测试描述为真实手机合格率保证。
@@ -0,0 +1,13 @@
# 展示肌龄限制±5岁
用户已确认:实际年龄25岁、接口肌龄40岁,展示30岁。
展示肌龄 = max(实际年龄−5, min(实际年龄+5, 原始肌龄))。
该限制仅影响展示,不修改接口原始肌龄,不用于产品推荐,不调整三个维度或综合得分。后台同时保留原始肌龄与展示肌龄,列表、详情和CSV与报告页使用同一规则。新报告存储展示值;历史报告读取时按原始肌龄和当时填写的实际年龄应用新展示规则,不改写历史文件或推荐。
缺少原始肌龄显示`--`,不拿实际年龄替代;缺少实际年龄时展示原始肌龄。保留原始小数精度,不另外取整。
同时移除肤色图片上的黄褐斑、雀斑检出状态浮层(HTML、CSS和渲染逻辑),不改变其评分参数与色素图30%透明度。
验证覆盖:±5边界、上下限、缺失值、小数、原始数据不变、历史展示值不作为输入、推荐继续使用原始肌龄;后端记录/列表/详情/CSV以及历史报告一致性;真实色斑案例图片无浮层。
+11
View File
@@ -0,0 +1,11 @@
# 微信退出登录
- 首页与「我的报告」的状态区,仅微信已登录时显示「退出登录」。游客模式不显示,不恢复旧的「切换登录方式」按钮。
- 点击使用系统确认框提示:确认退出微信登录?历史报告将保留,重新微信登录后可查看。取消不调用退出接口。
- 确认后调用现有 `api/auth/logout`,撤销当前服务端会话和 Cookie;成功才清除前端登录状态并回到入口页。失败提示错误,保留当前页面/身份并允许重试;请求期间禁用退出按钮,防止重复提交。
- 不删除账号、报告快照或微信偏好缓存。退出后重新登录同一微信,仍可查看原有历史报告。
- 退出时清空历史列表并作废在途历史请求,限制旧报告的返回导航;旧微信请求迟到也不能在游客身份下打开报告。
验证:`node h5/test/wechat-browser.mjs`(模拟微信、真实 HTTPS Cookie)覆盖取消、接口失败、确认退出、服务端会话撤销、返回键、迟到报告、游客不显示退出及重新微信登录后快照文件字节不变。`node h5/test/guest-privacy.mjs`、`node h5/test/report-persistence.mjs` 回归游客与历史报告。
用户已确认与原始肌龄显示、鸿蒙滚轮兼容一起发版。发布沿用 Git bundle 快进同步和服务重启,不代用户推送 origin,不删除历史报告。微信退出流程使用隔离模拟账号验证;线上不操作真实微信账号、报告或付费接口。
@@ -0,0 +1,26 @@
# 恢复321与色斑案例复测
本地修改,尚未部署。未调用付费检测接口,未修改线上记录。
## 拍摄提示音
每次网页摄像头抓拍先播放已有 countdown-321.mp3,等待真正的 ended 事件,再执行白屏补光和抓帧。沿用流程音队列,上一条步骤引导先播完;倒计时期间锁定快门、停止姿态提示,禁止重复触发。退出、页面隐藏使令牌失效,不再拍摄。拍下后的本地照片复验保持不变。
网页内自动和降级手动抓拍适用。系统相机原生快门不受网页控制;浏览器音频不可用时不阻断原有拍摄流程。
验证:node h5/test/countdown-capture-browser.mjs,三张逐张等待音频 ended、防重复快门、取消倒计时均通过。自动化控制音频结束事件,手机微信实际音量和播放仍需真机验证。
## 真实色斑案例
线上225条记录中207份原始结果可读取,所有结果 freckle.value 均为0,因此没有可用于验证的真实雀斑阳性案例。雀斑分类值没有提供雀斑数量,confidence 不等于雀斑个数,也不作为阳性依据。
选择棕色色斑最多的 R20261014-155810-LFB8 本地复测,brown_spot.count=81,freckle.value=0,仍展示雀斑未检出,不能把棕色色斑冒充雀斑。
- 色素分99;黄褐斑和雀斑均未检出,不扣该分类分。
- 黑眼圈分30;肤色项=99×80%+30×20%=85.2。
- 水分项79;肤色水润分类百分制=(85.2+79)/2=82.1,映射十分制9.46。
- 当前透亮感9.46,综合得分91。
- 色素图以30%透明度叠加,原始照片不降透明度。
- 无黄褐斑/雀斑区域坐标时只显示真实检出状态,不生成定位框。
H5_PIGMENTATION_CASE_DIR 指向私有临时案例目录运行 h5/test/pigmentation-case-browser.mjs,评分公式、真实标记、透明度和浏览器渲染验证通过。真实照片和原始返回不纳入 Git。
@@ -0,0 +1,9 @@
# 扫描等待页 30 秒兜底
从三张照片完成、打开扫描等待页开始计时,最多等待 30 秒。超时关闭扫描页、停止相机与播报、清空本轮照片、解锁操作,提示「检测服务开小差了,请重试」,返回档案页,由用户再次开始拍照。
独立页面看门狗覆盖网络未返回及扫描动画未完成;回调也检查实际截止时间,避免定时器延迟时误显示迟到报告。请求自身超时同步设为 30 秒,无 AbortController 的旧内核也会结束等待。成功、主动取消或页面隐藏时清理定时器并尽可能中止请求;超时先作废流程令牌,迟到结果不能切回报告页。
超时记入已有拍摄漏斗「明确失败」,不新增自动重试或检测调用。中止浏览器请求不保证已受理的服务端/上游调用停止,后台仍可能完成该次检测,且可能产生原有调用费用;服务端检测预算未更改。相比旧版 150 秒前端请求预算,超过 30 秒的慢检测现在也会提示重拍。
验证涵盖 29.999 秒成功、30 秒未返回、接口成功但动画不结束、迟到结果、取消、旧内核缺少 AbortController、成功后不再弹超时及定时器清理。
@@ -0,0 +1,64 @@
# 报告布局与跟手交互调整
> 综合评分布局已按后续要求调整为标题和分数整体在圈内居中;年龄标签与数字间距为 25px。最新实现与验证见《评分圈内居中与正脸语音修复-2026-10-12.md》。本文件其余跟手交互、JPG 优化与历史发布记录保留。
## 本次范围
1. 综合评分:标题移到黄色圆环外正上方,与「生物可视年龄」同高;分数数字本身水平、垂直居中,「分」独立放在数字右侧,不把分数挤偏。
2. 护理建议:标题左对齐,其他文案不变。
3. 检测等待:回退实拍背景,使用原 PSD 白色人头及黄色扫描动画,不出现用户照片。保留约 1 秒一轮、成功至少播放一轮、取消/失败立即退出,以及素材失败时的文字状态和取消入口。
4. 拍摄震动:复核现有功能。前端判断距离、居中、角度、光线等通过、进入「保持不动」时请求 `[70,40,70]` 毫秒短振;同一步抖动重试最多每两秒提醒一次,自动快门不重复震动。不支持、返回 false 或抛异常都静默跳过,不阻碍拍摄。是否实际震动仍取决于手机和浏览器,自动化不代替物理真机验证。
5. 报告三组页签:与「日常肤质」共用轻量滑动控制器。拖动时内容随手指平移,松手按距离/速度吸附;支持短拖回弹、轻扫、首尾阻力、系统取消和旋转屏幕。保持点按/键盘切换、上下滚动、图片点按放大;拖动不误触放大。各卡片独立,容器高度跟随当前内容,非当前页签从辅助阅读/焦点导航中排除。
6. JPG 优化:只转换在实际展示背景上可明显减小体积的三张产品图,原始 WebP、PSD 和原导出清单完整保留。背景和扫描底图仍用更小的 WebP;透明 logo、图标和蒙版保留 PNG。送检照片原本就是 JPEG,本次不二次压缩、不改变检测输入。
## 资源体积
| 产品 | 原始 WebP | 网页使用 JPG | JPG 质量 |
| --- | ---: | ---: | ---: |
| 25+ | 276,778 B | 63,959 B | 90 |
| 35+ | 267,094 B | 66,077 B | 90 |
| 50+ | 331,924 B | 153,787 B | 96 |
| 合计 | 875,796 B | 283,823 B | 减少 67.6% |
三张图保持 504×503,白色产品卡底色合成,渐进式 JPEG、4:4:4 色度采样,PSNR 均不低于 40dB。页面只加载实际推荐的一款。构建脚本 `tools/optimize_delivery_images.py` 可重复生成;原图/新图的 SHA-256、体积、尺寸与参数记录于 `design/ui-20261012/delivery-optimization-20261012.json`。
## 验证
本地 8080 服务运行后执行:
```bash
node h5/test/report-touch-layout.mjs
node h5/test/scan-placeholder.mjs
node h5/test/profile-interaction.mjs
node h5/test/customer-feedback.mjs
node h5/test/customer-feedback-second.js
node h5/test/customer-feedback-second.mjs
node h5/test/customer-feedback-third.mjs
node h5/test/customer-feedback-fourth.js
node h5/test/customer-feedback-fourth.mjs
node h5/test/capture-quality-browser.mjs
node h5/test/report-persistence.mjs
node h5/test/login-performance.mjs
node h5/test/recommendation.js
node h5/test/regression.js
node h5/test/customer-feedback.js
node h5/test/profile-store.js
node h5/test/analyze-timeout.js
```
以上均通过。覆盖五种报告宽度/六种扫描视口、真实移动端触摸拖动中位移、三组吸附与高度、纵向滚动、放大/键盘、原稿素材哈希、白色人头、扫描完整循环和失败降级、实际本地人脸模型自动三连拍、账号隔离/历史报告/前后台一致性,以及评分和推荐规则回归。
慢网性能模拟:150ms 延迟、约 2Mbps 下载、4 倍 CPU 降速,测试冷启动登录约 1.68 秒,首屏资源 271,106 B;字体、人脸识别、产品和扫描图仍不竞争登录首屏。这是本地模拟测量,不等同于所有真机的公网耗时。
本次未修改拍摄合格阈值、评分算法、推荐规则、报告文案、登录或保存逻辑。未追踪的 `new_feedback/` 保持原样,不纳入本次提交。
发布目标为 `ubuntu@1me.xajuzi.com:/home/ubuntu/juzi`,服务 `juzi-h5.service`。沿用 Git bundle 快进同步,不代用户推送 origin,不更改服务器环境配置或历史数据;上线后核对静态文件哈希、服务健康和匿名访问保护,并通过公网执行只读模拟账号浏览器测试。
## 线上验收(2026-10-12)
- 功能提交 `63c9e06`,从服务器基线 `69dc996` 快进发布,已重启服务。`active/running`,退出码 0、自动重启次数 0,本机及公网健康检查均通过。
- 公网首页、app/swipe-track/report-ui/profile-store/capture-quality 脚本、最新 CSS、三张 JPG、扫描底图与蒙版,共 12 份静态资源均为 200,SHA-256 与本地一致,保持 `Cache-Control: no-cache`;JPG 正确返回 `image/jpeg`。
- 公网匿名会话未登录且 `no-store`;报告列表、后台页面/统计返回 401,账号私有数据及服务端源码/测试入口返回 404。
- 公网 `report-touch-layout.mjs`、`scan-placeholder.mjs`、`profile-interaction.mjs` 全部通过;覆盖此次报告布局、三组真实触摸跟手/吸附/取消/边界/上下滚动/图片放大,以及白色人头扫描完整周期、失败/取消/迟到响应和基础信息记忆/隔离。没有页面 JS 异常,不创建线上账号/报告,不调用付费检测。
- 新报告专项脚本首次使用默认 `load` 导航与 30 秒预算,在公网冷加载字体/测试素材时超时。测试改为与现有公网用例一致的 60 秒预算及 `domcontentloaded` 导航,仍等待真实报告渲染与字体加载后执行原有断言,复验通过;不改运行时代码和界面行为。
- 已检查线上报告、滑动中状态和白色人头扫描截图。震动是否实际发生仍需支持该能力的手机真机确认。
@@ -0,0 +1,22 @@
# 报告底部 Figma 还原
设计文件 53PGFOThvhZwf2TPUHAcUz,稳韧力组1:47、底部组1:65。仅本地修改,尚未提交或部署。
- 大标题前移到日常护理上方、左对齐;日常护理及推荐产品合为一个自适应白卡。
- 标题1:369为两行文字组;按用户最新确认,两行文字各自居中,整体按白卡居中并保留1设计像素偏移。文字组随屏幕缩放,字号与上方稳韧力统一为22.106597px、行距28.738542px,不再随宽屏放大文字。
- 护理建议改为日常护理,删除固定结束语,保留按检测结果生成的护理内容。
- 白卡内增加推荐产品小标题。仍仅显示算法选中的一款,不生成产品列表或切换入口。
- 三款副标题分别为焕活光泽 透亮紧致、恢复弹性 紧实利落、强韧屏障 轮廓向上,不再显示双仓精华液第二行。
- 免责声明移到白卡下方,补充仅根据您最近的测肤记录推荐。
- 稳韧力标题、页签及段落字体/行距按设计尺寸微调;动态检测图、文案、评分和跟手滑动不变。
- 历史报告依旧使用当时推荐档位与护理文本,仅按该档位更新产品展示文案和素材;不改写保存数据。
## 静态素材核对
ui-20261015/product-25.png 对应1:69(481×511),product-35.png 对应1:75(481×511),product-50.png 对应1:87(481×511),product-badge.png 对应1:71(101×77)。全部为设计工具导出原始PNG,保留透明边角和原始裁切,无运行时临时素材URL;网页每次只加载选中的产品图。
产品卡按945×511比例,右侧图按481×511比例,徽标按101×77比例。布局以1080px设计稿折算375px基准并随屏幕缩放。用户最新确认产品标题恢复原来14.467593px,灰色副标题按同一比例(约1.041667)放大到12.013166px,行距同比放大;320px屏幕也保持同一字号,已核对三款文字均不遮挡图片。
## 验证
report-bottom-figma.mjs 覆盖三款选中产品、320/375/414/480宽度、元素顺序、白卡嵌套、图片/徽标比例、文字不遮挡图片、历史推荐对象不变、缺少肌龄与长护理内容。另运行报告持久化、游客隐私、产品推荐及报告滑动/布局回归。三款375px截图位于私有临时测试目录,不纳入Git。
@@ -0,0 +1,58 @@
# 实测描述与旧护理兼容:修复复测
> 本文记录的是此前扩大修改范围的版本,现已被用户收窄决定取代,不代表当前待发布文案。最终范围见《文案最终范围-仅皱纹优秀与痘痘检出-2026-10-15.md》:其他文案及护理建议原文均已恢复;分档与旧护理兼容保留。
日期:2026-10-15。此文为《线上10例报告文案对照》的修复后补充,不覆盖第一次测试暴露问题的记录。
## 本次修改
- 十分制显示分档仍为低于8改善、8至不足9良好、9及以上优秀,评分/综合分/推荐算法不变。
- 七项结论采用整体状态评价+实际检测数据,不再根据高分断言没有皱纹、泛红、色素沉积、眼袋或有很强的屏障能力;低分也不推断静态纹固化、中面部容积流失等未直接证实的状态。
- 皱纹所有档位保留皱纹及细纹数量;轮廓所有档位保留下颌线角度、实际眼袋与黑眼圈等级;水分保留分数、缺水面积和有效严重度;泛红保留实测面积。
- 痘痘正计数如实显示数量、种类及整体状态,零计数与缺失分开。检出脓包/结节仍提示具体种类,不凭数量推断炎症严重程度。
- 旧护理条目没有key时按七项的准确名称映射新版护理。保持旧项目、数量及顺序;不擅自把旧两项扩充成三项,不重新推荐产品。未知名称保留原文,不按数组位置乱配。
- 文案版本 `score-summary-factual-detail-v1`,与分档版本独立;即使已有新的8/9分档标签,旧文案也能刷新。
- 后台缺少原始数据无法刷新时明确显示“保存快照,未刷新文案”,不冒称已更新。原始报告、数据库及旧算法摘要不覆盖。
## 同一批10条真实案例复测
只读线上取数,使用保存的VM进行展示层回放,不调用付费检测。10条共70个页卡、25条护理建议,通过档位、实测数量保留、旧名称映射、源对象不可变、推荐不变及护理顺序不变检查。
|案例|复测|护理项目与新档位|
|---|---|---|
|R20261015-144549-7CNL|通过|泛红(优秀)→皱纹(优秀)→轮廓(优秀)|
|R20261015-134214-Y6L7|通过|轮廓(优秀)→皱纹(优秀)|
|R20261015-112350-ZLOF|通过|肤色(优秀)→水分(优秀)→泛红(良好)|
|R20261015-111214-JKNS|通过|皱纹(优秀)→肤色(良好)→水分(优秀)|
|R20261015-105142-4UTP|通过|皱纹(良好)→平滑度(优秀)|
|R20261015-105002-HVEV|通过|皱纹(优秀)→平滑度(优秀)|
|R20261015-104945-ZC5V|通过|皱纹(优秀)→泛红(优秀)|
|R20261015-104729-BLXO|通过|皱纹(优秀)→肤色(优秀)→水分(优秀)|
|R20261015-104209-V2J6|通过|皱纹(优秀)→肤色(优秀)→水分(优秀)|
|R20261015-103650-FVQA|通过|皱纹(优秀)→泛红(良好)|
## 新版代表文案
### R20261015-144549-7CNL
- 轮廓(9.4,优秀):面部轮廓整体状态较好,建议维持紧致护理并关注局部变化;左右下颌线角度分别为左 112.18°/ 右 112.67°;眼袋明显、黑眼圈中度。
- 皱纹(9.3,优秀):全脸共检测到皱纹 10 条、细纹 10 条;整体纹路状态较好,建议维持日常淡纹与保湿护理。
- 泛红(9.3,优秀):泛红面积 4%;整体泛红项状态较好,建议维持温和护理,持续关注泛红变化。
### R20261015-111214-JKNS
- 肤色(8.8,良好):检测到当前肌肤色素分 66,存在中度黑眼圈;整体肤色状态较稳定,建议持续关注局部暗沉与肤色均匀度。
- 水分(9.9,优秀):水分分 97;缺水面积 3%,缺水严重度 3,整体水分状态较好,建议维持补水与保湿习惯。
### R20261015-103650-FVQA
- 泛红(8.4,良好):泛红面积 12.5%;整体泛红项状态较稳定,建议保持温和护理并关注局部变化。
## 自动化回归
- `node h5/test/factual-copy.js`:实际参数、缺失与零值、旧名称护理、旧两项不扩充、未知项不乱配。
- `H5_COPY_CASE_FILE=/private/ten-copy-cases.json node h5/test/report-copy-cases.js`:真实10例只读展示回归;私有样例不进入Git。
- `H5_ACNE_MAX_TEST=1 node h5/test/report-persistence.mjs`:浏览器H5/后台/历史回看、筛选与导出,旧名称护理更新且快照文件字节不变。
本地修改完成,尚未提交或上线。此前“9条旧护理未刷新”的问题已修复,不代表本次把历史护理重新选成新规则三项。
@@ -0,0 +1,32 @@
# 报告文案统一按十分制分档
> 后续确认:文案仅修改皱纹优秀档及痘痘有皮损时的描述,其他模板和护理原文恢复原版。本文中关于额外眼袋/明显缺水文案调整的描述已被收回,最终范围见《文案最终范围-仅皱纹优秀与痘痘检出-2026-10-15.md》。分档规则不变。
## 原因与规则
案例 R20261015-144549-7CNL 的紧塑力 P=76、S=9.28(显示9.3),轮廓 P=79、S=9.37。旧文案按百分制80/90分档,而十分制使用7+3P/100,导致9.x仍显示改善。
用户确认:分数计算保持不变,报告文案和护理建议改为按页面实际显示的一位小数分档:低于8为改善,8到不足9为良好,9及以上为优秀。使用与页面相同的两位存储精度及一位显示舍入,例如S=8.95显示9.0,文案为优秀。新规则标识 `display-score-8-9-v1`。
## 覆盖范围
- 七项:轮廓、皱纹、肤色、平滑度、水分、泛红、痘痘;文案与对应护理建议复用同一档位。
- 肤色、水分仍使用各自独立分数;保留水分严重度交叉校验,不改评分公式。
- 痘痘不再通过“有脓包/结节”或“有皮损”独立降档,实际计数和中重度皮损提示保留,不能以优秀文案声称未检出。
- 轮廓眼袋、肤色黑眼圈、平滑度毛孔黑头按实际检测描述;明显缺水也不因文案等级较高就声称水润。
- 后台列表、七项评分、三维度评分、筛选、统计、CSV采用一致分档。
- 原始算法中的旧六分类等级、摘要及生活方式保留为审计数据,后台明确标注为旧版,不作为当前报告文案分档依据。
## 历史数据
旧报告打开时仅刷新展示文案、护理建议对应档位,不改原始数据、评分、产品推荐、护理项目选择和顺序,不覆盖历史快照。后台详情在具备原始返回时同样刷新;缺少原始数据时保留原有文案,不臆造检测数据。
## 真实案例复核
R20261015-144549-7CNL:紧塑力仍显示9.3,轮廓9.37按显示9.4归为优秀,皱纹9.28按显示9.3归为优秀。轮廓仍如实显示“眼袋明显、黑眼圈中度”,痘痘仍显示实际皮损,不改变计分、综合分和推荐规则。
## 回归
`node h5/test/display-grade.js` 覆盖七项、8/9临界舍入、空值、9.3案例、真实眼袋、旧报告不可变及历史推荐/顺序保留。
`H5_ACNE_MAX_TEST=1 node h5/test/report-persistence.mjs` 覆盖新记录落库、前后台展示、筛选、统计、CSV、历史报告刷新且不覆写原始快照。
@@ -0,0 +1,23 @@
# 报告等待页实拍背景
> 此文档记录上一版实现,已按后续用户要求回退:等待页不再展示用户照片,仅保留 PSD 白色人头和扫描动画。当前验证脚本为 `h5/test/scan-placeholder.mjs`。
原扫描页直接展示 PSD 的白色人脸占位。上一版改为在原 PSD 人脸蒙版内叠加本次正脸照片,扫描线置于照片上方;原素材不重绘,框、检测中文字、装饰图标与取消入口不变。
- 首张合格正脸就预解码,三张拍完使用同一张正脸,不使用最后一张侧脸或其他报告照片。
- 与拍摄预览一样居中、等比 `cover` 铺满,不拉伸照片。前置实时拍摄保留当时的预览镜像;后置/相册/系统相机文件不额外镜像。镜像只作用于 CSS,不改变送检 JPEG。
- 已有“一次完整扫描才进成功报告”保持不变;照片延迟解码时重启扫描,错误/取消不等待动画。
- 成功、失败、取消、页面隐藏或重新拍摄时清理等待页图片引用,不残留上一轮人脸。照片不新增缓存/上传/持久化。
- 素材或蒙版加载失败时降级为实拍背景、检测中文字和取消入口;照片解码失败使用深色背景,不露出白色占位。
- 不修改拍摄约束、评分、肌龄、推荐规则或报告保存逻辑。
当前回退版验证命令(本地服务需运行于 8080):
```bash
node h5/test/scan-placeholder.mjs
node h5/test/capture-quality-browser.mjs
node h5/test/customer-feedback-third.mjs
node h5/test/customer-feedback-fourth.mjs
```
测试只使用模拟账号、拦截分析响应,不创建线上记录、不调用付费检测。当前脚本验证白色人头、不展示用户照片、六种视口、扫描完整循环、成功/失败/取消清理、迟到响应及素材失败降级。
@@ -0,0 +1,16 @@
# 报告页滑动防冲突
仅优化报告详情卡的手势,不修改报告数据、算法或基础信息页的滑动参数。
- 图片区域可横向跟手切换;说明文字不触发横滑,页签仍可点击或键盘切换。
- 当前试用横向优先:移动超过 12px 且横向位移大于纵向的 2/3 即进入横滑;只有纵向位移达到横向的 1.5 倍时才锁定竖滑。一次手势锁定后不重新判方向。之前上线的竖向优先版本要求横向大于纵向的 1.5 倍才横滑。
- 修正浏览器抢占:仅改方向比例时,`touch-action: pan-y` 仍可能让浏览器提前滚动并发送 `pointercancel`。现在仅报告详情卡启用非被动 `touchmove`,与 Pointer Events 共用同一方向锁;图片区域单指横滑时取消原生滚动,竖滑和文字区域不拦截。不通过 `pointermove.preventDefault()` 或指针捕获来假定原生滚动已被阻止。
- 横滑过程中冻结卡片当前高度,包括图片/字体延迟加载导致的尺寸更新;松手或取消后再恢复当前页签高度,并沿用平滑高度过渡。
- 首尾保留阻尼反馈,但回弹位移最大为 16 CSS px,不允许拉出大片空白。
- 保留浏览器原生纵向滚动、图片点击放大、取消复位与无障碍页签行为。不尝试拦截微信系统返回手势。
验证:`node h5/test/report-touch-layout.mjs` 检查真实移动触摸横滑、斜滑、文字区不切页、图片与文字区竖向滚动、拖动期间高度不变、边缘位移上限和原有布局/交互;`node h5/test/profile-interaction.mjs` 回归共用滑动组件的基础信息页。
新增回归覆盖横移 60px/纵移 75px、横移 80px/纵移 90px 的六段真实触摸路径(之前均会触发 `pointercancel` 并竖滚)。现在每组三张报告卡都检查松手前跟手位移、无浏览器取消、正确切页,以及拖动中/松手后竖向位置均不变。本地报告、基础信息交互和算法 86 条断言均通过;不调用付费检测或写入报告。
之前的竖向优先版本已发布;当前横向优先版本已通过本地报告交互测试,按用户要求发版。发布沿用 Git bundle 快进同步至 `/home/ubuntu/juzi`,重启 `juzi-h5.service`,再经公网运行同一报告交互测试并检查服务健康及静态资源一致性。不代用户推送 origin,不包含未追踪的 `new_feedback/`。最终手机微信体验仍需真机复验。
@@ -0,0 +1,11 @@
# 报告顶部评分与年龄排版
- 分数数字与「分」组成同行整体,下面显示「综合评分」,间距 0 CSS px。两行共用水平中心线,整个分组在圆圈内水平、垂直居中。
- 「生物可视年龄」标题与年龄数字间距 0 CSS px,作为整体与左侧圆圈竖向居中对齐。
- 保留原文字、字号、数据和算法,不更改其他页面或服务器配置。
## 验证与发布
`node h5/test/report-touch-layout.mjs` 验证五种屏宽、0/75/82/100/缺失分数:单位计入分数组宽度、标题在数字下方、零间距、分组居中及年龄对齐;同时回归报告跟手切换等既有交互。测试使用模拟数据,不写报告、不调用付费检测。
本地验证通过。发布沿用 Git bundle 快进同步至 `/home/ubuntu/juzi` 并重启 `juzi-h5.service`,发布后经公网重跑上述测试并核对静态资源与健康检查。不代用户推送 origin,不包含未追踪的 `new_feedback/`。
+9
View File
@@ -0,0 +1,9 @@
# 拍摄光线仅提示
亮度低于 110 时显示「建议到光线明亮处拍摄,保持不动即可拍摄」;高于 220 时显示「光线过强,建议避开强光,保持不动即可拍摄」。110–220 不提示光线问题。
亮度不再是实时捕捉或拍后 JPEG 校验的拦截条件,无法读出亮度也不单独拦截。其他人脸置信度、角度、位置、距离和稳定性要求不变;过暗导致无法识别人脸时仍需要调整,不保证检测接口接受所有极暗或过曝照片。
保留亮度观测值,供后台校准使用;不改变报告计分、结论或付费接口调用次数。策略版本 `2026-10-16-light-advisory-v10`。
验证包含三视图暗/亮边界测试、实时暗光提示、保持暗光完成倒计时和拍后校验、实际本地模型自动三连拍;不调用付费检测。
@@ -0,0 +1,41 @@
# 拍摄约束放宽与标题更新
依据用户真实手机反馈:上一轮拍摄过于费劲,适度放宽前端门限,页面名称改为 `1'me肌肤状态检测`。此次不修改设计素材、评分、肌龄、推荐规则、微信登录流程或历史报告。
## 参数对照
参数版本由 `2026-10-12-fourth-v1` 升为 `2026-10-12-fourth-v2`;新报告后台保存新版本与配置快照,已有记录不回写。
| 项目 | 上一轮 v1 | 本次 v2 |
| --- | --- | --- |
| 人脸置信度 | ≥0.5 | ≥0.4 |
| 人脸框高占原帧 | 34%~64% | 30%~68% |
| 水平偏离屏幕中心 | ≤12% 屏宽 | ≤16% 屏宽 |
| 检测框边缘余量 | ≥2% 原帧 | ≥1% 原帧 |
| 正脸 yaw/pitch | ≤10°/12° | ≤15°/18° |
| 侧脸幅度/pitch | 35°~65°/≤15° | 30°~70°/≤20° |
| 歪头 roll | ≤10° | ≤15° |
| 脸部平均亮度 | 75~225 | 65~235 |
| 稳定时长/检测帧数 | ≥650ms/≥3 帧 | ≥450ms/≥2 帧 |
| yaw/pitch 漂移 | ≤5° | ≤8° |
| 中心 x/y 漂移 | ≤原帧 3.5% | ≤原帧 5.5% |
| 框高占比变化 | ≤4.5 个百分点 | ≤6.5 个百分点 |
仍要求单人出镜、完整入镜、正确视角、基本距离/光线;最终 JPEG 必须通过同一套本地复验。没有删除质量检查、绕过检测、增加服务端质量拦截或网络往返。漏检/识别异常及超过 2 秒的检测间隔仍打断累计。已有模型不可用降级路径保持不变,后台明确标记未校验。
最低稳定等待减少 200ms;慢设备从至少三次合格观察减为两次,减少持续保持姿势的难度。实际速度仍取决于手机模型推理耗时,不能保证所有机型和环境的检测合格率。
## 标题
`h5/index.html` 的 HTML title、Open Graph 分享标题及页面描述统一改为 `1'me肌肤状态检测`。首页 PSD 原组合图已经是「肌肤状态检测」,无需改图。
## 验证与发布
本地已通过:
- 参数边界:上一版偏头/俯仰/偏位/亮度/轻微漂移等拒绝样例现在通过,多人/错误视角/裁切/大幅移动仍拒绝;450ms 两帧准确触发,短于 450ms 不触发,漏检/超时间隔重置。
- 真实模型自动三连拍及最终 JPEG 本地复验,正脸不能冒充侧脸、错误方向不能推进,诊断照片归属正确,成功后释放摄像头。
- 隔离服务器后台诊断保存 v2 配置快照;页面与分享标题正确;quality=3 不阻断报告,游客状态、历史快照、五种屏宽及实际一轮扫描动画通过。
- 报告持久化、后台/H5/CSV 一致、账号隔离、皱纹原文;账号/OAuth 单测;原算法 86 条及推荐规则 126 用例;请求超时与震动安全回归。
部署目标:`ubuntu@1me.xajuzi.com:/home/ubuntu/juzi`,部署基线 `3f165bf`,通过 Git bundle 快进同步、不代用户 push。重启 `juzi-h5.service` 以保存新版后端参数快照。生产验证仅 GET 请求及浏览器内存拦截,不创建线上账号/报告,不调用收费 Face++。
@@ -0,0 +1,24 @@
# 拍摄角度不合格震动提醒
按用户确认的规则实现,纳入 2026-10-13 相机与首页更新发版。
- 实时检测确认单人、清晰度、距离与居中等前置条件通过,但角度不合格连续约 1 秒后,调用一次 30ms 轻震。
- 持续角度不合格时,相邻提醒至少间隔 4 秒;不使用独立定时器,只有收到新检测结果才可能提醒。
- 合格、无人脸、其他质量问题或检测失败立即取消震动并重置连续计时;漏帧也中断计时。检测结果间隔超过 2 秒会重新累计,避免推理卡顿被误认为持续不合格。
- 自动拍摄、手动快门和拍摄完成均不再震动。
- 拍摄锁、摄像头切换/未就绪、离开拍照页及切后台禁止提醒。停止检测、步骤重置或开始拍摄时取消尚未结束的震动。
- 前后提示保持 4 秒限频,不因角度短暂合格、镜头切换或步骤重置绕过冷却。
- 不支持接口、接口拒绝或抛出错误时静默忽略,不影响原有文字、语音和自动拍摄。
没有改变拍摄角度、距离、亮度、稳定时间或评分推荐算法。不是所有手机都能产生物理震动,微信/鸿蒙需真机复验,iOS 当前按无震动降级。
验证命令:
```sh
node h5/test/customer-feedback-second.js
node h5/test/customer-feedback-fourth.js
node h5/test/camera-flip-browser.mjs
node h5/test/capture-quality-browser.mjs
```
测试涵盖定时边界、持续限频、合格取消、无人脸、非角度问题、拍摄/切换/后台隔离、不支持/拒绝安全降级,以及相机翻转和真实本地模型三连拍回归。浏览器测试使用模拟相机和拦截的身份/报告接口,不调用付费检测、不写真实账号或报告。
@@ -0,0 +1,9 @@
# 拍照至报告页统计
后台「统计概览」新增累计及今日拍摄流程漏斗:开始拍照 → 正面拍完 → 右脸拍完 → 三张拍完 → 提交检测 → 收到成功结果 → 实际进入报告页。
每次点击「开始拍照」生成随机流程 ID,按流程及阶段去重;不是人数。只有报告渲染、切屏且报告页处于 active 状态后才计入成功进入,浏览历史报告与 mock 演示不计入。今日按流程开始时间的北京时间归属。显示全程转化率、成功进入平均耗时、收到成功结果但尚未进入、明确失败、主动取消、后台中断、尚未完成。中断可与其他状态重叠,未完成包括仍在进行中的流程,不能直接视为失败。
数据从上线开始积累,不回填历史。匿名埋点存于受静态访问保护的 `h5/data/capture-funnel.jsonl`,仅包含随机流程 ID、固定阶段、客户端耗时、服务端接收时间。不保存账号、照片、档案或任意错误文本。已登录游客也可统计,不保存游客身份。接口校验同源及会话;客户端上报仅作为运营观测,不能作为计费或检测成功的权威依据。
每次发送累计阶段快照,支持重复及乱序到达;使用 keepalive,失败不阻断拍摄。断网、关闭页面仍可能漏报,尚未提供离线重传;成功率为已收到的观测值。没有增加检测调用,没有改变报告结论和多脸规则。
@@ -0,0 +1,25 @@
# 最终确认:只改两处文案
用户要求:仅修改“皱纹·优秀档”“痘痘·有皮损时”,其余文案不改。此前七项整体改写已撤回,护理建议原文也全部恢复。
|项目|原文|最终修改|
|---|---|---|
|皱纹优秀档|面中、口周、眼周等各区域均无明显皱纹沟壑,无静态深纹与粗纹,各类纹路面积均处于极低水平,面部整体平整细腻。|全脸共检测到皱纹 {wr_cnt} 条、细纹 {fl_cnt} 条;整体纹路状态较好,建议维持日常淡纹与保湿护理。|
|痘痘有皮损时|随档位使用旧评价文案,优秀模板为未检出,旧逻辑通过降档避免套用。|检测到 {acne_cnt} 处皮损({acne_detail}){heavy_note},局部皮损仍需关注。|
`heavy_note` 沿用原有脓包/结节提示。不修改零计数、计数缺失、部分计数缺失时的原有保护。
## 保留的逻辑修复
- 七个小项文案与护理建议按十分制显示分数分档:低于8改善、8至低于9良好、9及以上优秀。不改变评分公式。
- 痘痘不再单独降档,实际皮损数量与种类保留。
- 旧护理条目按已知名称兼容,更新对应档位的原版护理全文;项目、数量、顺序和产品推荐不重选,原始快照不覆盖。
- 肤色、水分继续独立分档;水分交叉校验、原有毛孔/黑头实测数量保护、黑眼圈实测程度替换保持原实现,不增加其他文案分支。
## 验证
- `node h5/test/factual-copy.js`:以发布版本60f0965的文案快照逐字比较,全部护理模板及其他20个结论模板保持不变,验证两处文案、缺失值、历史护理名称与不可变数据。
- `H5_COPY_CASE_FILE=/private/ten-copy-cases.json node h5/test/report-copy-cases.js`:同批10例、70个页卡、25条护理建议,分档、两处文案、旧护理匹配、历史推荐及顺序检查通过。私有样例不入Git。
- 浏览器及持久化测试继续覆盖前后台展示、历史回看、筛选、CSV与快照不改写。
本文件记录最终确认范围。实际发布版本及部署状态以 Git 提交和服务器发布检查为准。
@@ -0,0 +1,14 @@
# 水分文案独立分档
问题案例 R20261015-111214-JKNS:接口水分97、缺水严重度3、缺水面积3%,色素66、黑眼圈30,合成肤色水润77.9。旧版水分文案跟随合成分档,错误套用改善档的含水量偏低、缺水比较严重。
## 本次修正
- 水分展示文案及其护理/生活方式建议独立使用水分项:低于80改善,80至低于90良好,90及以上优秀。
- 沿用原有交叉校验:水分分≥80且缺水严重度≥60时,按100−严重度评估,并明确提示分数与缺水指标不一致,不默默套用缺水文案。
- 改善档不再固定描述严重缺水。严重度为有效0~100时,≥60描述缺水程度较高,其他值仅报告严重度;缺失省略。缺水面积缺失也不当成0。
- 缺失或无效水分分不产生水分档位及水分护理建议。
- 三维度、综合得分、产品推荐、护理类别的选取规则均不变。水分展示项P现在为自己的水分项,不再伪装为合成分。
- 新生成报告与本地原始数据回放使用新文案。历史已保存的presentation文案保留,不批量改写文件;如需纠正历史报告展示,应另行确定范围。
参考文案与生成文件同步修改。water-copy.js覆盖真实矛盾值、80/90边界、色素不影响水分文案、严重度条件、交叉校验、缺失和输入不变;复测报告持久化、账号隔离、推荐与肤色文案。
@@ -0,0 +1,34 @@
# 游客不留存与弹窗护理间距调整
## 发版范围
本次已完成本地开发、截图与用户确认,按要求提交和部署。发布沿用 Git bundle 快进同步至 `/home/ubuntu/juzi` 并重启 `juzi-h5.service`;不代用户推送 origin,不删除既有服务器历史数据或更改微信配置。发布后校验静态资源哈希、服务健康、免登录入口、协议同意前后临时会话边界和游客空历史;不调用线上付费检测或写入真实报告。
- 欢迎弹窗保留完整句子「我已阅读并同意相关隐私协议」,仅「隐私协议」四字加粗并下划线;「相关」恢复为普通文字,点击协议不勾选同意。
- 报告护理建议标题的下方间距由 18u 减为 9u(375px 屏宽对应 18px→9px),仍左对齐,不影响其他标题。
- 仅微信登录保留个人偏好和历史报告。微信现有账号、最近 5 份完整报告、跨手机查询、后台记录与原始结果留存不变。
- 游客入口改为「免登录体验」:不要求登录页勾选协议,不发起登录请求、不弹登录成功,直接进入首页。开始检测时仍须在欢迎弹窗明确同意隐私协议,之后才建立后台检测所需的临时会话。首页/报告列表的「切换登录」按钮删除,状态改为不可点击的「游客模式」/「微信已登录」。微信登录保留在原入口页,以及游客历史页的「微信登录,保存报告」入口。
- 游客使用内存中临时身份和非持久 Cookie,不落盘账号/会话,不再发送或保存可恢复的游客凭据。浏览器旧游客身份、基础信息和早期本地报告缓存清除;不清空其他应用或微信的缓存。
- 游客基础信息只在当前页面操作中使用,例如拍摄失败返回可继续重拍;不写 localStorage,刷新/新游客登录后重新填写。游客报告仅供本次查看;「我的报告」显示不保存提示,不请求历史列表、不展示旧游客报告。
- 服务端同样限制:游客成功/失败均不新增 records.jsonl、照片、原始结果和完整报告快照;检测和评分在内存中正常进行。检测响应设为 no-store,游客不出现「保存失败」误提示。该规则指本项目不留存,不代表上游检测供应商的数据政策发生变化。
- 已有服务器游客历史数据不自动删除(避免未经确认批量清理);旧持久游客凭据不能恢复身份或读取历史。后续若需清理存量数据,应另行确认范围与备份。
## 验证
使用隔离临时目录、固定图片/上游响应与模拟微信;不操作线上账号/数据,不调用付费检测。
```bash
H5_ARTIFACT_DIR=/tmp/juzi-guest-privacy-20261013 node h5/test/guest-privacy.mjs
node h5/test/accounts.js
node h5/test/profile-store.js
node h5/test/profile-interaction.mjs
node h5/test/report-persistence.mjs
node h5/test/wechat-browser.mjs
node h5/test/customer-feedback-fourth.mjs
node h5/test/login-performance.mjs
node h5/test/report-touch-layout.mjs
```
新增端到端测试比对游客成功/失败前后的整个临时数据目录字节,确保无身份、照片、档案、报告、检测原文或后台记录写入;同一服务随后切换微信,验证报告/照片/后台记录正常保存。既有测试更新为仅微信持久化,保留跨设备、历史快照不可变、账号隔离和基础信息/拍照/报告交互回归。
截图:guest-entry.png、guest-home.png、welcome-privacy.png、care-spacing.png、guest-history.png(均为本地界面,报告内容为固定样例)。最新版可运行 `H5_ARTIFACT_DIR=/tmp/juzi-guest-no-login-20261013 node h5/test/guest-privacy.mjs` 生成。
@@ -0,0 +1,38 @@
# 痘痘按三视图各类型最大数量计分
## 规则
正脸读取 `result.<type>`,侧脸读取 `left_side_result.left_acne_info.<type>` / `right_side_result.right_acne_info.<type>`。每类分别取有效数量的最大值,不累加,也不是选皮损总数最多的照片。
| 类型 | 字段 | 每处扣分 |
|---|---|---:|
| 闭口 | closed_comedones | 1 |
| 丘疹 | acne | 2 |
| 脓包 | acne_pustule | 4 |
| 结节 | acne_nodule | 6 |
| 痘印 | acne_mark | 1.5 |
计数优先 `.count`,缺失时兼容已有矩形数量回退;负数、非整数、非有限数不计为有效数量。明确0才算未检出。
`计数扣分=min(50, Σ各类最大数量×权重)`。
正脸 `score_info.acne_score` 的既有100/85/70/55分档映射不变,侧脸整体评分不合并。
`痘痘P=min(正脸评分映射值,100−计数扣分)`;某一项缺失时仅用其他有效项;计数全部缺失则仅用正脸评分,二者都缺失不生成痘痘分。
报告文案与计分共用合并数量,侧脸检出脓包/结节也影响相应提示,计数不完整不声称完全未检出。这里的数量是三视图各类型最大值汇总,不等同于跨视图去重后的真实全脸总数。
## 后台和历史
新记录保存 `acne_scoring`,版本 `three-view-type-max-v1`,包括五类正/左/右数量、最大值、来源视图、权重、每项扣分、扣分合计/封顶结果、完整性和正脸评分/映射值。
后台详情展示计分明细表。报告快照同步保存用于展示的变量。旧记录和快照不迁移、不改写,历史推荐保持原样。
## 验证
- `node h5/test/acne-three-view.js`:分类分别取最大而非相加、不同类型来自不同视图、并列值、缺失/零/非法数量、计数回退、正脸评分约束、50分封顶、侧脸中重皮损文案与源数据不变。
- `H5_ACNE_MAX_TEST=1 node h5/test/report-persistence.mjs`:隔离合成三视图验证计分、报告文案、后台明细、保存/回看、CSV、历史不可变。
- `node h5/test/regression.js`:旧case2期望显式更新为侧脸闭口1处参与扣分;原始fixture不改写。
- 私有案例目录回放:`H5_WRINKLE_CASE_DIR=<private directory> node h5/test/wrinkle-case-browser.mjs`。
`R20261014-171215-CYLF`:闭口为0/1/0→取1,丘疹为0/1/1→取1,其他三类为0。扣3分,痘痘P=97、S=9.91,稳韧力由原9.97变为9.91。与待上线的整体皱纹规则合用,紧塑力8.95、透亮感9.63,综合得分`round(8.95×4+9.63×3+9.91×3)=94`(仅皱纹修改时为95)。
当前本地实现,未提交或部署;此前待发布的北京时间、整体皱纹评分修改继续保留。所有回放使用隔离数据和固定接口返回,不调用付费检测、不写线上记录。
@@ -0,0 +1,16 @@
# 皱纹三视图标注
- 报告皱纹页卡展示正面大图和两张侧脸缩略图,并保留各自标注。正面放大沿用缩略图的裁切;侧脸放大在缩略图的填充基础上缩小50%,净向下移动缩放后图片高度的25%(在之前下移50%的基础上先向上调20%,再向上调5%)。大图外框和报告小图不变。
- 每张照片只使用对应照片的皱纹增强图或真实标注坐标,不将正面标注投射到侧脸。
- 新检测优先复用三视图接口已提供的侧脸标注;缺失时分别以左右侧脸为主图补取,每侧最多调用一次。通常新增两次计费调用。
- 补取串行执行,每次前间隔 4 秒,每侧最多等待 20 秒;整个检测按 120 秒预算限制补取,给现有 150 秒前端超时预留余量。因此生成报告会增加等待时间。
- 补取失败不影响主报告、评分或产品推荐。该侧展示原图并明确说明缺少标注;后台记录每侧获取状态和错误代码。
- 微信用户保存报告时同时保存侧脸标注,历史回看不重复调用检测接口。旧报告不自动补取、不产生额外费用。游客仍不保存报告或照片。
- 补取结果仅用于图片展示,不参与七个小维度、三个大维度、综合评分或产品推荐计算。
## 验证
- 单元测试:请求照片与标注一一对应、顺序调用、复用已有标注、缺图、接口失败、超时、预算不足、不污染评分结果。
- 浏览器回归:320/375/414/480 宽度,正面浮层保留、左右独立浮层与大图、真实坐标兜底、缺图/缺标注提示、清理旧照片。
- 报告持久化回归:三次调用、响应与缓存标注一致、用户报告隔离及后台查看。
- 已使用获授权的真实案例分别补取左右侧脸,确认均返回独立皱纹增强图和标注坐标。
@@ -0,0 +1,37 @@
# 皱纹改用接口整体评分
按用户要求修改:皱纹百分制P直接采用 `score_info.wrinkle_score`(兼容顶层同名字段),取值0~100。不再计算 `100-最差分区值`,也不再用分区严重度限制该整体分。分区指标、计数、标记仍保留供查看。
- 皱纹S仍为 `7+3×P÷100`;紧塑力仍取轮廓和皱纹中的较差项。
- 缺失或无效的整体评分不生成皱纹分,不静默回退分区旧规则。
- 新报告/记录标记 `api-overall-v1`,后台详情说明来源。
- 修正维度一位小数显示中的浮点舍入:8.95显示9.0,9.45显示9.5;内部S、评分公式和综合得分计算精度不变。
- 不改写既有线上历史记录或快照。此次历史案例使用原始接口返回,在本地隔离服务复算;没有重新调用旷视或新增线上检测。
- 本次是用户指定的业务评分口径,不代表已核实供应商对该字段评分方向的定义。
## 案例复算
`R20261014-171215-CYLF`:
| 项目 | 原保存值 | 本地新规则 |
|---|---:|---:|
| 接口整体皱纹分 | 65 | 65(输入未改变) |
| 皱纹P | 0 | 65 |
| 皱纹S | 7 | 8.95 |
| 轮廓S | 9.73 | 9.73 |
| 紧塑力S | 7 | 8.95(显示9.0) |
| 透亮感S | 9.63 | 9.63 |
| 稳韧力S | 9.97 | 9.97 |
| 加权综合得分 | 87 | 95 |
`8.95×4+9.63×3+9.97×3=94.6`,四舍五入为95。
## 验证
`test/wrinkle-overall.js` 独立验证新评分来源、0/100边界、无分区仍可评分、严重度不影响整体分、缺失不回退、原输入不修改、维度舍入。
`test/wrinkle-case-browser.mjs` 只读取显式提供的私有样例目录(环境变量 `H5_WRINKLE_CASE_DIR`),以隔离临时微信身份和数据库回放接口返回,核对后台保存与报告页显示、生成截图。真实照片和原始数据不提交Git。
原Excel回归与引擎内置合成样例固定整体皱纹输入为旧P,隔离其他分类/文案的回归;原始fixture文件保持不变,新规则由独立测试和真实案例回放覆盖。
当前本地修改,尚未提交、部署。与此前待发布的后台北京时间修改共同保留在工作区。
@@ -0,0 +1,29 @@
# 相机翻转性能优化
本次优化纳入 2026-10-13 相机与首页更新发版。
## 修改范围
- 相机首帧 readyState ≥ 2 且宽高有效后即可显示,检查周期 50ms;去掉原有三轮 300ms 分辨率预热等待。
- 翻转过程中保留上一帧及原来的镜像方向,显示“正在切换摄像头…”。静帧仅保存在内存 Canvas,恢复预览或离开页面后清空,不上传、不持久化。
- 启动/翻转时禁用翻转按钮,拍摄和步骤推进期间拒绝翻转。
- 新镜头失败、首帧等待超时或浏览器明确返回错误的 facingMode 时,尝试恢复原镜头一次;恢复也失败才降级到系统相机。
- 翻转及恢复请求设置 12 秒超时,初次权限询问不设置该超时。首帧单独设置 12 秒超时,不再超时强行认为成功。迟到的流及时停止。
- 检测循环增加代次令牌,旧镜头推理结果不可更新新镜头状态或启动第二条循环;旧推理未结束时不叠加新推理。
- 模型加载回调也校验镜头令牌,避免旧回调覆盖切换状态。
拍摄角度、亮度、尺寸、清晰度、稳住时间、照片编码、评分和推荐规则均未修改。
## 验证
- `node h5/test/camera-flip-browser.mjs`:模拟相机验证首帧显示时延、静帧、重复点击、拍摄锁、失败/忽略 facingMode/超时恢复、旧推理隔离、离开后迟到流释放。
- `node h5/test/capture-quality-browser.mjs`:真实本地模型、三角度自动拍摄、最终 JPEG 复验、语音顺序、摄像头释放通过。
- `node h5/test/login-performance.mjs`:登录轻量加载、相机模型按需加载/失败重试等通过。
- 无付费检测调用、真实账号和报告写入。
模拟相机对比:原实现首帧后额外等待约 900ms,两轮优化后约 57~91ms(受测试机器调度影响)。不是手机真机性能结论。
## 仍需真机验证
微信 Android、iOS、鸿蒙:连续翻转、后置转前置、翻转时返回/切后台、拒绝相机权限、拍摄中点击翻转。
摄像头硬件/微信浏览器重新启动本身的耗时无法消除;不提供 getSettings().facingMode 的浏览器只能依赖 facingMode ideal 选择,无法保证识别其是否忽略请求。
@@ -0,0 +1,439 @@
# 线上最近10条成功案例:报告文案对照
测试日期:2026-10-15。对比线上已保存的报告快照(服务器代码60f0965)与本地待发布版 `display-score-8-9-v1`。
方式:只读SSH取数,复用保存的报告VM和原始检测字段,调用实际 `ReportUI.forDisplay` 展示路径;不是重新付费检测,也不是10台实机测试。本次未上线、未改线上记录。
## 结论
- 10条、70个页卡;46个档位变化,47段页卡文案变化。
- 三维度原分、综合分、保存的数据对象、产品推荐、原有护理项目及顺序未被修改。
- 9条旧报告缺少护理条目的key,现有刷新逻辑仍会保留旧版护理文案,未能同步新分档。此项未通过,需补充旧结构兼容后再发版。
- 9条旧报告的肤色/水分页卡原P字段都存了同一个“肤色水润”合成分;新版展示改用独立肤色、水分值。这是历史结构差异,不是三维度评分或总分被改写。
- 皱纹优秀档仍沿用已确认文案,不显示检出的皱纹/细纹数量。若希望高分报告也展示实际数量,需要另外调整优秀档文案,不能把“有计数”直接等同于“有明显静态深纹”。
## 10条总览
表中分数依次为紧塑力/透亮感/稳韧力;小项为原档位→新档位,仅列发生变化的档位。
|案例ID|三维度分数|综合分|档位变化|护理兼容|
|---|---|---:|---|---|
|R20261015-144549-7CNL|9.3/9.6/9.3|94|轮廓 改善→优秀;皱纹 改善→优秀;肤色 良好→优秀;泛红 改善→优秀;痘痘 良好→优秀|通过:护理文案同步新档位|
|R20261015-134214-Y6L7|9.2/9.8/9.9|96|轮廓 改善→优秀;皱纹 改善→优秀;痘痘 良好→优秀|未通过:旧结构护理文案未刷新|
|R20261015-112350-ZLOF|9.8/9.6/8.7|94|肤色 良好→优秀;水分 良好→优秀;泛红 改善→良好;痘痘 良好→优秀|未通过:旧结构护理文案未刷新|
|R20261015-111214-JKNS|9.2/9.3/9.9|94|皱纹 改善→优秀;肤色 改善→良好;平滑度 良好→优秀;水分 改善→优秀;痘痘 良好→优秀|未通过:旧结构护理文案未刷新|
|R20261015-105142-4UTP|8.9/9.5/9.6|93|皱纹 改善→良好;肤色 良好→优秀;平滑度 良好→优秀;水分 良好→优秀;痘痘 良好→优秀|未通过:旧结构护理文案未刷新|
|R20261015-105002-HVEV|9.0/9.5/9.8|94|皱纹 改善→优秀;肤色 良好→优秀;平滑度 良好→优秀;水分 良好→优秀;痘痘 良好→优秀|未通过:旧结构护理文案未刷新|
|R20261015-104945-ZC5V|9.0/9.4/9.3|92|轮廓 良好→优秀;皱纹 改善→优秀;肤色 改善→优秀;水分 改善→优秀;泛红 改善→优秀;痘痘 良好→优秀|未通过:旧结构护理文案未刷新|
|R20261015-104729-BLXO|9.0/9.7/9.9|95|皱纹 改善→优秀;肤色 良好→优秀;水分 良好→优秀;痘痘 良好→优秀|未通过:旧结构护理文案未刷新|
|R20261015-104209-V2J6|9.1/9.7/9.8|95|皱纹 改善→优秀;肤色 良好→优秀;水分 良好→优秀;痘痘 良好→优秀|未通过:旧结构护理文案未刷新|
|R20261015-103650-FVQA|9.3/9.7/8.4|91|皱纹 改善→优秀;肤色 良好→优秀;水分 良好→优秀;泛红 改善→良好;痘痘 良好→优秀|未通过:旧结构护理文案未刷新|
## 逐条页卡文案全文对照
### R20261015-144549-7CNL
原报告版本:2026-10-09+feedback-2026-10-12+care-2026-10-15。产品:1'me 主场精华 I(保持不变)。
#### 轮廓:改善→优秀(新版十分制显示 9.4)
线上原文:面部轮廓紧致度有待提升,下颌线清晰度不足,左右下颌线角度分别为左 112.18°/ 右 112.67°;眼袋明显、黑眼圈中度;中面部容积略欠饱满,肌肤支撑力偏弱。
本地新版:面部轮廓紧致立体,下颌线清晰;眼袋明显、黑眼圈中度;中面部容积饱满,肌肤支撑力充足。
#### 皱纹:改善→优秀(新版十分制显示 9.3)
线上原文:全脸共检测到皱纹 10 条、细纹 10 条,面中、口周、眼周等区域已出现静态纹倾向,纹路已呈现固化趋势,并非临时性干纹。
本地新版:面中、口周、眼周等各区域均无明显皱纹沟壑,无静态深纹与粗纹,各类纹路面积均处于极低水平,面部整体平整细腻。
#### 肤色:良好→优秀(新版十分制显示 9.6)
线上原文:检测到当前肌肤色素分 100,局部有色素沉积;存在中度黑眼圈,整体状态稳定,局部透亮感稍弱。
本地新版:检测到当前肌肤色素分 100,无明显色素沉积,存在中度黑眼圈;肤色均匀透亮。
#### 泛红:改善→优秀(新版十分制显示 9.3)
线上原文:泛红面积达 4%,肌肤泛红明显,敏感区域可见泛红;对外界刺激耐受度不足,容易受外界因素影响产生状态波动。
本地新版:泛红面积 4%,无明显泛红、敏感迹象;对外界刺激耐受度佳,肌肤处于稳定的健康状态。
#### 痘痘:良好→优秀(新版十分制显示 9.9)
线上原文:检测到 4 处皮损(闭口粉刺3处、痘印1处),以闭口粉刺、丘疹为主,无明显红肿痘痘;皮脂分泌基本正常,毛孔偶有轻微堵塞。
本地新版:检测到 4 处皮损(闭口粉刺3处、痘印1处),建议关注局部皮损并结合检测图片查看。
护理项目顺序:泛红→皱纹→轮廓。
护理「泛红」:改善→优秀。
线上原文:关注屏障修复与舒缓,精简护肤流程,选择温和洁面方式,修复受损角质层。若状态持续无明显好转,可寻求专业护肤建议。减少外界刺激,如酒精、香精、高浓度酸类精华等成分,避免冷热骤变刺激。
本地新版:持续规律护理并按季节阶段性调整,巩固皮肤耐受稳态。保持健康作息与均衡饮食,维持皮肤屏障稳态。
护理「皱纹」:改善→优秀。
线上原文:重视纹路管理,日常可使用含有肽类、胶原类成分的淡纹精华,针对眼周、法令纹等动态纹高发区域配合点按手法,遏制静态纹转化,帮肌肤恢复平滑细腻的状态。注意睡姿,减少面部长期受压;减少夸张表情,避免揉眼、托腮等表情性挤压习惯,减少纹路加深。
本地新版:以预防初纹为核心,搭配抗初老型精华,提前建立抗氧化/紧致护理习惯,延缓第一道静态纹出现。保持良好作息与运动习惯,坚持防晒,减轻光老化与糖化负担,维持肌肤稳定状态。
护理「轮廓」:改善→优秀。
线上原文:重点进行轮廓紧致管理,关注面中、苹果肌与下颌线区域。日常增加紧致精华步骤,配合由下向上的轻柔提升按摩,帮助肌肤维持弹润和支撑感,让轮廓看起来更紧实。保证充足、规律的睡眠,让肌肤在夜间充分修护;坚持防晒,减少紫外线带来的老化影响;减少熬夜。
本地新版:保持现有紧致抗衰护理节奏,日常叠加使用提亮精华,坚持防晒,加强光损伤防护与状态管理,持续关注面中疲态与轮廓支撑状态,让紧致状态更稳定、更持久。坚持防晒与规律作息,均衡饮食,保证优质蛋白、新鲜蔬果和健康脂肪摄入,适度运动。
### R20261015-134214-Y6L7
原报告版本:2026-10-09+feedback-2026-10-12。产品:1'me 主场精华 I(保持不变)。
#### 轮廓:改善→优秀(新版十分制显示 9.4)
线上原文:面部轮廓紧致度有待提升,下颌线清晰度不足,左右下颌线角度分别为左 111.24°/ 右 105.61°;眼袋明显、黑眼圈中度;中面部容积略欠饱满,肌肤支撑力偏弱。
本地新版:面部轮廓紧致立体,下颌线清晰;眼袋明显、黑眼圈中度;中面部容积饱满,肌肤支撑力充足。
#### 皱纹:改善→优秀(新版十分制显示 9.2)
线上原文:全脸共检测到皱纹 8 条、细纹 4 条,面中、口周、眼周等区域已出现静态纹倾向,纹路已呈现固化趋势,并非临时性干纹。
本地新版:面中、口周、眼周等各区域均无明显皱纹沟壑,无静态深纹与粗纹,各类纹路面积均处于极低水平,面部整体平整细腻。
#### 痘痘:良好→优秀(新版十分制显示 9.9)
线上原文:检测到 3 处皮损(闭口粉刺2处、痘印1处),以闭口粉刺、丘疹为主,无明显红肿痘痘;皮脂分泌基本正常,毛孔偶有轻微堵塞。
本地新版:检测到 3 处皮损(闭口粉刺2处、痘印1处),建议关注局部皮损并结合检测图片查看。
历史肤色/水分字段变化:肤色 P=92→86;水分 P=92→98。原三维度、综合分不变。
护理项目顺序:轮廓→皱纹。
护理兼容:旧条目只有name,没有key;现有本地forDisplay仍返回原护理文本,需要以稳定名称映射旧条目,或另行确认是否按新规则重新选三项。
### R20261015-112350-ZLOF
原报告版本:2026-10-09+feedback-2026-10-12。产品:1'me 主场精华 I(保持不变)。
#### 肤色:良好→优秀(新版十分制显示 9.4)
线上原文:检测到当前肌肤色素分 89,局部有色素沉积;存在中度黑眼圈,整体状态稳定,局部透亮感稍弱。
本地新版:检测到当前肌肤色素分 89,无明显色素沉积,存在中度黑眼圈;肤色均匀透亮。
#### 水分:良好→优秀(新版十分制显示 9.9)
线上原文:水分分 95;缺水面积 4.3%、肌肤含水量处于健康水平,整体水润度良好。
本地新版:水分分 95;角质层含水量充足,肌肤屏障锁水能力强,表面饱满水润,无干燥紧绷表现。
#### 泛红:改善→良好(新版十分制显示 8.7)
线上原文:泛红面积达 8.2%,肌肤泛红明显,敏感区域可见泛红;对外界刺激耐受度不足,容易受外界因素影响产生状态波动。
本地新版:泛红面积 8.2%,肌肤存在局部轻微泛红;对外界环境有良好的耐受度,仅在接触强刺激物时偶有轻微反应。
#### 痘痘:良好→优秀(新版十分制显示 9.7)
线上原文:检测到 6 处皮损(闭口粉刺1处、丘疹2处、痘印3处),以闭口粉刺、丘疹为主,无明显红肿痘痘;皮脂分泌基本正常,毛孔偶有轻微堵塞。
本地新版:检测到 6 处皮损(闭口粉刺1处、丘疹2处、痘印3处),建议关注局部皮损并结合检测图片查看。
历史肤色/水分字段变化:肤色 P=88→81;水分 P=88→95。原三维度、综合分不变。
护理项目顺序:肤色→水分→泛红。
护理兼容:旧条目只有name,没有key;现有本地forDisplay仍返回原护理文本,需要以稳定名称映射旧条目,或另行确认是否按新规则重新选三项。
### R20261015-111214-JKNS
原报告版本:2026-10-09+feedback-2026-10-12。产品:1'me 主场精华 I(保持不变)。
#### 轮廓:优秀→优秀(新版十分制显示 9.7)
线上原文:面部轮廓紧致立体,下颌线清晰;眼周无明显眼袋问题、黑眼圈中度;中面部容积饱满,肌肤支撑力充足。
本地新版:面部轮廓紧致立体,下颌线清晰;眼周眼袋初显、黑眼圈中度;中面部容积饱满,肌肤支撑力充足。
#### 皱纹:改善→优秀(新版十分制显示 9.2)
线上原文:全脸共检测到皱纹 25 条、细纹 27 条,面中、口周、眼周等区域已出现静态纹倾向,纹路已呈现固化趋势,并非临时性干纹。
本地新版:面中、口周、眼周等各区域均无明显皱纹沟壑,无静态深纹与粗纹,各类纹路面积均处于极低水平,面部整体平整细腻。
#### 肤色:改善→良好(新版十分制显示 8.8)
线上原文:检测到当前肌肤色素分 66,存在色素沉着较显著,存在中度黑眼圈,整体透亮感不足。
本地新版:检测到当前肌肤色素分 66,局部有色素沉积;存在中度黑眼圈,整体状态稳定,局部透亮感稍弱。
#### 平滑度:良好→优秀(新版十分制显示 9.7)
线上原文:肌肤整体较为平滑,局部区域细腻度不足;检测到粗大毛孔 330 颗、黑头 17 颗,有轻微角质粗糙感。
本地新版:肌肤质感整体较好;本次检测到粗大毛孔 330 颗、黑头 17 颗,局部仍有可改善之处。
#### 水分:改善→优秀(新版十分制显示 9.9)
线上原文:水分分 97;缺水面积 3%、肌肤缺水比较严重 3,肌肤含水量偏低,水润度不足,表面偏干燥,容易出现干纹。
本地新版:水分分 97;角质层含水量充足,肌肤屏障锁水能力强,表面饱满水润,无干燥紧绷表现。
#### 痘痘:良好→优秀(新版十分制显示 9.9)
线上原文:检测到 4 处皮损(闭口粉刺4处),以闭口粉刺、丘疹为主,无明显红肿痘痘;皮脂分泌基本正常,毛孔偶有轻微堵塞。
本地新版:检测到 4 处皮损(闭口粉刺4处),建议关注局部皮损并结合检测图片查看。
历史肤色/水分字段变化:肤色 P=77.9→58.8;水分 P=77.9→97。原三维度、综合分不变。
护理项目顺序:皱纹→肤色→水分。
护理兼容:旧条目只有name,没有key;现有本地forDisplay仍返回原护理文本,需要以稳定名称映射旧条目,或另行确认是否按新规则重新选三项。
### R20261015-105142-4UTP
原报告版本:2026-10-09+feedback-2026-10-12。产品:1'me 主场精华 I(保持不变)。
#### 皱纹:改善→良好(新版十分制显示 8.9)
线上原文:全脸共检测到皱纹 17 条、细纹 16 条,面中、口周、眼周等区域已出现静态纹倾向,纹路已呈现固化趋势,并非临时性干纹。
本地新版:全脸共检测到皱纹 17 条、细纹 16 条,面部整体平整,仅眼周或口周有细微的干纹,无明显静态深纹,纹路面积占比相对较低。
#### 肤色:良好→优秀(新版十分制显示 9.3)
线上原文:检测到当前肌肤色素分 84,局部有色素沉积;仅存在轻度黑眼圈,整体状态稳定,局部透亮感稍弱。
本地新版:检测到当前肌肤色素分 84,无明显色素沉积,仅存在轻度黑眼圈;肤色均匀透亮。
#### 平滑度:良好→优秀(新版十分制显示 9.5)
线上原文:肌肤整体较为平滑,局部区域细腻度不足;检测到粗大毛孔 332 颗、黑头 46 颗,有轻微角质粗糙感。
本地新版:肌肤质感整体较好;本次检测到粗大毛孔 332 颗、黑头 46 颗,局部仍有可改善之处。
#### 水分:良好→优秀(新版十分制显示 9.9)
线上原文:水分分 97;缺水面积 2.5%、肌肤含水量处于健康水平,整体水润度良好。
本地新版:水分分 97;角质层含水量充足,肌肤屏障锁水能力强,表面饱满水润,无干燥紧绷表现。
#### 痘痘:良好→优秀(新版十分制显示 9.6)
线上原文:检测到 4 处皮损(闭口粉刺2处、丘疹1处、痘印1处),以闭口粉刺、丘疹为主,无明显红肿痘痘;皮脂分泌基本正常,毛孔偶有轻微堵塞。
本地新版:检测到 4 处皮损(闭口粉刺2处、丘疹1处、痘印1处),建议关注局部皮损并结合检测图片查看。
历史肤色/水分字段变化:肤色 P=86.9→76.8;水分 P=86.9→97。原三维度、综合分不变。
护理项目顺序:皱纹→平滑度。
护理兼容:旧条目只有name,没有key;现有本地forDisplay仍返回原护理文本,需要以稳定名称映射旧条目,或另行确认是否按新规则重新选三项。
### R20261015-105002-HVEV
原报告版本:2026-10-09+feedback-2026-10-12。产品:1'me 主场精华 I(保持不变)。
#### 皱纹:改善→优秀(新版十分制显示 9.0)
线上原文:全脸共检测到皱纹 16 条、细纹 11 条,面中、口周、眼周等区域已出现静态纹倾向,纹路已呈现固化趋势,并非临时性干纹。
本地新版:面中、口周、眼周等各区域均无明显皱纹沟壑,无静态深纹与粗纹,各类纹路面积均处于极低水平,面部整体平整细腻。
#### 肤色:良好→优秀(新版十分制显示 9.3)
线上原文:检测到当前肌肤色素分 88,局部有色素沉积;仅存在轻度黑眼圈,整体状态稳定,局部透亮感稍弱。
本地新版:检测到当前肌肤色素分 88,无明显色素沉积,仅存在轻度黑眼圈;肤色均匀透亮。
#### 平滑度:良好→优秀(新版十分制显示 9.5)
线上原文:肌肤整体较为平滑,局部区域细腻度不足;检测到粗大毛孔 303 颗、黑头 44 颗,有轻微角质粗糙感。
本地新版:肌肤质感整体较好;本次检测到粗大毛孔 303 颗、黑头 44 颗,局部仍有可改善之处。
#### 水分:良好→优秀(新版十分制显示 9.9)
线上原文:水分分 98;缺水面积 1.4%、肌肤含水量处于健康水平,整体水润度良好。
本地新版:水分分 98;角质层含水量充足,肌肤屏障锁水能力强,表面饱满水润,无干燥紧绷表现。
#### 痘痘:良好→优秀(新版十分制显示 9.9)
线上原文:检测到 2 处皮损(闭口粉刺1处、痘印1处),以闭口粉刺、丘疹为主,无明显红肿痘痘;皮脂分泌基本正常,毛孔偶有轻微堵塞。
本地新版:检测到 2 处皮损(闭口粉刺1处、痘印1处),建议关注局部皮损并结合检测图片查看。
历史肤色/水分字段变化:肤色 P=87.9→77.8;水分 P=87.9→98。原三维度、综合分不变。
护理项目顺序:皱纹→平滑度。
护理兼容:旧条目只有name,没有key;现有本地forDisplay仍返回原护理文本,需要以稳定名称映射旧条目,或另行确认是否按新规则重新选三项。
### R20261015-104945-ZC5V
原报告版本:2026-10-09+feedback-2026-10-12。产品:1'me 主场精华 I(保持不变)。
#### 轮廓:良好→优秀(新版十分制显示 9.7)
线上原文:面部轮廓清晰,下颌线较为清晰,眼周眼袋初显、黑眼圈中度;中面部容积无明显流失;肌肤处于健康状态,支撑力表现良好。
本地新版:面部轮廓紧致立体,下颌线清晰;眼周无明显眼袋问题、黑眼圈中度;中面部容积饱满,肌肤支撑力充足。
#### 皱纹:改善→优秀(新版十分制显示 9.0)
线上原文:全脸共检测到皱纹 15 条、细纹 8 条,面中、口周、眼周等区域已出现静态纹倾向,纹路已呈现固化趋势,并非临时性干纹。
本地新版:面中、口周、眼周等各区域均无明显皱纹沟壑,无静态深纹与粗纹,各类纹路面积均处于极低水平,面部整体平整细腻。
#### 肤色:改善→优秀(新版十分制显示 9.4)
线上原文:检测到当前肌肤色素分 91,存在色素沉着较显著,存在中度黑眼圈,整体透亮感不足。
本地新版:检测到当前肌肤色素分 91,无明显色素沉积,存在中度黑眼圈;肤色均匀透亮。
#### 水分:改善→优秀(新版十分制显示 9.4)
线上原文:水分分 81;缺水面积 16.4%、肌肤缺水比较严重 19,肌肤含水量偏低,水润度不足,表面偏干燥,容易出现干纹。
本地新版:水分分 81;角质层含水量充足,肌肤屏障锁水能力强,表面饱满水润,无干燥紧绷表现。
#### 泛红:改善→优秀(新版十分制显示 9.3)
线上原文:泛红面积达 8.5%,肌肤泛红明显,敏感区域可见泛红;对外界刺激耐受度不足,容易受外界因素影响产生状态波动。
本地新版:泛红面积 8.5%,无明显泛红、敏感迹象;对外界刺激耐受度佳,肌肤处于稳定的健康状态。
#### 痘痘:良好→优秀(新版十分制显示 9.6)
线上原文:检测到 7 处皮损(闭口粉刺2处、丘疹1处、痘印4处),以闭口粉刺、丘疹为主,无明显红肿痘痘;皮脂分泌基本正常,毛孔偶有轻微堵塞。
本地新版:检测到 7 处皮损(闭口粉刺2处、丘疹1处、痘印4处),建议关注局部皮损并结合检测图片查看。
历史肤色/水分字段变化:肤色 P=79.9→78.8;水分 P=79.9→81。原三维度、综合分不变。
护理项目顺序:皱纹→泛红。
护理兼容:旧条目只有name,没有key;现有本地forDisplay仍返回原护理文本,需要以稳定名称映射旧条目,或另行确认是否按新规则重新选三项。
### R20261015-104729-BLXO
原报告版本:2026-10-09+feedback-2026-10-12。产品:1'me 主场精华 I(保持不变)。
#### 皱纹:改善→优秀(新版十分制显示 9.0)
线上原文:全脸共检测到皱纹 11 条、细纹 10 条,面中、口周、眼周等区域已出现静态纹倾向,纹路已呈现固化趋势,并非临时性干纹。
本地新版:面中、口周、眼周等各区域均无明显皱纹沟壑,无静态深纹与粗纹,各类纹路面积均处于极低水平,面部整体平整细腻。
#### 肤色:良好→优秀(新版十分制显示 9.4)
线上原文:检测到当前肌肤色素分 88,局部有色素沉积;仅存在轻度黑眼圈,整体状态稳定,局部透亮感稍弱。
本地新版:检测到当前肌肤色素分 88,无明显色素沉积,仅存在轻度黑眼圈;肤色均匀透亮。
#### 水分:良好→优秀(新版十分制显示 10.0)
线上原文:水分分 99;缺水面积 0.8%、肌肤含水量处于健康水平,整体水润度良好。
本地新版:水分分 99;角质层含水量充足,肌肤屏障锁水能力强,表面饱满水润,无干燥紧绷表现。
#### 痘痘:良好→优秀(新版十分制显示 9.9)
线上原文:检测到 3 处皮损(闭口粉刺2处、痘印1处),以闭口粉刺、丘疹为主,无明显红肿痘痘;皮脂分泌基本正常,毛孔偶有轻微堵塞。
本地新版:检测到 3 处皮损(闭口粉刺2处、痘印1处),建议关注局部皮损并结合检测图片查看。
历史肤色/水分字段变化:肤色 P=89.9→80.8;水分 P=89.9→99。原三维度、综合分不变。
护理项目顺序:皱纹→肤色→水分。
护理兼容:旧条目只有name,没有key;现有本地forDisplay仍返回原护理文本,需要以稳定名称映射旧条目,或另行确认是否按新规则重新选三项。
### R20261015-104209-V2J6
原报告版本:2026-10-09+feedback-2026-10-12。产品:1'me 主场精华 I(保持不变)。
#### 皱纹:改善→优秀(新版十分制显示 9.1)
线上原文:全脸共检测到皱纹 12 条、细纹 9 条,面中、口周、眼周等区域已出现静态纹倾向,纹路已呈现固化趋势,并非临时性干纹。
本地新版:面中、口周、眼周等各区域均无明显皱纹沟壑,无静态深纹与粗纹,各类纹路面积均处于极低水平,面部整体平整细腻。
#### 肤色:良好→优秀(新版十分制显示 9.4)
线上原文:检测到当前肌肤色素分 89,局部有色素沉积;仅存在轻度黑眼圈,整体状态稳定,局部透亮感稍弱。
本地新版:检测到当前肌肤色素分 89,无明显色素沉积,仅存在轻度黑眼圈;肤色均匀透亮。
#### 水分:良好→优秀(新版十分制显示 9.9)
线上原文:水分分 97;缺水面积 2.5%、肌肤含水量处于健康水平,整体水润度良好。
本地新版:水分分 97;角质层含水量充足,肌肤屏障锁水能力强,表面饱满水润,无干燥紧绷表现。
#### 痘痘:良好→优秀(新版十分制显示 9.9)
线上原文:检测到 2 处皮损(闭口粉刺1处、痘印1处),以闭口粉刺、丘疹为主,无明显红肿痘痘;皮脂分泌基本正常,毛孔偶有轻微堵塞。
本地新版:检测到 2 处皮损(闭口粉刺1处、痘印1处),建议关注局部皮损并结合检测图片查看。
历史肤色/水分字段变化:肤色 P=89.2→81.4;水分 P=89.2→97。原三维度、综合分不变。
护理项目顺序:皱纹→肤色→水分。
护理兼容:旧条目只有name,没有key;现有本地forDisplay仍返回原护理文本,需要以稳定名称映射旧条目,或另行确认是否按新规则重新选三项。
### R20261015-103650-FVQA
原报告版本:2026-10-09+feedback-2026-10-12。产品:1'me 主场精华 I(保持不变)。
#### 皱纹:改善→优秀(新版十分制显示 9.3)
线上原文:全脸共检测到皱纹 6 条、细纹 5 条,面中、口周、眼周等区域已出现静态纹倾向,纹路已呈现固化趋势,并非临时性干纹。
本地新版:面中、口周、眼周等各区域均无明显皱纹沟壑,无静态深纹与粗纹,各类纹路面积均处于极低水平,面部整体平整细腻。
#### 肤色:良好→优秀(新版十分制显示 9.5)
线上原文:检测到当前肌肤色素分 89,局部有色素沉积;存在中度黑眼圈,整体状态稳定,局部透亮感稍弱。
本地新版:检测到当前肌肤色素分 89,无明显色素沉积,存在中度黑眼圈;肤色均匀透亮。
#### 水分:良好→优秀(新版十分制显示 9.9)
线上原文:水分分 95;缺水面积 4.7%、肌肤含水量处于健康水平,整体水润度良好。
本地新版:水分分 95;角质层含水量充足,肌肤屏障锁水能力强,表面饱满水润,无干燥紧绷表现。
#### 泛红:改善→良好(新版十分制显示 8.4)
线上原文:泛红面积达 12.5%,肌肤泛红明显,敏感区域可见泛红;对外界刺激耐受度不足,容易受外界因素影响产生状态波动。
本地新版:泛红面积 12.5%,肌肤存在局部轻微泛红;对外界环境有良好的耐受度,仅在接触强刺激物时偶有轻微反应。
#### 痘痘:良好→优秀(新版十分制显示 9.7)
线上原文:检测到 6 处皮损(闭口粉刺2处、丘疹2处、痘印2处),以闭口粉刺、丘疹为主,无明显红肿痘痘;皮脂分泌基本正常,毛孔偶有轻微堵塞。
本地新版:检测到 6 处皮损(闭口粉刺2处、丘疹2处、痘印2处),建议关注局部皮损并结合检测图片查看。
历史肤色/水分字段变化:肤色 P=88.9→82.8;水分 P=88.9→95。原三维度、综合分不变。
护理项目顺序:皱纹→泛红。
护理兼容:旧条目只有name,没有key;现有本地forDisplay仍返回原护理文本,需要以稳定名称映射旧条目,或另行确认是否按新规则重新选三项。
## 代表性差异
- 7CNL:轮廓9.4由改善变优秀,同时保留眼袋明显、黑眼圈中度;皱纹9.3改优秀,原10条皱纹/10条细纹的计数不再出现在优秀文案。
- JKNS:水分97、缺水严重度3,线上原文仍写“肌肤缺水比较严重3”;新版改为水润充足。肤色独立分58.8→十分制8.8,改良好,而不是随水分变优秀。
- FVQA:泛红8.4由改善改良好,泛红面积12.5%仍保留。
## 发布前建议
先补齐旧护理条目的兼容,再重复这10例回归。保留历史推荐、分数和原始快照;若要让全部旧报告也采用新版“七选三”护理顺序,应单独确认,不能在仅改分档时默默重选。
@@ -0,0 +1,38 @@
# 综合得分:三维度按 4:3:3 加权
## 已确认公式
综合得分 = Math.round(紧塑力S × 4 + 透亮感S × 3 + 稳韧力S × 3)
- 三个维度为10分制,综合得分为100分制整数。
- 使用现有维度S分数(保留两位小数)计算,不先取页面显示的一位小数;最终四舍五入一次。
- 三维度任何一个缺失、非数值或越界,返回 null,页面显示 `--`,CSV为空;不按0分计、不重新分配权重。
- 不再读取旷视 `score_info.total_score` 或顶层 `total_score` 作为综合得分。原始接口数据仍原样保留供复核。
例如:紧塑力8、透亮感9、稳韧力8.5 → 84.5 → 85分。
## 生效位置
统一函数 `SkinScore.weightedTotal(dims)`,供评分引擎、H5报告页和后台复用。
- 新检测的 `vm.total` 与后台记录 `total_score` 保存加权结果,规则标记为 `dimensions-433-v1`。
- 后台列表列名“综合得分”,详情明确显示公式与精度规则,CSV列名“综合得分(4:3:3)”。
- 历史报告H5展示、后台列表/详情/CSV按已保存维度计算,不使用旧接口总分;不批量改写历史记录、报告快照或原始返回文件。
- 不修改维度评分、原始肌龄、文案或推荐产品规则;历史推荐保持不变。
- 历史快照和历史列表接口里既有的摘要总分不做迁移;用户页面不会使用该旧摘要分数,报告详情由统一函数计算显示。
## 本地验证
```sh
node h5/test/weighted-total.js
node h5/js/skin-score.js
node h5/test/regression.js
node h5/test/recommendation.js
node h5/test/raw-skin-age.js
node h5/test/accounts.js
H5_ARTIFACT_DIR=/tmp/juzi-weighted-total-20261014 node h5/test/report-persistence.mjs
```
使用临时数据目录、固定微信身份和接口样例进行测试,不调用付费检测,不读写真实线上报告。验证新记录保存、前后端展示一致、CSV、历史记录文件不变、缺失维度、安全隔离与既有产品/文案回归。
已获用户确认,纳入 2026-10-14 发版;部署后只读核对线上静态资源、报告页与后台接口,不调用真实付费检测或新增测试记录。
@@ -0,0 +1,11 @@
# 肤色文案与实际黑眼圈等级
仅调整报告展示文案,不改肤色计分、20%黑眼圈权重、综合得分或产品推荐。
肤色三档仍沿用肤色水润分类百分制原值:低于80=改善,80至低于90=良好,大于等于90=优秀;色素分占位符取现有 melanin。三档新模板同步到 reference/AI测肤报告页话术修改.md,再用 tools/build_report_copy.py 生成 report-copy.js,避免重生成时丢失本次调整。
黑眼圈描述独立读取 dark_circle_severity.value:0无明显黑眼圈,1仅存在轻度黑眼圈,2存在中度黑眼圈,3可见明显黑眼圈。仅接受0~3的整数;无效/缺失省略描述,不读 dark_circle.value,不用 dark_circle_score 推断。即使肤色优秀,也可能显示明显黑眼圈;即使肤色改善,也可能显示无明显黑眼圈。
消费者文案删除追加的黑眼圈分数及20%/80%权重说明;后台评分明细保留。新生成的报告与后台详情共用新展示模板,已保存的历史 presentation 保留,不迁移历史记录。
验证:color-copy.js 覆盖60种分档/程度组合、80/90边界、与黑眼圈分数相反的程度、缺失/无效程度、缺失色素分与不修改输入。另复测评分、色素透明度、真实色斑案例及报告持久化。
@@ -0,0 +1,30 @@
# 肤色项加入黑眼圈20%,色素图30%叠加
## 用户确认的评分口径
色素扣斑分=melanin_score-melasma×10-freckle×5。
肤色项=色素扣斑分×80%+dark_circle_score×20%。
肤色水润P=肤色项×50%+水分项×50%;水分项原有严重度交叉校验不变。
透亮感仍取平滑度和肤色水润中的较差项。没有拆分现有肤色/水分底层类别或新增维度。
黑眼圈缺失/无效时不编造分数,按其余有效权重归一,后台记录计算来源。报告肤色文案显示黑眼圈评分与20%权重,后台详情说明色素扣斑分、黑眼圈分、合成肤色项和水分项;新记录保存 `tone_scoring`(`color-dark-circle-20-v1`)。历史快照和评分不重写。
## 图像
- 肤色页原始照片保持不透明,brown_area 色素图在照片上使用 multiply 混合、globalAlpha=0.3。
- 没有色素图时,可用接口返回的 melanin_mark 真实坐标叠加;有色素图时不重复叠加同一份色素坐标。
- 黄褐斑和雀斑当前返回value/confidence分类结果,没有各自位置坐标。图上显示检出/未检出/未获取;只有实际收到该类型坐标时才绘制标记,不能借褐色斑坐标冒充黄褐斑/雀斑。
- 图层或坐标均缺失时降级为原始照片并注明,不填补不存在的检测区域。
- 30%只应用于肤色页检测叠加层,不改皱纹、平滑度、水分、泛红、痘痘图层或整体人脸照片的不透明度。
## 验证
- `node h5/test/tone-dark-circle.js`:80/20、外层50/50、先扣斑再加权、0分、缺失/非法值、来源记录、文案、输入不变。
- `node h5/test/tone-overlay-browser.mjs`:用固定颜色的合成测试图核对实际像素的30%混合,整张照片opacity仍为1;有/无分类值不伪造坐标;无图降级。
- `H5_ACNE_MAX_TEST=1 node h5/test/report-persistence.mjs`:新来源与前后端评分、文案保存回看、历史不改写。
- 私有CYLF案例本地回放截图:色素94、黑眼圈68、水分93,肤色项88.8,肤色水润90.9;平滑度87.6仍更低,所以透亮感仍9.63,叠加待发布的皱纹/痘痘规则,综合得分94。
- 旧Excel/参考断言将黑眼圈输入固定到原色素扣斑分,以隔离其他既有计算/文案回归;新规则由独立测试验证,不改写原始fixture。
所有测试使用临时服务、固定接口返回或样例图片,不发起付费调用、不写线上记录。当前本地修改,未提交或部署。
@@ -0,0 +1,40 @@
# 评分圈内居中、基础信息页固定与正脸语音修复
> 2026-10-13:报告顶部排版已按后续确认调整,以下 25px 年龄间距及原评分排列仅为历史记录。当前以《报告顶部评分与年龄排版-2026-10-13.md》为准;基础信息与语音修复不变。
## 范围
1. 报告:综合评分标题和分数在圆环内上下排列,作为一个整体垂直居中,标题与数字分别水平居中。数字右侧保留「分」。年龄标题到年龄数字的边界距离固定为 25 CSS px,不随屏宽比例增大;原显示文案不更名。
2. 基础信息:页面禁止纵向滚动、滚动链及下拉回弹,肤质轮播也不再把纵向手势交给页面。正常高度保留原间距;短视口只压缩纵向留白及肤质插画的可用高度,控件、说明、提交按钮完整可见,不以截掉下半页实现禁滚。出生年月、性别、省市的独立滚轮弹层仍支持纵向滑动。触摸防回弹监听只挂在基础信息页,不拦截弹层或报告页。
3. 语音:根因在 `HeadPose.check` 的正脸分支——正脸角度偏向一侧时,用「请向左/右转头」提示回正,和第二步侧脸指令混淆。现在正脸角度不合格统一提示「请正对屏幕」,不播侧脸转头指令;`setHint` 还按当前步骤和正脸照片是否已保存做防护。下一步语音必须有上一张已校验保存的照片,既有单通道队列保证「拍摄完成」先播完、再播侧脸引导。侧脸阶段的方向提示不变。
不更改拍照角度/距离/光线/稳定时长阈值,不改检测算法、报告保存、推荐、微信配置或数据。现有白色人头扫描页和三组报告跟手交互不变。未追踪的 `new_feedback/` 不纳入提交。
## 验证
本地 8080 服务运行后:
```bash
H5_ARTIFACT_DIR=/tmp/juzi-profile-voice-fix-20261012 node h5/test/report-touch-layout.mjs
H5_PROFILE_FONTS=1 H5_ARTIFACT_DIR=/tmp/juzi-profile-voice-fix-20261012 node h5/test/profile-interaction.mjs
node h5/test/capture-quality-browser.mjs
node h5/test/customer-feedback-second.js
node h5/test/customer-feedback-fourth.js
node h5/test/customer-feedback-fourth.mjs
node h5/test/customer-feedback-third.mjs
node h5/test/scan-placeholder.mjs
node h5/test/login-performance.mjs
node h5/test/report-persistence.mjs
node h5/test/recommendation.js
node h5/test/regression.js
```
- 五种屏宽下检查 0/75/82/100/缺失分数,标题加分数整体居中、均在圆内,年龄间距精确为 25px,报告的纵向滚动与三组横滑保留。
- 基础信息在 320×568、375×667、375×726、390×664、414×896、480×726、375×440 下不滚动且控件完整;真实触摸纵拖及鼠标滚轮不移动页面,之后横滑仍正常。用真实纵向触摸确认出生年月滚轮可改变选择,验证取消不保存以及账号隔离/重载记忆。
- 用真实本地人脸模型和已有照片组成合成相机视频:先提供侧脸作为不合格正脸,停留第一步并实际播「请正对屏幕」,无「转头」/下一步/完成音;再提供合格正脸,最终 JPEG 校验通过后自动保存并播「拍摄完成」,实际音频播完再开始第二步引导,随后完成三视图检测流程。
- 语音测试记录真实 MP3 解码后的 WebAudio 起播时间及当时屏幕步骤,不把语音文件预加载误认为起播。浏览器、无音频支持、真机系统音量/权限等行为仍以设备实际能力为准。
- 浏览器测试拦截模拟账号和分析响应或使用隔离数据服务,不写线上报告、不调用付费接口。
## 发布
基线 `2a49ebd`,目标 `ubuntu@1me.xajuzi.com:/home/ubuntu/juzi`、`juzi-h5.service`。沿用 Git bundle 快进同步,不代用户推送 origin,不改服务器配置和历史报告。发布后经公网执行布局、基础信息和真实自动拍照/语音测试,核对资源哈希、服务健康及匿名访问保护。
@@ -0,0 +1,19 @@
# 透亮感改为三项独立最低分
用户确认方案2:透亮感百分制P=min(独立肤色项,水分项,平滑度项),缺项剔除,三项全缺失则不评分。十分制沿用7+3×P/100、显示范围7~10、内部保留两位小数。综合得分仍按紧塑力4、透亮感3、稳韧力3四舍五入取整数。
独立肤色项继续使用色素扣斑后80%+黑眼圈20%;水分保留已有缺水交叉校验。新透亮感参与原有优先关注及肤龄边界产品推荐判断,未另改推荐规则。
为兼容旧文案、旧六分类接口及历史记录,cats.tone和旧版统计字段暂时保留。旧平均分不再进入新版透亮感、综合得分、优先关注、产品推荐或七项护理选择。正式报告使用七项展示,后台新记录新增sub_scores七项独立评分,并记录vita_score_rule=independent-color-water-smooth-min-v1;历史记录仍标注历史六分类,不以新版重算历史评分或推荐。
本次上线包括已确认的独立肤色/水分文案、七选三且关注优先的护理建议,以及产品标题恢复14.467593px/灰色副标题同比放大。三图轮播仍是本地试验,不纳入正式页面,不上传真实案例数据或私有预览脚本。
## 真实案例回放(当前规则)
|案例|旧透亮感S|新透亮感S|旧总分|新总分|推荐变化|
|---|---:|---:|---:|---:|---|
|R20261014-171911-O6YJ|9.26|9.09|88|88|仍25+|
|R20261015-111214-JKNS|9.34|8.76|94|93|仍25+|
|R20261014-171215-CYLF|9.63|9.63|94|94|仍25+|
JKNS优先关注由紧塑力变为透亮感,护理建议顺序变为肤色→平滑度→皱纹,符合关注项前置要求。测试仅回放现有返回,未重新调用付费检测。
Binary file not shown.
@@ -1,4 +1,4 @@
# 1meAI测肤旷视UI问题反馈 # 1'meAI测肤旷视UI问题反馈
## UI交互界面调整 ## UI交互界面调整
@@ -0,0 +1,37 @@
# 傅艳NEW与项思炼的会话 2026年10月12日
> 查看原消息记录,可点击[回到会话](https://applink.feishu.cn/client/message/link/open?token=Amqujz9KgEyzasxJCtuBi%2BE%3D)
傅艳NEW 2026年10月12日 10:16
@傅艳NEW 傅老师有几个问题需要和您反馈下:
这里我们的诉求是做成滑动和点击都可以选中的方式,技术上能做到吗?
![exported\_img\_v3\_0216d\_92abffb2\-da3a\-42d4\-bd42\-6d1b6a864e3g\.png](图片和附件/exported_img_v3_0216d_92abffb2-da3a-42d4-bd42-6d1b6a864e3g.png)
这里目前这样可以点开大图需要保留,在此基础上我们希望消费者脸占比大一点,就是只展示头部,背景少一点;
![exported\_img\_v3\_0216d\_d5d64a81\-acad\-4db9\-970c\-2dd1398b532g\.png](图片和附件/exported_img_v3_0216d_d5d64a81-acad-4db9-970c-2dd1398b532g.png)
报告中皱纹的文案可以拆的更细一点吗?把抬头纹、法令纹\(左右\)、鱼尾纹(左右)、眼部细纹\(左右\)、嘴角纹\(左右\)、眉间纹、额头纹、脸颊纹\(左右\)的具体指标全部说明;
这里平滑度这里有一些框,但写的结果是无明显粗大毛孔与黑头;那这里框的是什么?
![exported\_img\_v3\_0216d\_ee1903ba\-96ef\-4778\-9891\-027588093c7g\.jpeg](图片和附件/exported_img_v3_0216d_ee1903ba-96ef-4778-9891-027588093c7g.jpeg)
所有类似的黄色按钮的色号与我们设计要求不一致;
![exported\_img\_v3\_0216d\_718c2f13\-c978\-4e07\-bf72\-21b24236bd6g\.png](图片和附件/exported_img_v3_0216d_718c2f13-c978-4e07-bf72-21b24236bd6g.png)
这里依然存在断句问题,我们希望无论什么手机,宽幅多少,这里都用技术方法强制显示这种断句方式;
![exported\_img\_v3\_0216d\_eec8b07b\-be07\-4d37\-9a9f\-4057c5afc56g\.png](图片和附件/exported_img_v3_0216d_eec8b07b-be07-4d37-9a9f-4057c5afc56g.png)
我们目前点开链接会出现严重卡顿,是因为测试版本还是成品就是这样?
这里的文案和我们设计稿内给出的不一致,需要修改
![exported\_img\_v3\_0216d\_40a9e343\-5d09\-4760\-aa80\-6d13cc9ac8eg\.png](图片和附件/exported_img_v3_0216d_40a9e343-5d09-4760-aa80-6d13cc9ac8eg.png)
我们设置的字体大小希望用技术方式在不同型号的手机上呈现相同大小的字体,不要随着手机型号或者大小有改变

Some files were not shown because too many files have changed in this diff Show More