Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e34eeb1df | ||
|
|
873b45d3a5 | ||
|
|
774376fa16 | ||
|
|
381da60ae4 | ||
|
|
8bc6251899 | ||
|
|
2eb0d8e807 | ||
|
|
43a5373233 | ||
|
|
fed52930cc | ||
|
|
f5c416dd9d | ||
|
|
65ba3cc7a5 | ||
|
|
ac0b7ca42b | ||
|
|
459e21dad4 | ||
|
|
4af825a5e4 | ||
|
|
9eec4369da | ||
|
|
d07f8cc1bb | ||
|
|
bb6da17f54 | ||
|
|
571d78a2ed | ||
|
|
9181069643 | ||
|
|
b19811f974 | ||
|
|
5e07985b19 | ||
|
|
6a628f837b | ||
|
|
9ff7a582af | ||
|
|
03a8907b4e | ||
|
|
30c90f8df5 | ||
|
|
8365ef459f | ||
|
|
98d6189f5c | ||
|
|
49db1a6f5b | ||
|
|
c6d65493b6 | ||
|
|
fbc556d1a6 | ||
|
|
8115db4ec2 | ||
|
|
480fed40cc | ||
|
|
5632ac43d7 | ||
|
|
482fb4280d | ||
|
|
757b96dfad | ||
|
|
2d80f6dfd0 | ||
|
|
60f0965c5a | ||
|
|
77493bdff5 | ||
|
|
60ae6c97ef | ||
|
|
d8c67762da | ||
|
|
61a7f7c726 | ||
|
|
fc711e3b1c | ||
|
|
6acc541cd8 | ||
|
|
84a92137aa | ||
|
|
7381306139 | ||
|
|
e71368db6d | ||
|
|
4e42e33994 | ||
|
|
1fdbb575b0 |
@@ -1,19 +1,46 @@
|
||||
# 密钥与敏感配置 —— 绝不入库
|
||||
# 密钥与敏感配置
|
||||
apikey.bash
|
||||
apikey.*
|
||||
aipkey.bash
|
||||
.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/
|
||||
*.log
|
||||
__pycache__/
|
||||
aipkey.bash
|
||||
h5/data/
|
||||
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-out/cache/
|
||||
graphify-out/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/
|
||||
|
||||
@@ -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)。
|
||||
@@ -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 实例。
|
||||
@@ -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 <token>。</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
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1 @@
|
||||
实际淘宝 IDE 模拟器运行截图(非真机截图)。login-centered.png 从完整 IDE 窗口截图中裁取模拟器屏幕,未修改页面内容。
|
||||
|
After Width: | Height: | Size: 95 KiB |
|
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 后补齐真实几何。
|
||||
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 131 KiB |
@@ -0,0 +1,4 @@
|
||||
2 张淘宝 IDE 模拟器实际运行截图,未使用 H5 投影、Mock 报告或重绘界面。仅裁去 IDE 外框,未修改界面内容。
|
||||
01:登录页。02:未勾选协议点击游客体验后,真实显示“请先阅读并同意用户协议和隐私政策”。
|
||||
格式 PNG,单张 239×423,均小于 4MB,满足用户提供的最大尺寸 2160×3840。
|
||||
后续主页与档案运行截图需要先手动完成模拟器中的淘宝登录验证。
|
||||
|
After Width: | Height: | Size: 201 KiB |
@@ -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
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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,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 节“不要做什么”。
|
||||
@@ -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`——那是唯一还悬着的技术问题,也决定拍照页最终形态。
|
||||
@@ -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)。主分支可快进到适配分支,不需覆盖已有提交或改写历史。
|
||||
@@ -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
|
||||
```
|
||||
@@ -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 保留为可选对照,其默认自拍和图像导出仍未验收。
|
||||
- 手机上的新游客接口和原系统相机报告故障最终复验(本地回归已覆盖,线上尚未更新)。
|
||||
- 固定商品真机跳转、真实网关大响应及后台跨源上线配置。
|
||||
- 照片保留/清理周期、数据库/对象存储迁移;本轮没有自动删除旧数据。
|
||||
@@ -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、原始数据不变以及异步皱纹浮层回归。不调用付费检测接口。
|
||||
@@ -1,5 +1,7 @@
|
||||
# 产品推荐规则:肤龄基准与边界调整
|
||||
|
||||
> 2026-10-16 更新:以下为原始肌龄依据的历史规则说明。当前已改为报告展示肌龄,分档和边界条件保留;以 [产品推荐改用展示肌龄-2026-10-16.md](产品推荐改用展示肌龄-2026-10-16.md) 为准。
|
||||
|
||||
状态: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,不改写既有报告或调用线上付费检测。摄像头切换不在本次修改范围内。
|
||||
@@ -0,0 +1,9 @@
|
||||
# 取消每张照片拍后的二次检测
|
||||
|
||||
删除 `checkStill` 以及实时抓拍、系统相机/相册选图路径对它的调用。保存/读取照片成功后直接进入该张拍摄完成与下一步流程,不再检测已保存照片的人脸、角度、亮度或等待检测模型。
|
||||
|
||||
保留实时预览阶段的人脸识别、角度、位置与稳定性检测;光线仍仅提醒。保留 321 动画、补光、空帧/图片读取失败提示和取消/后台中断的过期回调保护。三张完成后的测肤报告接口调用不变,该接口仍可能因无法检测照片等原因返回失败。
|
||||
|
||||
拍摄观测诊断现在记录最后一次通过的预览帧,明确标记 `check_stage: preview`;系统相机/文件及手动降级无预览数据时标记 `check_stage: none`,不伪造拍后检测通过。策略版本 `2026-10-16-preview-only-v11`。
|
||||
|
||||
验证:真实本地模型自动三连拍时禁止调用静态照片检测;文件路径直接推进及取消保护;321 倒计时与流程漏斗回归。不调用付费接口。
|
||||
@@ -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记录不变,无真实账号/报告或付费检测调用。
|
||||
|
||||
当前本地实现,尚未提交或部署。
|
||||
@@ -1,5 +1,7 @@
|
||||
# 基础信息记忆与肤质拖动
|
||||
|
||||
> 2026-10-13 调整:以下偏好缓存仅对微信登录生效。游客仅保留当前页面操作中的临时表单值,不写缓存、不回填历史;刷新后重新填写。详见《游客不留存与弹窗护理间距调整-2026-10-13.md》。
|
||||
|
||||
## 范围
|
||||
|
||||
- 出生年月、性别、地区每次点滚轮“确定”即保存,不必拍照或提交报告。
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# 展示肌龄限制±5岁
|
||||
|
||||
用户已确认:实际年龄25岁、接口肌龄40岁,展示30岁。
|
||||
|
||||
展示肌龄 = max(实际年龄−5, min(实际年龄+5, 原始肌龄))。
|
||||
|
||||
该限制仅影响展示,不修改接口原始肌龄,不用于产品推荐,不调整三个维度或综合得分。后台同时保留原始肌龄与展示肌龄,列表、详情和CSV与报告页使用同一规则。新报告存储展示值;历史报告读取时按原始肌龄和当时填写的实际年龄应用新展示规则,不改写历史文件或推荐。
|
||||
|
||||
缺少原始肌龄显示`--`,不拿实际年龄替代;缺少实际年龄时展示原始肌龄。保留原始小数精度,不另外取整。
|
||||
|
||||
同时移除肤色图片上的黄褐斑、雀斑检出状态浮层(HTML、CSS和渲染逻辑),不改变其评分参数与色素图30%透明度。
|
||||
|
||||
验证覆盖:±5边界、上下限、缺失值、小数、原始数据不变、历史展示值不作为输入、推荐继续使用原始肌龄;后端记录/列表/详情/CSV以及历史报告一致性;真实色斑案例图片无浮层。
|
||||
@@ -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,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,9 @@
|
||||
# 拍摄光线仅提示
|
||||
|
||||
亮度低于 110 时显示「建议到光线明亮处拍摄,保持不动即可拍摄」;高于 220 时显示「光线过强,建议避开强光,保持不动即可拍摄」。110–220 不提示光线问题。
|
||||
|
||||
亮度不再是实时捕捉或拍后 JPEG 校验的拦截条件,无法读出亮度也不单独拦截。其他人脸置信度、角度、位置、距离和稳定性要求不变;过暗导致无法识别人脸时仍需要调整,不保证检测接口接受所有极暗或过曝照片。
|
||||
|
||||
保留亮度观测值,供后台校准使用;不改变报告计分、结论或付费接口调用次数。策略版本 `2026-10-16-light-advisory-v10`。
|
||||
|
||||
验证包含三视图暗/亮边界测试、实时暗光提示、保持暗光完成倒计时和拍后校验、实际本地模型自动三连拍;不调用付费检测。
|
||||
@@ -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,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优先关注由紧塑力变为透亮感,护理建议顺序变为肤色→平滑度→皱纹,符合关注项前置要求。测试仅回放现有返回,未重新调用付费检测。
|
||||
@@ -1,4 +1,4 @@
|
||||
# 1meAI测肤旷视UI问题反馈
|
||||
# 1'meAI测肤旷视UI问题反馈
|
||||
|
||||
## UI交互界面调整
|
||||
|
||||
|
||||
@@ -4,28 +4,28 @@
|
||||
"2": "app.js",
|
||||
"3": "skin-score.js",
|
||||
"4": "customer-feedback-second.js",
|
||||
"5": "t",
|
||||
"6": "n",
|
||||
"5": "showScreen",
|
||||
"6": "countdown-capture-browser.mjs",
|
||||
"7": "test/profile-store.js",
|
||||
"8": "admin.js",
|
||||
"9": "__awaiter",
|
||||
"10": "wechat-browser.mjs",
|
||||
"10": "n",
|
||||
"11": "lib/accounts.js",
|
||||
"12": "server.js",
|
||||
"13": "files",
|
||||
"14": ".draw",
|
||||
"15": "extractFaces",
|
||||
"14": "normalize",
|
||||
"15": "factual-copy.js",
|
||||
"16": "巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范",
|
||||
"17": "report-persistence.mjs",
|
||||
"17": "extractFaces",
|
||||
"18": "1'me肌肤状态检测 H5",
|
||||
"19": "customer-feedback-fourth.js",
|
||||
"20": "build_fixtures.py",
|
||||
"21": "disposeUnusedWeightTensors",
|
||||
"22": "k",
|
||||
"21": "展示肌龄限制正负5岁-2026-10-14.md",
|
||||
"22": "acne-three-view.js",
|
||||
"23": "facepp-skinanalyze(旷视皮肤分析专业版调用技能)",
|
||||
"24": "巨子测肤 · 1'me 语音播报内容设计文档 V1.0",
|
||||
"25": "mobile-select.iife.min.js",
|
||||
"26": "NeuralNetwork",
|
||||
"26": "display-age-recommendation.js",
|
||||
"27": "gen_voice.py",
|
||||
"28": "C",
|
||||
"29": "face-api.js 本地化依赖(h5/vendor/faceapi/)",
|
||||
@@ -37,15 +37,16 @@
|
||||
"35": "customer-feedback.js",
|
||||
"36": "actualFont",
|
||||
"37": "UI 外发版 10.09 图片资源",
|
||||
"38": "FaceLandmark68Net",
|
||||
"38": "最终确认:只改两处文案",
|
||||
"39": "UI交互界面调整",
|
||||
"40": "第四轮反馈修改与验收",
|
||||
"41": "showScreen",
|
||||
"41": "test/capture-funnel.js",
|
||||
"42": "layer_at",
|
||||
"43": "TinyYolov2Base",
|
||||
"43": "t",
|
||||
"44": "start-local.sh",
|
||||
"45": "PredictAllAgeAndGenderWithFaceAlignmentTask",
|
||||
"46": "PredictAllFaceExpressionsWithFaceAlignmentTask",
|
||||
"45": "dimension-cap.js",
|
||||
"46": "zoom-fill.js",
|
||||
"47": ".extractWeights",
|
||||
"48": "已修改",
|
||||
"49": "mobile-select/README.md",
|
||||
"50": "call_skinanalyze_3view.sh",
|
||||
@@ -53,7 +54,7 @@
|
||||
"52": "region-data.js",
|
||||
"53": "护理建议(全量文本,按需求摘抄)",
|
||||
"54": "UI 对齐 10.09 设计稿:调研与实施计划",
|
||||
"55": "FaceLandmarks68",
|
||||
"55": "guest-privacy.mjs",
|
||||
"56": "report-ui.js",
|
||||
"57": "regression.js",
|
||||
"58": ".forwardInput",
|
||||
@@ -62,9 +63,9 @@
|
||||
"61": "报告布局与跟手交互调整",
|
||||
"62": "提交前 Review(2026-10-11)",
|
||||
"63": "e2e-browser.mjs",
|
||||
"64": "start",
|
||||
"64": "综合得分:三维度按 4:3:3 加权",
|
||||
"65": "微信登录接入与公网部署说明",
|
||||
"66": ".runAndExtendWithFaceDetections",
|
||||
"66": "capture-quality.js",
|
||||
"67": "ui-20261009.mjs",
|
||||
"68": "analyze-timeout.js",
|
||||
"69": "misans/README.md",
|
||||
@@ -72,47 +73,129 @@
|
||||
"71": "报告顶部评分与年龄排版",
|
||||
"72": "build_report_copy.py",
|
||||
"73": "camera-geometry.js",
|
||||
"74": "report-copy.js",
|
||||
"74": "water-copy.js",
|
||||
"75": "prepare_h5_assets.py",
|
||||
"76": "报告页滑动防冲突-2026-10-13.md",
|
||||
"77": "drawFaceExpressions",
|
||||
"78": ".toString",
|
||||
"77": "care-selection.js",
|
||||
"78": "drawFaceExpressions",
|
||||
"79": "登录反馈与加载优化(2026-10-12)",
|
||||
"80": "线上微信登录部署记录",
|
||||
"81": "wechat-production.mjs",
|
||||
"82": "拍摄约束放宽与标题更新",
|
||||
"83": "10.09 新版 H5 实施与验收",
|
||||
"84": "微信登录联调:2026-10-12",
|
||||
"85": "test/accounts.js",
|
||||
"85": "相机翻转性能优化",
|
||||
"86": "report-touch-layout.mjs",
|
||||
"87": "产品推荐规则:肤龄基准与边界调整",
|
||||
"88": "ui-20261012/manifest.json",
|
||||
"89": "傅艳NEW与项思炼的会话 2026年10月12日.md",
|
||||
"90": "customer-feedback.mjs",
|
||||
"91": "wi",
|
||||
"91": "scan-wait-timeout.js",
|
||||
"92": "客户反馈修改(2026-10-12)",
|
||||
"93": ".runAndExtendWithFaceDetection",
|
||||
"93": "vizCompose",
|
||||
"94": "第三轮反馈:PSD 对齐、验收及上线记录",
|
||||
"95": "login-performance.mjs",
|
||||
"96": "production-loop.mjs",
|
||||
"99": "DetectSingleFaceLandmarksTask",
|
||||
"100": "extractAllFacesAndComputeResults",
|
||||
"102": "extractFaceTensors",
|
||||
"95": "i",
|
||||
"96": "capture-funnel-browser.mjs",
|
||||
"97": "account-client.js",
|
||||
"98": "extractFaceTensors",
|
||||
"99": "extractAllFacesAndComputeResults",
|
||||
"100": ".runAndExtendWithFaceDetection",
|
||||
"101": "wechat-fixture.cjs",
|
||||
"102": ".toString",
|
||||
"103": "第四轮反馈/傅艳NEW与项思炼的会话 2026年10月12日.md",
|
||||
"104": "export_fourth_feedback_assets.py",
|
||||
"105": "capture-quality-browser.mjs",
|
||||
"106": "第三轮反馈20261012/傅艳NEW与项思炼的会话 2026年10月12日.md",
|
||||
"107": "基础信息记忆与肤质拖动",
|
||||
"108": "profile-interaction.mjs",
|
||||
"109": "submitPhotos",
|
||||
"110": "doCapture",
|
||||
"111": "i",
|
||||
"110": "resetDetectState",
|
||||
"111": "color-copy.js",
|
||||
"112": "微信退出登录-2026-10-13.md",
|
||||
"113": "swipe-track.js",
|
||||
"114": "optimize_delivery_images.py",
|
||||
"116": "convLayer",
|
||||
"117": "startCamera",
|
||||
"115": "痘痘按三视图各类型最大数量计分",
|
||||
"116": "capture-no-recheck.js",
|
||||
"117": "display-grade.js",
|
||||
"119": "肤色文案与实际黑眼圈等级-2026-10-14.md",
|
||||
"120": "disposeUnusedWeightTensors",
|
||||
"121": ".draw",
|
||||
"122": "startCamera",
|
||||
"123": "评分圈内居中、基础信息页固定与正脸语音修复",
|
||||
"124": "TinyYolov2",
|
||||
"124": "admin-beijing-browser.mjs",
|
||||
"125": "报告等待页实拍背景-2026-10-12.md",
|
||||
"127": "height"
|
||||
"126": "wrinkle-case-browser.mjs",
|
||||
"127": "splash-brand-browser.mjs",
|
||||
"128": "后台北京时间统一",
|
||||
"129": "报告底部 Figma 还原",
|
||||
"130": "wrinkle-overall.js",
|
||||
"131": "原始肌龄显示与鸿蒙滚轮兼容",
|
||||
"132": "harmony-picker.mjs",
|
||||
"133": "recommendation.js",
|
||||
"134": "camera-flip-browser.mjs",
|
||||
"135": "恢复321与色斑案例复测",
|
||||
"136": "七项护理选三与关注优先",
|
||||
"137": "皱纹改用接口整体评分",
|
||||
"138": "拍照报告转化统计-2026-10-16.md",
|
||||
"139": "private-case-preview.mjs",
|
||||
"140": "FaceLandmarks68",
|
||||
"141": "实测描述与旧护理兼容:修复复测",
|
||||
"142": "tone-dark-circle.js",
|
||||
"143": "test/accounts.js",
|
||||
"144": "肤色项加入黑眼圈20%,色素图30%叠加",
|
||||
"145": "PredictAllAgeAndGenderWithFaceAlignmentTask",
|
||||
"146": "NeuralNetwork",
|
||||
"147": "拍摄角度不合格震动提醒-2026-10-13.md",
|
||||
"148": "PredictAllFaceExpressionsWithFaceAlignmentTask",
|
||||
"149": "report-persistence.mjs",
|
||||
"150": "R20261015-104945-ZC5V",
|
||||
"151": "wechat-browser.mjs",
|
||||
"152": "customer-feedback-fourth.mjs",
|
||||
"153": "tone-overlay-browser.mjs",
|
||||
"155": "h5/README.md",
|
||||
"156": "报告文案统一按十分制分档",
|
||||
"157": "水分文案独立分档",
|
||||
"158": "R20261015-111214-JKNS",
|
||||
"159": "皱纹三视图标注",
|
||||
"160": "age",
|
||||
"161": "head-pose.js",
|
||||
"162": "contour-eye-copy.js",
|
||||
"163": "test/wrinkle-jobs.js",
|
||||
"164": "PredictSingleAgeAndGenderWithFaceAlignmentTask",
|
||||
"165": "PredictSingleFaceExpressionsWithFaceAlignmentTask",
|
||||
"166": "扫描等待30秒兜底-2026-10-16.md",
|
||||
"167": "线上最近10条成功案例:报告文案对照",
|
||||
"168": "R20261015-103650-FVQA",
|
||||
"169": "R20261015-105002-HVEV",
|
||||
"170": "R20261015-105142-4UTP",
|
||||
"171": "R20261015-144549-7CNL",
|
||||
"172": "拍摄光线仅提示-2026-10-16.md",
|
||||
"173": "R20261015-104209-V2J6",
|
||||
"174": "R20261015-104729-BLXO",
|
||||
"175": "R20261015-112350-ZLOF",
|
||||
"176": "逐条页卡文案全文对照",
|
||||
"177": "report-copy-cases.js",
|
||||
"178": "login-performance.mjs",
|
||||
"179": "contour-sides.mjs",
|
||||
"180": "production-loop.mjs",
|
||||
"181": "save",
|
||||
"182": "capture-diagnostics.js",
|
||||
"184": "wrinkle-three-views.mjs",
|
||||
"185": "FaceLandmark68Net",
|
||||
"187": "取消拍后校验-2026-10-16.md",
|
||||
"188": "wi",
|
||||
"189": "convLayer",
|
||||
"190": "FaceProcessor",
|
||||
"191": "extractConvParamsFactory",
|
||||
"192": "convLayer$1",
|
||||
"193": ".forwardInput",
|
||||
"194": "wrinkle-async-browser.mjs",
|
||||
"195": "AgeGenderNet",
|
||||
"196": ".runAndExtendWithFaceDetections",
|
||||
"197": "FaceExpressionNet",
|
||||
"198": "FaceFeatureExtractor",
|
||||
"199": "DetectSingleFaceLandmarksTask",
|
||||
"200": "TinyXception",
|
||||
"201": "TinyFaceFeatureExtractor",
|
||||
"202": "TinyYolov2",
|
||||
"205": "侧脸皱纹浮层异步加载-2026-10-16.md"
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# Graph Report - juzi (2026-10-13)
|
||||
# Graph Report - juzi (2026-10-16)
|
||||
|
||||
## Corpus Check
|
||||
- 107 files · ~4,905,775 words
|
||||
- 178 files · ~5,066,846 words
|
||||
- Verdict: corpus is large enough that graph structure adds value.
|
||||
|
||||
## Summary
|
||||
- 1457 nodes · 2904 edges · 116 communities (91 shown, 25 thin omitted)
|
||||
- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 210 edges (avg confidence: 0.57)
|
||||
- 1969 nodes · 3493 edges · 199 communities (154 shown, 45 thin omitted)
|
||||
- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 235 edges (avg confidence: 0.58)
|
||||
- Token cost: 0 input · 0 output
|
||||
|
||||
## Graph Freshness
|
||||
- Built from commit: `4c478b16`
|
||||
- Built from commit: `571d78a2`
|
||||
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
|
||||
- Run `graphify update .` after code changes (no API cost).
|
||||
|
||||
@@ -20,28 +20,27 @@
|
||||
- app.js
|
||||
- skin-score.js
|
||||
- customer-feedback-second.js
|
||||
- t
|
||||
- n
|
||||
- showScreen
|
||||
- test/profile-store.js
|
||||
- admin.js
|
||||
- __awaiter
|
||||
- wechat-browser.mjs
|
||||
- n
|
||||
- lib/accounts.js
|
||||
- server.js
|
||||
- files
|
||||
- .draw
|
||||
- extractFaces
|
||||
- normalize
|
||||
- factual-copy.js
|
||||
- 巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范
|
||||
- report-persistence.mjs
|
||||
- extractFaces
|
||||
- 1'me肌肤状态检测 H5
|
||||
- customer-feedback-fourth.js
|
||||
- build_fixtures.py
|
||||
- disposeUnusedWeightTensors
|
||||
- k
|
||||
- 展示肌龄限制正负5岁-2026-10-14.md
|
||||
- acne-three-view.js
|
||||
- facepp-skinanalyze(旷视皮肤分析专业版调用技能)
|
||||
- 巨子测肤 · 1'me 语音播报内容设计文档 V1.0
|
||||
- mobile-select.iife.min.js
|
||||
- NeuralNetwork
|
||||
- display-age-recommendation.js
|
||||
- gen_voice.py
|
||||
- C
|
||||
- face-api.js 本地化依赖(h5/vendor/faceapi/)
|
||||
@@ -51,21 +50,22 @@
|
||||
- build-fixture.py
|
||||
- customer-feedback.js
|
||||
- UI 外发版 10.09 图片资源
|
||||
- FaceLandmark68Net
|
||||
- 最终确认:只改两处文案
|
||||
- UI交互界面调整
|
||||
- 第四轮反馈修改与验收
|
||||
- showScreen
|
||||
- test/capture-funnel.js
|
||||
- layer_at
|
||||
- TinyYolov2Base
|
||||
- t
|
||||
- start-local.sh
|
||||
- PredictAllAgeAndGenderWithFaceAlignmentTask
|
||||
- PredictAllFaceExpressionsWithFaceAlignmentTask
|
||||
- dimension-cap.js
|
||||
- zoom-fill.js
|
||||
- .extractWeights
|
||||
- 已修改
|
||||
- mobile-select/README.md
|
||||
- call_skinanalyze_3view.sh
|
||||
- 护理建议(全量文本,按需求摘抄)
|
||||
- UI 对齐 10.09 设计稿:调研与实施计划
|
||||
- FaceLandmarks68
|
||||
- guest-privacy.mjs
|
||||
- report-ui.js
|
||||
- regression.js
|
||||
- .forwardInput
|
||||
@@ -73,58 +73,136 @@
|
||||
- 报告布局与跟手交互调整
|
||||
- 提交前 Review(2026-10-11)
|
||||
- e2e-browser.mjs
|
||||
- start
|
||||
- 综合得分:三维度按 4:3:3 加权
|
||||
- 微信登录接入与公网部署说明
|
||||
- .runAndExtendWithFaceDetections
|
||||
- capture-quality.js
|
||||
- ui-20261009.mjs
|
||||
- analyze-timeout.js
|
||||
- misans/README.md
|
||||
- scan-placeholder.mjs
|
||||
- 报告顶部评分与年龄排版
|
||||
- build_report_copy.py
|
||||
- water-copy.js
|
||||
- 报告页滑动防冲突-2026-10-13.md
|
||||
- care-selection.js
|
||||
- drawFaceExpressions
|
||||
- .toString
|
||||
- 登录反馈与加载优化(2026-10-12)
|
||||
- 线上微信登录部署记录
|
||||
- wechat-production.mjs
|
||||
- 拍摄约束放宽与标题更新
|
||||
- 10.09 新版 H5 实施与验收
|
||||
- 微信登录联调:2026-10-12
|
||||
- test/accounts.js
|
||||
- 相机翻转性能优化
|
||||
- report-touch-layout.mjs
|
||||
- 产品推荐规则:肤龄基准与边界调整
|
||||
- ui-20261012/manifest.json
|
||||
- 傅艳NEW与项思炼的会话 2026年10月12日.md
|
||||
- wi
|
||||
- scan-wait-timeout.js
|
||||
- 客户反馈修改(2026-10-12)
|
||||
- .runAndExtendWithFaceDetection
|
||||
- vizCompose
|
||||
- 第三轮反馈:PSD 对齐、验收及上线记录
|
||||
- login-performance.mjs
|
||||
- production-loop.mjs
|
||||
- DetectSingleFaceLandmarksTask
|
||||
- extractAllFacesAndComputeResults
|
||||
- i
|
||||
- capture-funnel-browser.mjs
|
||||
- account-client.js
|
||||
- extractFaceTensors
|
||||
- extractAllFacesAndComputeResults
|
||||
- .runAndExtendWithFaceDetection
|
||||
- wechat-fixture.cjs
|
||||
- .toString
|
||||
- 第四轮反馈/傅艳NEW与项思炼的会话 2026年10月12日.md
|
||||
- 第三轮反馈20261012/傅艳NEW与项思炼的会话 2026年10月12日.md
|
||||
- 基础信息记忆与肤质拖动
|
||||
- profile-interaction.mjs
|
||||
- submitPhotos
|
||||
- doCapture
|
||||
- i
|
||||
- convLayer
|
||||
- resetDetectState
|
||||
- color-copy.js
|
||||
- 微信退出登录-2026-10-13.md
|
||||
- 痘痘按三视图各类型最大数量计分
|
||||
- capture-no-recheck.js
|
||||
- display-grade.js
|
||||
- 肤色文案与实际黑眼圈等级-2026-10-14.md
|
||||
- disposeUnusedWeightTensors
|
||||
- .draw
|
||||
- startCamera
|
||||
- 评分圈内居中、基础信息页固定与正脸语音修复
|
||||
- TinyYolov2
|
||||
- admin-beijing-browser.mjs
|
||||
- 报告等待页实拍背景-2026-10-12.md
|
||||
- height
|
||||
- wrinkle-case-browser.mjs
|
||||
- 后台北京时间统一
|
||||
- 报告底部 Figma 还原
|
||||
- wrinkle-overall.js
|
||||
- 原始肌龄显示与鸿蒙滚轮兼容
|
||||
- harmony-picker.mjs
|
||||
- recommendation.js
|
||||
- 恢复321与色斑案例复测
|
||||
- 七项护理选三与关注优先
|
||||
- 皱纹改用接口整体评分
|
||||
- 拍照报告转化统计-2026-10-16.md
|
||||
- private-case-preview.mjs
|
||||
- FaceLandmarks68
|
||||
- 实测描述与旧护理兼容:修复复测
|
||||
- tone-dark-circle.js
|
||||
- test/accounts.js
|
||||
- 肤色项加入黑眼圈20%,色素图30%叠加
|
||||
- PredictAllAgeAndGenderWithFaceAlignmentTask
|
||||
- NeuralNetwork
|
||||
- 拍摄角度不合格震动提醒-2026-10-13.md
|
||||
- PredictAllFaceExpressionsWithFaceAlignmentTask
|
||||
- report-persistence.mjs
|
||||
- R20261015-104945-ZC5V
|
||||
- wechat-browser.mjs
|
||||
- customer-feedback-fourth.mjs
|
||||
- h5/README.md
|
||||
- 报告文案统一按十分制分档
|
||||
- 水分文案独立分档
|
||||
- R20261015-111214-JKNS
|
||||
- 皱纹三视图标注
|
||||
- age
|
||||
- head-pose.js
|
||||
- contour-eye-copy.js
|
||||
- test/wrinkle-jobs.js
|
||||
- PredictSingleAgeAndGenderWithFaceAlignmentTask
|
||||
- PredictSingleFaceExpressionsWithFaceAlignmentTask
|
||||
- 扫描等待30秒兜底-2026-10-16.md
|
||||
- 线上最近10条成功案例:报告文案对照
|
||||
- R20261015-103650-FVQA
|
||||
- R20261015-105002-HVEV
|
||||
- R20261015-105142-4UTP
|
||||
- R20261015-144549-7CNL
|
||||
- 拍摄光线仅提示-2026-10-16.md
|
||||
- R20261015-104209-V2J6
|
||||
- R20261015-104729-BLXO
|
||||
- R20261015-112350-ZLOF
|
||||
- 逐条页卡文案全文对照
|
||||
- report-copy-cases.js
|
||||
- login-performance.mjs
|
||||
- production-loop.mjs
|
||||
- save
|
||||
- capture-diagnostics.js
|
||||
- FaceLandmark68Net
|
||||
- 取消拍后校验-2026-10-16.md
|
||||
- wi
|
||||
- convLayer
|
||||
- FaceProcessor
|
||||
- extractConvParamsFactory
|
||||
- convLayer$1
|
||||
- .forwardInput
|
||||
- wrinkle-async-browser.mjs
|
||||
- AgeGenderNet
|
||||
- .runAndExtendWithFaceDetections
|
||||
- FaceExpressionNet
|
||||
- FaceFeatureExtractor
|
||||
- DetectSingleFaceLandmarksTask
|
||||
- TinyXception
|
||||
- TinyFaceFeatureExtractor
|
||||
- TinyYolov2
|
||||
- 侧脸皱纹浮层异步加载-2026-10-16.md
|
||||
|
||||
## God Nodes (most connected - your core abstractions)
|
||||
1. `__awaiter()` - 59 edges
|
||||
2. `__generator()` - 59 edges
|
||||
3. `n()` - 37 edges
|
||||
4. `r()` - 35 edges
|
||||
5. `i()` - 33 edges
|
||||
3. `n()` - 39 edges
|
||||
4. `r()` - 36 edges
|
||||
5. `i()` - 34 edges
|
||||
6. `t()` - 32 edges
|
||||
7. `C()` - 32 edges
|
||||
8. `toNetInput()` - 27 edges
|
||||
@@ -138,103 +216,99 @@
|
||||
h5/js/admin.js → h5/vendor/faceapi/face-api.js
|
||||
- `bind()` --indirect_call--> `a()` [INFERRED]
|
||||
h5/js/admin.js → h5/vendor/faceapi/face-api.js
|
||||
- `checkStill()` --indirect_call--> `d()` [INFERRED]
|
||||
h5/js/app.js → h5/vendor/faceapi/face-api.js
|
||||
- `detectFrame()` --indirect_call--> `d()` [INFERRED]
|
||||
h5/js/app.js → h5/vendor/faceapi/face-api.js
|
||||
- `vizWrinkleMarks()` --indirect_call--> `key()` [INFERRED]
|
||||
h5/js/app.js → h5/js/profile-store.js
|
||||
|
||||
## Import Cycles
|
||||
- None detected.
|
||||
|
||||
## Communities (116 total, 25 thin omitted)
|
||||
## Communities (199 total, 45 thin omitted)
|
||||
|
||||
### Community 0 - "face-api.js"
|
||||
Cohesion: 0.02
|
||||
Nodes (41): bgrToRgbTensor(), cn(), computeReshapedDimensions(), createBrowserEnv(), createFileSystem(), createNodejsEnv(), decodeBoxesLayer(), Dimensions() (+33 more)
|
||||
Nodes (46): boxPredictionLayer(), cn(), computeReshapedDimensions(), createBrowserEnv(), createFileSystem(), createNodejsEnv(), decodeBoxesLayer(), depthwiseConvLayer() (+38 more)
|
||||
|
||||
### Community 1 - "线上闭环测试(不含真实微信授权)"
|
||||
Cohesion: 0.25
|
||||
Nodes (7): 发现并修复, 可复现命令, 已通过的检查, 线上闭环测试(不含真实微信授权), 结论, 话术及仓库同步, 验证边界
|
||||
|
||||
### Community 2 - "app.js"
|
||||
Cohesion: 0.11
|
||||
Nodes (22): cancelSkinDrag(), esc(), goSkin(), openHistory(), renderHistoryList(), renderReport(), renderViz(), restartScanCycle() (+14 more)
|
||||
Cohesion: 0.09
|
||||
Nodes (22): cancelSkinDrag(), clearCaptureCountdown(), decode(), drain(), esc(), goSkin(), load(), loginAllowed() (+14 more)
|
||||
|
||||
### Community 3 - "skin-score.js"
|
||||
Cohesion: 0.12
|
||||
Nodes (45): acneGrade(), buildViewModel(), calcAcne(), calcContour(), calcSens(), calcSmooth(), calcTone(), calcWrinkles() (+37 more)
|
||||
Cohesion: 0.07
|
||||
Nodes (64): acneGrade(), buildViewModel(), calcAcne(), calcContour(), calcSens(), calcSmooth(), calcTone(), calcWrinkles() (+56 more)
|
||||
|
||||
### Community 4 - "customer-feedback-second.js"
|
||||
Cohesion: 0.17
|
||||
Nodes (10): assert, context, face, fs, geometry, original, path, source (+2 more)
|
||||
Cohesion: 0.13
|
||||
Nodes (12): assert, context, events, face, fs, geometry, missingHints, original (+4 more)
|
||||
|
||||
### Community 5 - "t"
|
||||
Cohesion: 0.09
|
||||
Nodes (31): be(), bi(), Bo(), ce(), cl(), de(), e(), Ef() (+23 more)
|
||||
|
||||
### Community 6 - "n"
|
||||
Cohesion: 0.14
|
||||
Nodes (37): a(), ae(), bf(), cf(), d(), drawContour(), dt(), ea() (+29 more)
|
||||
### Community 5 - "showScreen"
|
||||
Cohesion: 0.27
|
||||
Nodes (10): enterGuest(), loadAppFont(), loginSucceeded(), logoutWechat(), openOverlay(), prepareScanArtwork(), renderAccountStatus(), resetReport() (+2 more)
|
||||
|
||||
### Community 7 - "test/profile-store.js"
|
||||
Cohesion: 0.07
|
||||
Nodes (29): browser(), init(), read(), rememberKind(), request(), age(), clean(), create() (+21 more)
|
||||
Cohesion: 0.14
|
||||
Nodes (13): assert, data, empty, now, other, Profile, quota, regions (+5 more)
|
||||
|
||||
### Community 8 - "admin.js"
|
||||
Cohesion: 0.20
|
||||
Nodes (29): ageText(), api(), bind(), captureQualityHtml(), closeDetail(), collectFilters(), dimsHtml(), doRetry() (+21 more)
|
||||
Cohesion: 0.18
|
||||
Nodes (32): ageText(), api(), bind(), captureQualityHtml(), closeDetail(), collectFilters(), dimsHtml(), doRetry() (+24 more)
|
||||
|
||||
### Community 9 - "__awaiter"
|
||||
Cohesion: 0.13
|
||||
Nodes (12): __awaiter(), ComposableTask(), fetchImage(), fetchJson(), fetchNetWeights(), fetchOrThrow(), __generator(), getModelUris() (+4 more)
|
||||
Cohesion: 0.12
|
||||
Nodes (13): __awaiter(), ComposableTask(), fetchImage(), fetchJson(), fetchNetWeights(), fetchOrThrow(), __generator(), getModelUris() (+5 more)
|
||||
|
||||
### Community 10 - "wechat-browser.mjs"
|
||||
Cohesion: 0.22
|
||||
Nodes (6): createAccounts, h5, ReportUI, require, root, SkinScore
|
||||
### Community 10 - "n"
|
||||
Cohesion: 0.12
|
||||
Nodes (48): a(), ae(), be(), bf(), ce(), cf(), d(), de() (+40 more)
|
||||
|
||||
### Community 11 - "lib/accounts.js"
|
||||
Cohesion: 0.17
|
||||
Nodes (20): atomic(), body(), cookie(), cookies(), crypto, fs, json(), login() (+12 more)
|
||||
Cohesion: 0.16
|
||||
Nodes (19): body(), cookie(), cookies(), crypto, fs, json(), login(), net (+11 more)
|
||||
|
||||
### Community 12 - "server.js"
|
||||
Cohesion: 0.08
|
||||
Nodes (52): accounts, appendRecord(), callFacepp(), CaptureDiagnostics, CRED, crypto, csvCell(), DATA_DIR (+44 more)
|
||||
Cohesion: 0.06
|
||||
Nodes (65): collect(), hasAnnotation(), accounts, appendRecord(), BeijingTime, callFacepp(), CaptureDiagnostics, captureFunnel (+57 more)
|
||||
|
||||
### Community 13 - "files"
|
||||
Cohesion: 0.12
|
||||
Nodes (15): files, analyze-fail, analyzing, countdown-321, hint-far, hint-front, hint-hold, hint-near (+7 more)
|
||||
|
||||
### Community 15 - "extractFaces"
|
||||
Cohesion: 0.17
|
||||
Nodes (18): awaitMediaLoaded(), bufferToImage(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractImagePatches(), getContext2dOrThrow(), getEnv() (+10 more)
|
||||
### Community 14 - "normalize"
|
||||
Cohesion: 0.26
|
||||
Nodes (8): conv(), denseBlock3(), denseBlock4(), depthwiseSeparableConv(), mainBlock(), normalize(), reductionBlock(), __spreadArrays()
|
||||
|
||||
### Community 15 - "factual-copy.js"
|
||||
Cohesion: 0.13
|
||||
Nodes (13): assert, before, bytes, Copy, display, missing, Original, r (+5 more)
|
||||
|
||||
### Community 16 - "巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范"
|
||||
Cohesion: 0.15
|
||||
Nodes (12): 1. 基准与尺寸换算, 2. 色彩规范, 3. 字体规范, 4. 组件切图(assets/svg/), 5. 图片素材(assets/images/,共 15 张), 6. 页面清单(design/ 下有整屏 PNG 预览), 7. 目录结构, 8. 还原建议 (+4 more)
|
||||
|
||||
### Community 17 - "report-persistence.mjs"
|
||||
Cohesion: 0.29
|
||||
Nodes (6): child, dataRoot, fx, h5, ReportUI, require
|
||||
### Community 17 - "extractFaces"
|
||||
Cohesion: 0.19
|
||||
Nodes (19): awaitMediaLoaded(), bufferToImage(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractImagePatches(), getContext2dOrThrow(), getEnv() (+11 more)
|
||||
|
||||
### Community 18 - "1'me肌肤状态检测 H5"
|
||||
Cohesion: 0.13
|
||||
Nodes (15): 10.09 更新, 1'me肌肤状态检测 H5, Admin API(统一返回 `{ok:false,message}` 错误), Windows 本机部署(提测用), 后台查看(admin.html), 密钥配置, 已知占位清单, 技术说明 (+7 more)
|
||||
|
||||
### Community 19 - "customer-feedback-fourth.js"
|
||||
Cohesion: 0.06
|
||||
Nodes (37): evaluate(), finite(), assertPoints(), check(), clamp(), dist(), estimate(), makeFrontal() (+29 more)
|
||||
Cohesion: 0.12
|
||||
Nodes (17): assert, brokenLandmarks, check(), Diagnostics, face(), HeadPose, held, ReportUI (+9 more)
|
||||
|
||||
### Community 20 - "build_fixtures.py"
|
||||
Cohesion: 0.40
|
||||
Nodes (9): build_case1(), build_case2(), build_truth(), col(), dump(), main(), parse_value(), set_path() (+1 more)
|
||||
|
||||
### Community 21 - "disposeUnusedWeightTensors"
|
||||
Cohesion: 0.05
|
||||
Nodes (39): conv(), conv$1(), convDown(), convLayer$1(), convNoRelu(), denseBlock3(), denseBlock4(), depthwiseSeparableConv() (+31 more)
|
||||
|
||||
### Community 22 - "k"
|
||||
Cohesion: 0.10
|
||||
Nodes (21): b(), ba(), ca(), da(), fa(), gn(), k(), kn() (+13 more)
|
||||
### Community 22 - "acne-three-view.js"
|
||||
Cohesion: 0.14
|
||||
Nodes (14): assert, capped, empty, heavy, keys, node(), partial, r (+6 more)
|
||||
|
||||
### Community 23 - "facepp-skinanalyze(旷视皮肤分析专业版调用技能)"
|
||||
Cohesion: 0.20
|
||||
@@ -248,17 +322,17 @@ Nodes (8): 1. 音频格式约定(已定), 2. 播报清单(14 条), 3. 播放策
|
||||
Cohesion: 0.20
|
||||
Nodes (16): _arrayLikeToArray(), _arrayWithHoles(), _arrayWithoutHoles(), _classCallCheck(), _createClass(), _defineProperties(), h(), _iterableToArray() (+8 more)
|
||||
|
||||
### Community 26 - "NeuralNetwork"
|
||||
Cohesion: 0.05
|
||||
Nodes (30): AgeGenderNet(), createFaceDetectionNet(), createFaceRecognitionNet(), createMtcnn(), createSsdMobilenetv1(), createTinyFaceDetector(), createTinyYolov2(), extractConvParamsFactory() (+22 more)
|
||||
### Community 26 - "display-age-recommendation.js"
|
||||
Cohesion: 0.20
|
||||
Nodes (9): assert, before, bytes, old, presentation, saved, SS, UI (+1 more)
|
||||
|
||||
### Community 27 - "gen_voice.py"
|
||||
Cohesion: 0.30
|
||||
Nodes (11): ndarray, Path, gen_countdown(), main(), normalize_clip(), 倒计时单独按字生成再拼接,避免「三、二、一」整句被读成播报腔、与提示音气质漂移。 与提示音共用同一 speaker + instruct;句间留约…, 裁掉过长首尾静音,保留 ≤150ms 边沿,避免提示显迟钝。, 响度归一 −16 LUFS + 转 MP3 48k CBR mono 22050Hz。 (+3 more)
|
||||
|
||||
### Community 28 - "C"
|
||||
Cohesion: 0.11
|
||||
Nodes (25): ao(), C(), cd(), co(), Do(), eo(), fo(), Ft() (+17 more)
|
||||
Cohesion: 0.09
|
||||
Nodes (31): ao(), Bo(), C(), cd(), co(), Do(), eo(), fo() (+23 more)
|
||||
|
||||
### Community 29 - "face-api.js 本地化依赖(h5/vendor/faceapi/)"
|
||||
Cohesion: 0.29
|
||||
@@ -273,13 +347,17 @@ Cohesion: 0.60
|
||||
Nodes (4): main(), 递归缩放 result 内全部坐标:{left,top,width,height} 矩形与 {x,y} 点,与原数据同为整数。, scale_coords(), to_data_url()
|
||||
|
||||
### Community 35 - "customer-feedback.js"
|
||||
Cohesion: 0.06
|
||||
Nodes (27): alternate, approved, assert, crop, details, excellent, fs, model (+19 more)
|
||||
Cohesion: 0.05
|
||||
Nodes (32): acneBefore, acneMarks, acneRaw, alternate, approved, assert, crop, details (+24 more)
|
||||
|
||||
### Community 37 - "UI 外发版 10.09 图片资源"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): UI 外发版 10.09 图片资源, 前端素材对应, 文件位置, 重新导出
|
||||
|
||||
### Community 38 - "最终确认:只改两处文案"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): 保留的逻辑修复, 最终确认:只改两处文案, 验证
|
||||
|
||||
### Community 39 - "UI交互界面调整"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 1meAI测肤旷视UI问题反馈, UI交互界面调整, 交互, **设计**
|
||||
@@ -288,41 +366,61 @@ Nodes (4): 1meAI测肤旷视UI问题反馈, UI交互界面调整, 交互, **设
|
||||
Cohesion: 0.22
|
||||
Nodes (8): PSD 来源, 前端参数及限制, 后台校准留痕, 已确认并实现, 生产部署及只读验收, 皱纹(紧塑力)原文, 第四轮反馈修改与验收, 验证
|
||||
|
||||
### Community 41 - "showScreen"
|
||||
Cohesion: 0.22
|
||||
Nodes (11): loadAppFont(), loginAllowed(), loginBrowser(), loginSucceeded(), loginWechat(), openOverlay(), prepareScanArtwork(), renderAccountStatus() (+3 more)
|
||||
### Community 41 - "test/capture-funnel.js"
|
||||
Cohesion: 0.06
|
||||
Nodes (36): boundary(), day(), format(), milliseconds(), range(), shifted(), BeijingTime, extras (+28 more)
|
||||
|
||||
### Community 42 - "layer_at"
|
||||
Cohesion: 0.18
|
||||
Nodes (11): list, bounds(), layer_at(), main(), save_image(), composite(), main(), sha256() (+3 more)
|
||||
|
||||
### Community 43 - "t"
|
||||
Cohesion: 0.14
|
||||
Nodes (14): bi(), Ef(), er(), ia(), Ie(), j(), Kt(), Mf() (+6 more)
|
||||
|
||||
### Community 44 - "start-local.sh"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): HOST, PORT, start-local.sh script
|
||||
|
||||
### Community 45 - "dimension-cap.js"
|
||||
Cohesion: 0.22
|
||||
Nodes (8): assert, before, legacy, r, S, saved, UI, vm
|
||||
|
||||
### Community 46 - "zoom-fill.js"
|
||||
Cohesion: 0.18
|
||||
Nodes (10): assert, canvases, context, drawn, frame, fs, helper, img (+2 more)
|
||||
|
||||
### Community 47 - ".extractWeights"
|
||||
Cohesion: 0.15
|
||||
Nodes (14): createFaceDetectionNet(), createFaceRecognitionNet(), createMtcnn(), createSsdMobilenetv1(), createTinyFaceDetector(), createTinyYolov2(), extractFCParamsFactory(), extractorsFactory$2() (+6 more)
|
||||
|
||||
### Community 48 - "已修改"
|
||||
Cohesion: 0.22
|
||||
Nodes (8): 12 日第二批:明确项修改, 公网部署与复核(2026-10-12,北京时间), 准备捕捉提醒, 字重, 已修改, 拍照展示, 首页高清批注, 验证与状态
|
||||
|
||||
### Community 53 - "护理建议(全量文本,按需求摘抄)"
|
||||
Cohesion: 0.10
|
||||
Nodes (19): 5\.1 轮廓(紧塑力), 5\.2 皱纹(紧塑力), 5\.3 肤色(透亮感), 5\.4 平滑度(透亮感), 5\.5 水分(透亮感), 5\.6 泛红(稳韧力), 5\.7 痘痘(稳韧力), 6\.1 轮廓 (+11 more)
|
||||
Nodes (20): 2026-10-15 本次确认范围, 5\.1 轮廓(紧塑力), 5\.2 皱纹(紧塑力), 5\.3 肤色(透亮感), 5\.4 平滑度(透亮感), 5\.5 水分(透亮感), 5\.6 泛红(稳韧力), 5\.7 痘痘(稳韧力) (+12 more)
|
||||
|
||||
### Community 54 - "UI 对齐 10.09 设计稿:调研与实施计划"
|
||||
Cohesion: 0.11
|
||||
Nodes (17): 1. 结论和依据, 2. 统一比较口径, 3. 逐页差异和调整内容, 4. 报告数据如何接到新 UI, 5. 登录与“我的报告”的真实能力缺口, 6. 原稿中需要整理的内容, 7. 实施顺序与交付物, 8. 验收定义 (+9 more)
|
||||
|
||||
### Community 55 - "guest-privacy.mjs"
|
||||
Cohesion: 0.25
|
||||
Nodes (5): child, dataRoot, fx, h5, require
|
||||
|
||||
### Community 56 - "report-ui.js"
|
||||
Cohesion: 0.50
|
||||
Nodes (7): build(), count(), fill(), focusOf(), numeric(), scalar(), wrinkleDetails()
|
||||
Cohesion: 0.35
|
||||
Nodes (12): build(), count(), dimensionScore(), fill(), focusOf(), forDisplay(), internalLevel(), levelForScore() (+4 more)
|
||||
|
||||
### Community 57 - "regression.js"
|
||||
Cohesion: 0.60
|
||||
Nodes (5): diffCtx(), loadJSON(), main(), okNum(), okStr()
|
||||
|
||||
### Community 58 - ".forwardInput"
|
||||
Cohesion: 0.19
|
||||
Nodes (8): FaceRecognitionNet(), iou(), nonMaxSuppression(), nonMaxSuppression$1(), scale(), stage1(), stage2(), stage3()
|
||||
Cohesion: 0.11
|
||||
Nodes (16): height(), extractBoundingBoxes(), FaceLandmark68NetBase(), FaceRecognitionNet(), getSizesForScale(), iou(), nonMaxSuppression(), nonMaxSuppression$1() (+8 more)
|
||||
|
||||
### Community 61 - "报告布局与跟手交互调整"
|
||||
Cohesion: 0.33
|
||||
@@ -336,14 +434,18 @@ Nodes (4): 仍需公网/业务确认, 已发现并修复, 提交前 Review(202
|
||||
Cohesion: 0.29
|
||||
Nodes (4): EXPECTED, fails, pageErrors, pwRoot
|
||||
|
||||
### Community 64 - "start"
|
||||
Cohesion: 0.29
|
||||
Nodes (7): decode(), drain(), load(), preload(), start(), stopCur(), unlock()
|
||||
### Community 64 - "综合得分:三维度按 4:3:3 加权"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 已确认公式, 本地验证, 生效位置, 综合得分:三维度按 4:3:3 加权
|
||||
|
||||
### Community 65 - "微信登录接入与公网部署说明"
|
||||
Cohesion: 0.29
|
||||
Nodes (7): 公网服务器配置, 公网真机验收清单, 反向代理与后台保护, 已完成的链路, 微信登录接入与公网部署说明, 数据、迁移与备份, 本机开发环境限制(不代表线上状态)
|
||||
|
||||
### Community 66 - "capture-quality.js"
|
||||
Cohesion: 0.60
|
||||
Nodes (3): evaluate(), finite(), scoreThreshold()
|
||||
|
||||
### Community 68 - "analyze-timeout.js"
|
||||
Cohesion: 0.20
|
||||
Nodes (7): assert, end, fs, path, source, start, vm
|
||||
@@ -352,20 +454,24 @@ Nodes (7): assert, end, fs, path, source, start, vm
|
||||
Cohesion: 0.29
|
||||
Nodes (3): errors, samples, unexpectedWrites
|
||||
|
||||
### Community 77 - "drawFaceExpressions"
|
||||
Cohesion: 0.12
|
||||
Nodes (10): DrawBox(), drawDetections(), drawFaceExpressions(), DrawFaceLandmarks(), FaceExpressions(), isWithFaceDetection(), isWithFaceExpressions(), isWithFaceLandmarks() (+2 more)
|
||||
### Community 74 - "water-copy.js"
|
||||
Cohesion: 0.18
|
||||
Nodes (9): assert, Copy, crossed, example, old, savedBefore, SS, UI (+1 more)
|
||||
|
||||
### Community 78 - ".toString"
|
||||
Cohesion: 0.12
|
||||
Nodes (11): FaceLandmarks5(), FaceMatch(), getCenterPoint(), la(), Point(), pr(), pt(), vt() (+3 more)
|
||||
### Community 77 - "care-selection.js"
|
||||
Cohesion: 0.22
|
||||
Nodes (9): assert, check(), Copy, heavy, keys, sample(), SS, UI (+1 more)
|
||||
|
||||
### Community 78 - "drawFaceExpressions"
|
||||
Cohesion: 0.22
|
||||
Nodes (6): DrawBox(), drawDetections(), drawFaceExpressions(), DrawFaceLandmarks(), FaceExpressions(), isWithFaceExpressions()
|
||||
|
||||
### Community 79 - "登录反馈与加载优化(2026-10-12)"
|
||||
Cohesion: 0.33
|
||||
Cohesion: 0.25
|
||||
Nodes (5): 体验变化, 公网部署后复测, 慢的原因与处理, 登录反馈与加载优化(2026-10-12), 验证
|
||||
|
||||
### Community 80 - "线上微信登录部署记录"
|
||||
Cohesion: 0.33
|
||||
Cohesion: 0.29
|
||||
Nodes (6): 初次部署时的凭据查找记录(现已由用户提供), 初次部署的线上检查(历史结果), 后续更新:已配置真实凭据, 已执行, 用户确认加入白名单后的复测, 线上微信登录部署记录
|
||||
|
||||
### Community 81 - "wechat-production.mjs"
|
||||
@@ -377,16 +483,16 @@ Cohesion: 0.40
|
||||
Nodes (4): 参数对照, 拍摄约束放宽与标题更新, 标题, 验证与发布
|
||||
|
||||
### Community 83 - "10.09 新版 H5 实施与验收"
|
||||
Cohesion: 0.29
|
||||
Nodes (5): 10.09 新版 H5 实施与验收, 按确认项实现, 文档未定义部分的实施口径, 素材再生成, 验证命令
|
||||
Cohesion: 0.22
|
||||
Nodes (6): 10.09 新版 H5 实施与验收, 按确认项实现, 文档未定义部分的实施口径, 素材再生成, 验证命令, 产品推荐按报告展示肌龄
|
||||
|
||||
### Community 84 - "微信登录联调:2026-10-12"
|
||||
Cohesion: 0.22
|
||||
Cohesion: 0.33
|
||||
Nodes (6): 后续真机确认与体验优化, 微信登录联调:2026-10-12, 本轮修正, 白名单与凭据, 真实微信验收与排查, 自动验证
|
||||
|
||||
### Community 85 - "test/accounts.js"
|
||||
Cohesion: 0.22
|
||||
Nodes (9): assert, create, fs, http, os, path, ReportUI, SkinScore (+1 more)
|
||||
### Community 85 - "相机翻转性能优化"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 仍需真机验证, 修改范围, 相机翻转性能优化, 验证
|
||||
|
||||
### Community 87 - "产品推荐规则:肤龄基准与边界调整"
|
||||
Cohesion: 0.33
|
||||
@@ -396,89 +502,293 @@ Nodes (6): 产品推荐规则:肤龄基准与边界调整, 决策顺序, 后
|
||||
Cohesion: 0.22
|
||||
Nodes (8): assets, design_scale, document_size, native_header_px, shape_metrics, source, source_sha256, text_styles
|
||||
|
||||
### Community 91 - "wi"
|
||||
Cohesion: 0.28
|
||||
Nodes (9): Ci(), Ei(), Fi(), Ii(), ki(), pe(), ri(), wi() (+1 more)
|
||||
### Community 91 - "scan-wait-timeout.js"
|
||||
Cohesion: 0.22
|
||||
Nodes (6): assert, fs, helpers, source, submit, vm
|
||||
|
||||
### Community 92 - "客户反馈修改(2026-10-12)"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): 公网部署与复测, 客户反馈修改(2026-10-12), 皱纹数据说明, 逐项处理, 验证
|
||||
|
||||
### Community 93 - ".runAndExtendWithFaceDetection"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): allFacesMtcnn(), allFacesSsdMobilenetv1(), allFacesTinyYolov2(), detectAllFaces(), DetectSingleFaceTask()
|
||||
### Community 93 - "vizCompose"
|
||||
Cohesion: 0.17
|
||||
Nodes (15): loadReportWrinkles(), renderContourSides(), renderReport(), renderViz(), renderWrinkleSides(), setDim(), vizCollectMarks(), vizCompose() (+7 more)
|
||||
|
||||
### Community 94 - "第三轮反馈:PSD 对齐、验收及上线记录"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): 原稿与导出可追溯性, 已完成, 本地验证, 生产部署及只读验收, 第三轮反馈:PSD 对齐、验收及上线记录
|
||||
|
||||
### Community 96 - "production-loop.mjs"
|
||||
### Community 95 - "i"
|
||||
Cohesion: 0.07
|
||||
Nodes (37): b(), ba(), bn(), ca(), cl(), da(), dn(), ee() (+29 more)
|
||||
|
||||
### Community 96 - "capture-funnel-browser.mjs"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): artifacts, errors, samples
|
||||
Nodes (3): child, h5, root
|
||||
|
||||
### Community 100 - "extractAllFacesAndComputeResults"
|
||||
### Community 97 - "account-client.js"
|
||||
Cohesion: 0.53
|
||||
Nodes (8): browser(), clearGuestCache(), ensureGuest(), guest(), init(), logout(), rememberKind(), request()
|
||||
|
||||
### Community 98 - "extractFaceTensors"
|
||||
Cohesion: 0.09
|
||||
Nodes (12): ComputeAllFaceDescriptorsTask(), ComputeSingleFaceDescriptorTask(), extendWithAge(), extendWithFaceDescriptor(), extendWithFaceExpressions(), extendWithGender(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult() (+4 more)
|
||||
Nodes (13): bgrToRgbTensor(), Box(), DetectAllFaceLandmarksTask(), extendWithFaceDetection(), extendWithFaceLandmarks(), extractFaceTensors(), FaceLandmarks(), isTensor3D() (+5 more)
|
||||
|
||||
### Community 102 - "extractFaceTensors"
|
||||
### Community 99 - "extractAllFacesAndComputeResults"
|
||||
Cohesion: 0.10
|
||||
Nodes (11): Box(), DetectAllFaceLandmarksTask(), extendWithFaceDetection(), extendWithFaceLandmarks(), extractFaceTensors(), FaceLandmarks(), isDimensions(), isTensor3D() (+3 more)
|
||||
Nodes (11): ComputeAllFaceDescriptorsTask(), ComputeSingleFaceDescriptorTask(), extendWithAge(), extendWithFaceDescriptor(), extendWithFaceExpressions(), extendWithGender(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult() (+3 more)
|
||||
|
||||
### Community 107 - "基础信息记忆与肤质拖动"
|
||||
### Community 100 - ".runAndExtendWithFaceDetection"
|
||||
Cohesion: 0.33
|
||||
Nodes (4): 回归命令, 基础信息记忆与肤质拖动, 肤质交互, 范围
|
||||
Nodes (5): allFacesMtcnn(), allFacesSsdMobilenetv1(), allFacesTinyYolov2(), detectAllFaces(), DetectSingleFaceTask()
|
||||
|
||||
### Community 101 - "wechat-fixture.cjs"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): crypto, fs, path
|
||||
|
||||
### Community 102 - ".toString"
|
||||
Cohesion: 0.12
|
||||
Nodes (11): FaceLandmarks5(), FaceMatch(), getCenterPoint(), la(), Point(), pr(), pt(), vt() (+3 more)
|
||||
|
||||
### Community 108 - "profile-interaction.mjs"
|
||||
Cohesion: 0.40
|
||||
Nodes (3): dragBy(), h5, touch()
|
||||
|
||||
### Community 109 - "submitPhotos"
|
||||
Cohesion: 0.24
|
||||
Nodes (12): bindBackdropClose(), closeLoginSuccess(), closeOverlay(), handleCamHidden(), leaveCamera(), photosByView(), popHistorySilently(), requestAnalyze() (+4 more)
|
||||
Cohesion: 0.22
|
||||
Nodes (14): beginCaptureJourney(), bindBackdropClose(), closeLoginSuccess(), closeOverlay(), handleCamHidden(), leaveCamera(), photosByView(), popHistorySilently() (+6 more)
|
||||
|
||||
### Community 110 - "doCapture"
|
||||
### Community 110 - "resetDetectState"
|
||||
Cohesion: 0.13
|
||||
Nodes (27): afterCurrent(), armNudge(), busyWithFlow(), captureHaptic(), captureStageHint(), detectFrame(), detTick(), doCapture() (+19 more)
|
||||
|
||||
### Community 111 - "color-copy.js"
|
||||
Cohesion: 0.25
|
||||
Nodes (7): assert, descriptions, missing, SS, templates, UI, v
|
||||
|
||||
### Community 115 - "痘痘按三视图各类型最大数量计分"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 后台和历史, 痘痘按三视图各类型最大数量计分, 规则, 验证
|
||||
|
||||
### Community 116 - "capture-no-recheck.js"
|
||||
Cohesion: 0.18
|
||||
Nodes (10): assert, completed, ctx, Diagnostics, fileFunction, fs, normalized, path (+2 more)
|
||||
|
||||
### Community 117 - "display-grade.js"
|
||||
Cohesion: 0.17
|
||||
Nodes (22): armNudge(), busyWithFlow(), captureFromVideo(), captureHaptic(), captureStageHint(), checkStill(), doCapture(), dropHint() (+14 more)
|
||||
Nodes (11): assert, Copy, display, keys, old, oldBytes, r, SS (+3 more)
|
||||
|
||||
### Community 111 - "i"
|
||||
Cohesion: 0.12
|
||||
Nodes (18): bn(), dn(), ee(), et(), fn(), i(), je(), ln() (+10 more)
|
||||
### Community 120 - "disposeUnusedWeightTensors"
|
||||
Cohesion: 0.10
|
||||
Nodes (23): disposeUnusedWeightTensors(), extractorsFactory$3(), extractorsFactory$5(), extractorsFactory$7(), extractorsFactory$9(), extractParamsFromWeigthMap(), extractParamsFromWeigthMap$1(), extractParamsFromWeigthMap$2() (+15 more)
|
||||
|
||||
### Community 116 - "convLayer"
|
||||
Cohesion: 0.14
|
||||
Nodes (17): boxPredictionLayer(), convLayer(), convWithBatchNorm(), depthwiseConvLayer(), depthwiseSeparableConv$1(), fullyConnectedLayer(), getStridesForLayerIdx(), leaky() (+9 more)
|
||||
|
||||
### Community 117 - "startCamera"
|
||||
Cohesion: 0.15
|
||||
Nodes (17): applyMirror(), beginDetect(), detectFrame(), detTick(), effMirror(), enableFallback(), getCamStream(), handleCamVisible() (+9 more)
|
||||
### Community 122 - "startCamera"
|
||||
Cohesion: 0.19
|
||||
Nodes (14): applyMirror(), beginDetect(), effMirror(), enableFallback(), getCamStream(), loadFaceApi(), loadModels(), onCamTrackEnded() (+6 more)
|
||||
|
||||
### Community 123 - "评分圈内居中、基础信息页固定与正脸语音修复"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 发布, 范围, 评分圈内居中、基础信息页固定与正脸语音修复, 验证
|
||||
|
||||
### Community 127 - "height"
|
||||
### Community 124 - "admin-beijing-browser.mjs"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): child, file, records, root, times
|
||||
|
||||
### Community 126 - "wrinkle-case-browser.mjs"
|
||||
Cohesion: 0.22
|
||||
Nodes (8): child, profile, projected, raw, require, root, SS, UI
|
||||
|
||||
### Community 128 - "后台北京时间统一"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): 修改, 后台北京时间统一, 验证
|
||||
|
||||
### Community 129 - "报告底部 Figma 还原"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): 报告底部 Figma 还原, 静态素材核对, 验证
|
||||
|
||||
### Community 130 - "wrinkle-overall.js"
|
||||
Cohesion: 0.25
|
||||
Nodes (6): assert, base, changed, onlyOverall, SS, UI
|
||||
|
||||
### Community 131 - "原始肌龄显示与鸿蒙滚轮兼容"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): 原始肌龄显示与鸿蒙滚轮兼容, 回归, 范围
|
||||
|
||||
### Community 133 - "recommendation.js"
|
||||
Cohesion: 0.33
|
||||
Nodes (6): assert, check(), missing, raw, SS, UI
|
||||
|
||||
### Community 135 - "恢复321与色斑案例复测"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): 恢复321与色斑案例复测, 拍摄提示音, 真实色斑案例
|
||||
|
||||
### Community 136 - "七项护理选三与关注优先"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): 七项护理选三与关注优先, 真实案例回放, 验证
|
||||
|
||||
### Community 137 - "皱纹改用接口整体评分"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): 案例复算, 皱纹改用接口整体评分, 验证
|
||||
|
||||
### Community 139 - "private-case-preview.mjs"
|
||||
Cohesion: 0.29
|
||||
Nodes (6): fixture, mime, photos, raw, root, server
|
||||
|
||||
### Community 141 - "实测描述与旧护理兼容:修复复测"
|
||||
Cohesion: 0.22
|
||||
Nodes (8): R20261015-103650-FVQA, R20261015-111214-JKNS, R20261015-144549-7CNL, 同一批10条真实案例复测, 实测描述与旧护理兼容:修复复测, 新版代表文案, 本次修改, 自动化回归
|
||||
|
||||
### Community 142 - "tone-dark-circle.js"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): a, assert, near(), SS, UI
|
||||
|
||||
### Community 143 - "test/accounts.js"
|
||||
Cohesion: 0.22
|
||||
Nodes (9): assert, create, fs, http, os, path, ReportUI, SkinScore (+1 more)
|
||||
|
||||
### Community 144 - "肤色项加入黑眼圈20%,色素图30%叠加"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 图像, 用户确认的评分口径, 肤色项加入黑眼圈20%,色素图30%叠加, 验证
|
||||
|
||||
### Community 149 - "report-persistence.mjs"
|
||||
Cohesion: 0.29
|
||||
Nodes (6): child, dataRoot, fx, h5, ReportUI, require
|
||||
|
||||
### Community 150 - "R20261015-104945-ZC5V"
|
||||
Cohesion: 0.29
|
||||
Nodes (7): R20261015-104945-ZC5V, 水分:改善→优秀(新版十分制显示 9.4), 泛红:改善→优秀(新版十分制显示 9.3), 痘痘:良好→优秀(新版十分制显示 9.6), 皱纹:改善→优秀(新版十分制显示 9.0), 肤色:改善→优秀(新版十分制显示 9.4), 轮廓:良好→优秀(新版十分制显示 9.7)
|
||||
|
||||
### Community 151 - "wechat-browser.mjs"
|
||||
Cohesion: 0.22
|
||||
Nodes (6): createAccounts, h5, ReportUI, require, root, SkinScore
|
||||
|
||||
### Community 152 - "customer-feedback-fourth.mjs"
|
||||
Cohesion: 0.29
|
||||
Nodes (6): CaptureQuality, child, dataRoot, h5, ReportUI, require
|
||||
|
||||
### Community 155 - "h5/README.md"
|
||||
Cohesion: 0.15
|
||||
Nodes (9): 回归命令, 基础信息记忆与肤质拖动, 肤质交互, 范围, 发版范围, 游客不留存与弹窗护理间距调整, 验证, 真实案例回放(当前规则) (+1 more)
|
||||
|
||||
### Community 156 - "报告文案统一按十分制分档"
|
||||
Cohesion: 0.29
|
||||
Nodes (6): 历史数据, 原因与规则, 回归, 报告文案统一按十分制分档, 真实案例复核, 覆盖范围
|
||||
|
||||
### Community 158 - "R20261015-111214-JKNS"
|
||||
Cohesion: 0.29
|
||||
Nodes (7): R20261015-111214-JKNS, 平滑度:良好→优秀(新版十分制显示 9.7), 水分:改善→优秀(新版十分制显示 9.9), 痘痘:良好→优秀(新版十分制显示 9.9), 皱纹:改善→优秀(新版十分制显示 9.2), 肤色:改善→良好(新版十分制显示 8.8), 轮廓:优秀→优秀(新版十分制显示 9.7)
|
||||
|
||||
### Community 160 - "age"
|
||||
Cohesion: 0.36
|
||||
Nodes (4): age(), clean(), create(), key()
|
||||
|
||||
### Community 161 - "head-pose.js"
|
||||
Cohesion: 0.33
|
||||
Nodes (8): assertPoints(), check(), clamp(), dist(), estimate(), makeFrontal(), makeSide(), midpoint()
|
||||
|
||||
### Community 162 - "contour-eye-copy.js"
|
||||
Cohesion: 0.20
|
||||
Nodes (9): assert, base, before, display, expected, raw, S, saved (+1 more)
|
||||
|
||||
### Community 163 - "test/wrinkle-jobs.js"
|
||||
Cohesion: 0.13
|
||||
Nodes (17): crypto, fs, path, register(), remember(), run(), save(), unavailable() (+9 more)
|
||||
|
||||
### Community 167 - "线上最近10条成功案例:报告文案对照"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): 10条总览, 代表性差异, 发布前建议, 线上最近10条成功案例:报告文案对照, 结论
|
||||
|
||||
### Community 168 - "R20261015-103650-FVQA"
|
||||
Cohesion: 0.33
|
||||
Nodes (6): R20261015-103650-FVQA, 水分:良好→优秀(新版十分制显示 9.9), 泛红:改善→良好(新版十分制显示 8.4), 痘痘:良好→优秀(新版十分制显示 9.7), 皱纹:改善→优秀(新版十分制显示 9.3), 肤色:良好→优秀(新版十分制显示 9.5)
|
||||
|
||||
### Community 169 - "R20261015-105002-HVEV"
|
||||
Cohesion: 0.33
|
||||
Nodes (6): R20261015-105002-HVEV, 平滑度:良好→优秀(新版十分制显示 9.5), 水分:良好→优秀(新版十分制显示 9.9), 痘痘:良好→优秀(新版十分制显示 9.9), 皱纹:改善→优秀(新版十分制显示 9.0), 肤色:良好→优秀(新版十分制显示 9.3)
|
||||
|
||||
### Community 170 - "R20261015-105142-4UTP"
|
||||
Cohesion: 0.33
|
||||
Nodes (6): R20261015-105142-4UTP, 平滑度:良好→优秀(新版十分制显示 9.5), 水分:良好→优秀(新版十分制显示 9.9), 痘痘:良好→优秀(新版十分制显示 9.6), 皱纹:改善→良好(新版十分制显示 8.9), 肤色:良好→优秀(新版十分制显示 9.3)
|
||||
|
||||
### Community 171 - "R20261015-144549-7CNL"
|
||||
Cohesion: 0.33
|
||||
Nodes (6): R20261015-144549-7CNL, 泛红:改善→优秀(新版十分制显示 9.3), 痘痘:良好→优秀(新版十分制显示 9.9), 皱纹:改善→优秀(新版十分制显示 9.3), 肤色:良好→优秀(新版十分制显示 9.6), 轮廓:改善→优秀(新版十分制显示 9.4)
|
||||
|
||||
### Community 173 - "R20261015-104209-V2J6"
|
||||
Cohesion: 0.40
|
||||
Nodes (5): R20261015-104209-V2J6, 水分:良好→优秀(新版十分制显示 9.9), 痘痘:良好→优秀(新版十分制显示 9.9), 皱纹:改善→优秀(新版十分制显示 9.1), 肤色:良好→优秀(新版十分制显示 9.4)
|
||||
|
||||
### Community 174 - "R20261015-104729-BLXO"
|
||||
Cohesion: 0.40
|
||||
Nodes (5): R20261015-104729-BLXO, 水分:良好→优秀(新版十分制显示 10.0), 痘痘:良好→优秀(新版十分制显示 9.9), 皱纹:改善→优秀(新版十分制显示 9.0), 肤色:良好→优秀(新版十分制显示 9.4)
|
||||
|
||||
### Community 175 - "R20261015-112350-ZLOF"
|
||||
Cohesion: 0.40
|
||||
Nodes (5): R20261015-112350-ZLOF, 水分:良好→优秀(新版十分制显示 9.9), 泛红:改善→良好(新版十分制显示 8.7), 痘痘:良好→优秀(新版十分制显示 9.7), 肤色:良好→优秀(新版十分制显示 9.4)
|
||||
|
||||
### Community 176 - "逐条页卡文案全文对照"
|
||||
Cohesion: 0.40
|
||||
Nodes (5): R20261015-134214-Y6L7, 痘痘:良好→优秀(新版十分制显示 9.9), 皱纹:改善→优秀(新版十分制显示 9.2), 轮廓:改善→优秀(新版十分制显示 9.4), 逐条页卡文案全文对照
|
||||
|
||||
### Community 177 - "report-copy-cases.js"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): assert, cases, fs, UI
|
||||
|
||||
### Community 180 - "production-loop.mjs"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): artifacts, errors, samples
|
||||
|
||||
### Community 181 - "save"
|
||||
Cohesion: 0.50
|
||||
Nodes (5): atomic(), reportSnapshot(), save(), storeReport(), updateWrinkleViews()
|
||||
|
||||
### Community 182 - "capture-diagnostics.js"
|
||||
Cohesion: 0.38
|
||||
Nodes (5): height(), FaceLandmark68NetBase(), getSizesForScale(), normalize$1(), rescaleAndNormalize()
|
||||
Nodes (6): assess(), CaptureQuality, HeadPose, normalize(), scalar(), views
|
||||
|
||||
### Community 188 - "wi"
|
||||
Cohesion: 0.28
|
||||
Nodes (9): Ci(), Ei(), Fi(), Ii(), ki(), pe(), ri(), wi() (+1 more)
|
||||
|
||||
### Community 189 - "convLayer"
|
||||
Cohesion: 0.36
|
||||
Nodes (8): convLayer(), fullyConnectedLayer(), ONet(), PNet(), prelu(), qh(), RNet(), sharedLayer()
|
||||
|
||||
### Community 191 - "extractConvParamsFactory"
|
||||
Cohesion: 0.24
|
||||
Nodes (9): extractConvParamsFactory(), extractorsFactory(), extractorsFactory$1(), extractorsFactory$6(), extractorsFactory$8(), extractParams(), extractParams$6(), extractParamsTiny() (+1 more)
|
||||
|
||||
### Community 192 - "convLayer$1"
|
||||
Cohesion: 0.47
|
||||
Nodes (6): conv$1(), convDown(), convLayer$1(), convNoRelu(), residual(), residualDown()
|
||||
|
||||
### Community 193 - ".forwardInput"
|
||||
Cohesion: 0.47
|
||||
Nodes (3): convWithBatchNorm(), depthwiseSeparableConv$1(), leaky()
|
||||
|
||||
### Community 194 - "wrinkle-async-browser.mjs"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): child, dir, h5, require
|
||||
|
||||
## Knowledge Gaps
|
||||
- **320 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+315 more)
|
||||
- **653 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+648 more)
|
||||
These have ≤1 connection - possible missing edges or undocumented components.
|
||||
- **25 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||
- **45 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||
|
||||
## Suggested Questions
|
||||
_Questions this graph is uniquely positioned to answer:_
|
||||
|
||||
- **Why does `height()` connect `height` to `app.js`, `customer-feedback.js`, `scan-placeholder.mjs`, `extractFaceTensors`, `showScreen`, `__awaiter`, `profile-interaction.mjs`, `extractFaces`, `.forwardInput`?**
|
||||
_High betweenness centrality (0.062) - this node is a cross-community bridge._
|
||||
- **Why does `v()` connect `n` to `report-ui.js`, `face-api.js`, `app.js`, `C`?**
|
||||
- **Why does `height()` connect `.forwardInput` to `app.js`, `customer-feedback.js`, `extractFaceTensors`, `showScreen`, `scan-placeholder.mjs`, `profile-interaction.mjs`, `extractFaces`?**
|
||||
_High betweenness centrality (0.045) - this node is a cross-community bridge._
|
||||
- **Why does `k()` connect `k` to `face-api.js`, `app.js`, `t`, `n`, `lib/accounts.js`?**
|
||||
_High betweenness centrality (0.039) - this node is a cross-community bridge._
|
||||
- **Are the 24 inferred relationships involving `n()` (e.g. with `newRecordId()` and `ae()`) actually correct?**
|
||||
_`n()` has 24 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Are the 27 inferred relationships involving `r()` (e.g. with `handleExport()` and `handleRecordsQuery()`) actually correct?**
|
||||
_`r()` has 27 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Are the 9 inferred relationships involving `i()` (e.g. with `da()` and `f()`) actually correct?**
|
||||
_`i()` has 9 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Why does `r()` connect `n` to `face-api.js`, `.toString`, `test/capture-funnel.js`, `t`, `server.js`, `normalize`, `C`, `i`?**
|
||||
_High betweenness centrality (0.035) - this node is a cross-community bridge._
|
||||
- **Why does `n()` connect `n` to `face-api.js`, `skin-score.js`, `t`, `server.js`, `acne-three-view.js`, `C`, `i`?**
|
||||
_High betweenness centrality (0.027) - this node is a cross-community bridge._
|
||||
- **Are the 26 inferred relationships involving `n()` (e.g. with `calcAcne()` and `newRecordId()`) actually correct?**
|
||||
_`n()` has 26 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Are the 28 inferred relationships involving `r()` (e.g. with `ingest()` and `handleExport()`) actually correct?**
|
||||
_`r()` has 28 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Are the 10 inferred relationships involving `i()` (e.g. with `report-persistence.mjs` and `da()`) actually correct?**
|
||||
_`i()` has 10 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **What connects `source`, `document_size`, `pages` to the rest of the system?**
|
||||
_320 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
_653 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
@@ -40,23 +40,23 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/app.js": {
|
||||
"mtime": 1791823225.0391843,
|
||||
"ast_hash": "4787d84f22ac0257204e8ddbdd421140",
|
||||
"mtime": 1792148228.1725585,
|
||||
"ast_hash": "606796c7bf51f17bdadf83ee9bc76b30",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/head-pose.js": {
|
||||
"mtime": 1791819697.652488,
|
||||
"ast_hash": "303298be5be4af0e25b609a83795a810",
|
||||
"mtime": 1792070106.4565594,
|
||||
"ast_hash": "8d8256c7ee28c725f4e5b4a429455bac",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/skin-score.js": {
|
||||
"mtime": 1791735501.357809,
|
||||
"ast_hash": "783b6c9abbe2bf1832e8eafe5aead7e3",
|
||||
"mtime": 1792148550.126307,
|
||||
"ast_hash": "5fc0f09032bf3ece46312d5b060c91ee",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/server.js": {
|
||||
"mtime": 1791800754.2112646,
|
||||
"ast_hash": "9983e6c978b3357e31bbfce59202dcde",
|
||||
"mtime": 1792148550.122564,
|
||||
"ast_hash": "6e8f1c90c7673d52691cfc11d7aac613",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/build_fixtures.py": {
|
||||
@@ -80,8 +80,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/regression.js": {
|
||||
"mtime": 1791735501.3654072,
|
||||
"ast_hash": "4a712e295c115b6051726cd028e67be2",
|
||||
"mtime": 1792066212.160884,
|
||||
"ast_hash": "4fd58d98a1cc12a7a19745092f1e15ba",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/vendor/faceapi/face-api.js": {
|
||||
@@ -135,13 +135,13 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/README.md": {
|
||||
"mtime": 1791811100.15898,
|
||||
"ast_hash": "4b8b63aa8184ead90d868aff34e9a6e3",
|
||||
"mtime": 1792046412.3080864,
|
||||
"ast_hash": "5e60028c676088d6e62d373ff94f20c7",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/index.html": {
|
||||
"mtime": 1791821148.115547,
|
||||
"ast_hash": "770a726605078c6c0eebd95304e8e954",
|
||||
"mtime": 1792140157.1269789,
|
||||
"ast_hash": "a6c0d49e3c0f23f436446aa4f0a4a06c",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/vendor/faceapi/README.md": {
|
||||
@@ -770,8 +770,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/e2e-browser.mjs": {
|
||||
"mtime": 1791773638.010959,
|
||||
"ast_hash": "9327216df7c2e80f2615cb7f800b7f1a",
|
||||
"mtime": 1791875688.6889038,
|
||||
"ast_hash": "ca6e17bc320f31adb34e5b9e0ed28f81",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/assets/svg/prod-badge.svg": {
|
||||
@@ -815,8 +815,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/admin.js": {
|
||||
"mtime": 1791806985.2188702,
|
||||
"ast_hash": "d9d5781dd607497f2803404be4e23e7a",
|
||||
"mtime": 1792148279.018301,
|
||||
"ast_hash": "a20c65d76c3f65e89d9ecb685d700cae",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/region-data.js": {
|
||||
@@ -830,8 +830,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/vendor/mobile-select/mobile-select.iife.min.js": {
|
||||
"mtime": 1791559485.790093,
|
||||
"ast_hash": "52578c3b95933f96493d3b879d353421",
|
||||
"mtime": 1791878239.1849728,
|
||||
"ast_hash": "9aeccc95862e05a14439c9cc906af5e5",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"tools/export_psd_assets.py": {
|
||||
@@ -860,13 +860,13 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/admin.html": {
|
||||
"mtime": 1791735369.9312572,
|
||||
"ast_hash": "ddfe588d49452e3973e21b080e61dbd7",
|
||||
"mtime": 1792136351.592967,
|
||||
"ast_hash": "8b6c17ed20217424aafa2f9383a5195e",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/vendor/mobile-select/README.md": {
|
||||
"mtime": 1791559485.790093,
|
||||
"ast_hash": "fe3287899b96f598c60f9bc030a1368e",
|
||||
"mtime": 1791878239.2009728,
|
||||
"ast_hash": "f8b834ff5056a3402914e3c99d4807ff",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"design/ui-20261009/assets-overview.jpg": {
|
||||
@@ -1140,8 +1140,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/assets/img/ui-20261009/icon-switch-camera.png": {
|
||||
"mtime": 1791642110.915571,
|
||||
"ast_hash": "6197c8000c21156ffecdf77f9c5ed5ba",
|
||||
"mtime": 1791884685.3970492,
|
||||
"ast_hash": "a2805c01722e185dfca450a1eed8c70d",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/assets/img/ui-20261009/icon-wechat.png": {
|
||||
@@ -1230,8 +1230,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/account-client.js": {
|
||||
"mtime": 1791774062.6429472,
|
||||
"ast_hash": "3197b952f29f9e45176d4300082adaae",
|
||||
"mtime": 1791878732.5729809,
|
||||
"ast_hash": "6e1c148f77744345313e2a7ecf1ea756",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/camera-geometry.js": {
|
||||
@@ -1240,33 +1240,33 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/report-copy.js": {
|
||||
"mtime": 1791730702.7819743,
|
||||
"ast_hash": "eedcef7cdd1a3f00fe7cb5dabcafb95c",
|
||||
"mtime": 1792119867.9142396,
|
||||
"ast_hash": "4d8562a761d5e80b83627df39e3d2d6e",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/report-ui.js": {
|
||||
"mtime": 1791814981.3709843,
|
||||
"ast_hash": "aac3430a5e0c0a386c10e32b7060f2f7",
|
||||
"mtime": 1792148470.2383204,
|
||||
"ast_hash": "dc51361171b0126b2f21da3fbe3e6b8c",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/lib/accounts.js": {
|
||||
"mtime": 1791806985.2188702,
|
||||
"ast_hash": "a8116f973f1e01dcbfbe2e1e26c7c826",
|
||||
"mtime": 1792148228.1725585,
|
||||
"ast_hash": "ad0f3c61ada00a0d6b0dc3ed3d2bf43f",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/accounts.js": {
|
||||
"mtime": 1791770576.0104883,
|
||||
"ast_hash": "d927f5d1486b56505ba4da49ec6a769c",
|
||||
"mtime": 1791873124.7889152,
|
||||
"ast_hash": "85fe766a7c305ffee3a8c904e038d8a6",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/report-persistence.mjs": {
|
||||
"mtime": 1791793231.819183,
|
||||
"ast_hash": "24636f96fd5cd576cde11ef9e911f3ff",
|
||||
"mtime": 1792148394.0279312,
|
||||
"ast_hash": "765ec67ba0ad43b480d57b3fc847fa6e",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/ui-20261009.mjs": {
|
||||
"mtime": 1791800811.939266,
|
||||
"ast_hash": "778edb5314c2d35ae75c05344b96f532",
|
||||
"mtime": 1791875688.6929038,
|
||||
"ast_hash": "22779f48c489e35943f89eae9a81c17b",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"tools/build_report_copy.py": {
|
||||
@@ -1300,8 +1300,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"reference/AI\u6d4b\u80a4\u62a5\u544a\u9875\u8bdd\u672f\u4fee\u6539.md": {
|
||||
"mtime": 1791730674.7447748,
|
||||
"ast_hash": "7e6f8ef4ae4a1c010f3344ccdc088cf6",
|
||||
"mtime": 1792119867.8840585,
|
||||
"ast_hash": "8042fb30daafa9942cf46e27a19a709d",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"design/ui-20261009/audit-current/01-splash.png": {
|
||||
@@ -1530,8 +1530,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/wechat-browser.mjs": {
|
||||
"mtime": 1791800811.939266,
|
||||
"ast_hash": "616790aae9cf9b41d30ee8b8da979cb0",
|
||||
"mtime": 1791879046.0249834,
|
||||
"ast_hash": "af64c3a7fcd5c9338f9b86a584f987b6",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/2026-10-11\u63d0\u4ea4\u524dReview.md": {
|
||||
@@ -1540,13 +1540,13 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/analyze-timeout.js": {
|
||||
"mtime": 1791800811.939266,
|
||||
"ast_hash": "07b7c2248ae3fd4112046828ce522318",
|
||||
"mtime": 1792137936.2202272,
|
||||
"ast_hash": "e87c7f42ce92ee6464af3463aead3ada",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/production-loop.mjs": {
|
||||
"mtime": 1791773638.010959,
|
||||
"ast_hash": "0d0d46ec3ecfce9e767e7f31050befe2",
|
||||
"mtime": 1791875833.91293,
|
||||
"ast_hash": "60cd5e73b5600b731b0e01b0e12e12f4",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u7ebf\u4e0a\u5fae\u4fe1\u767b\u5f55\u90e8\u7f72\u8bb0\u5f55-2026-10-11.md": {
|
||||
@@ -1555,8 +1555,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/browser-failures.mjs": {
|
||||
"mtime": 1791773638.010959,
|
||||
"ast_hash": "8cb2774864d3d59bd5a418ad61819129",
|
||||
"mtime": 1791875688.6929038,
|
||||
"ast_hash": "00147b9f66e099be169cb84bf6331e66",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u7ebf\u4e0a\u95ed\u73af\u6d4b\u8bd5-2026-10-11.md": {
|
||||
@@ -1565,13 +1565,13 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/recommendation.js": {
|
||||
"mtime": 1791735559.0619977,
|
||||
"ast_hash": "589f7046d94485dfc1df20a6129640e0",
|
||||
"mtime": 1792148550.126307,
|
||||
"ast_hash": "fb89d42b850cbbf54cd6cb189f48b65c",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u4ea7\u54c1\u63a8\u8350\u89c4\u5219-\u80a4\u9f84\u7248.md": {
|
||||
"mtime": 1791735707.6717124,
|
||||
"ast_hash": "69c1278c6c8eea5866982940e20e421c",
|
||||
"mtime": 1792148470.2383204,
|
||||
"ast_hash": "7b448209ee4f3be3f4a65fc9d86eabff",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u5fae\u4fe1\u767b\u5f55\u8054\u8c03-2026-10-12.md": {
|
||||
@@ -1585,8 +1585,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/login-performance.mjs": {
|
||||
"mtime": 1791800811.939266,
|
||||
"ast_hash": "ffe9df947564cdef24e8f842cbc577f1",
|
||||
"mtime": 1791875688.6889038,
|
||||
"ast_hash": "1f0d6e798cf1df853eb1309a171b2122",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/assets/img/ui-20261009/bg-history-h5.webp": {
|
||||
@@ -1620,8 +1620,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/customer-feedback.js": {
|
||||
"mtime": 1791806985.2188702,
|
||||
"ast_hash": "b0897ac30075bc2a350af40ef6660733",
|
||||
"mtime": 1792065226.843781,
|
||||
"ast_hash": "80941e33bb834ed1b120ac83a90e8dd8",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/customer-feedback.mjs": {
|
||||
@@ -1675,8 +1675,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/customer-feedback-second.js": {
|
||||
"mtime": 1791819755.8747528,
|
||||
"ast_hash": "03ea3689f3c86e78a1f5bd46966f4145",
|
||||
"mtime": 1792137543.797617,
|
||||
"ast_hash": "b0643aea1a92733a259d53b7342aad4c",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/customer-feedback-second.mjs": {
|
||||
@@ -1695,8 +1695,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/customer-feedback-third.mjs": {
|
||||
"mtime": 1791815038.0630124,
|
||||
"ast_hash": "32560625a79f7c8a9d7b74de7337729c",
|
||||
"mtime": 1791997644.8472245,
|
||||
"ast_hash": "58089e992930098527cf51a3f45ace98",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"tools/export_third_feedback_assets.py": {
|
||||
@@ -1805,28 +1805,28 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/capture-quality.js": {
|
||||
"mtime": 1791809137.1269257,
|
||||
"ast_hash": "ec66579ee0cde06a3fa045d612da69c1",
|
||||
"mtime": 1792137441.2704568,
|
||||
"ast_hash": "8b4570b827eed27570a4bff3c0e7e25b",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/lib/capture-diagnostics.js": {
|
||||
"mtime": 1791800706.5472584,
|
||||
"ast_hash": "7fec6b17bde84b1df8ef30d026c0af66",
|
||||
"mtime": 1792137441.2704568,
|
||||
"ast_hash": "2f2433117ddaa0d6367954456a99f9bb",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/capture-quality-browser.mjs": {
|
||||
"mtime": 1791819803.979456,
|
||||
"ast_hash": "a4164c6ce0209e18a0b22a93745c90cf",
|
||||
"mtime": 1792137627.2183216,
|
||||
"ast_hash": "a7aaaa81d7b9a7c56b02751f9695bb1f",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/customer-feedback-fourth.js": {
|
||||
"mtime": 1791819755.8706622,
|
||||
"ast_hash": "92a53685400808b24315f85ed851566d",
|
||||
"mtime": 1792137441.2704568,
|
||||
"ast_hash": "6e162ac194ebca6611ed2c8692ab953d",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/customer-feedback-fourth.mjs": {
|
||||
"mtime": 1791809137.130926,
|
||||
"ast_hash": "a07ebd722efaf2d8b6e382f566a5f649",
|
||||
"mtime": 1792064944.0438669,
|
||||
"ast_hash": "79406f61ffdece2d34ab19d139aefaf4",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"tools/export_fourth_feedback_assets.py": {
|
||||
@@ -1890,23 +1890,23 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/profile-store.js": {
|
||||
"mtime": 1791810317.830946,
|
||||
"ast_hash": "5723356fa753e9a66fdee448e617390e",
|
||||
"mtime": 1791872891.6128974,
|
||||
"ast_hash": "67650e3352c06e2a9ab8965d8da3d421",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/profile-interaction.mjs": {
|
||||
"mtime": 1791820047.336818,
|
||||
"ast_hash": "2efa96cee5dcf3671644b1b82f890ee3",
|
||||
"mtime": 1791873124.7889152,
|
||||
"ast_hash": "4f1fcae2e04530f3a85b33e5ee12453d",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/profile-store.js": {
|
||||
"mtime": 1791810317.8349462,
|
||||
"ast_hash": "66a0e7d6ddc5eee9cd69c097257746d1",
|
||||
"mtime": 1791873040.5609126,
|
||||
"ast_hash": "412a4fc48325d5ad886994c0e22adad0",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u57fa\u7840\u4fe1\u606f\u8bb0\u5fc6\u4e0e\u80a4\u8d28\u62d6\u52a8-2026-10-12.md": {
|
||||
"mtime": 1791810600.0989332,
|
||||
"ast_hash": "cec2f2e829751e8b5d2e5e40c348f5e4",
|
||||
"mtime": 1791873518.1209404,
|
||||
"ast_hash": "e6e11bfd6642c38c51d11ebe85499b98",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u62a5\u544a\u7b49\u5f85\u9875\u5b9e\u62cd\u80cc\u666f-2026-10-12.md": {
|
||||
@@ -1930,8 +1930,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/scan-placeholder.mjs": {
|
||||
"mtime": 1791815038.0590124,
|
||||
"ast_hash": "bb8566ee5f3ad2a6bf95aafafb7145ba",
|
||||
"mtime": 1791885786.4050858,
|
||||
"ast_hash": "98f0327cec6167ceb0a435240f763e4c",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"tools/optimize_delivery_images.py": {
|
||||
@@ -1973,5 +1973,400 @@
|
||||
"mtime": 1791821522.1000154,
|
||||
"ast_hash": "517105aaf52427102a325e0450142446",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/guest-privacy.mjs": {
|
||||
"mtime": 1792063312.584469,
|
||||
"ast_hash": "9d77ff05f907aa39b79e114e7432959d",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/wechat-fixture.cjs": {
|
||||
"mtime": 1791873124.7849152,
|
||||
"ast_hash": "bf1d2d3b6c76743b42c9fcfa3f18444d",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u6e38\u5ba2\u4e0d\u7559\u5b58\u4e0e\u5f39\u7a97\u62a4\u7406\u95f4\u8ddd\u8c03\u6574-2026-10-13.md": {
|
||||
"mtime": 1791876185.0449448,
|
||||
"ast_hash": "905d6f5ae894843f99e66a15ad3d8b33",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/harmony-picker.mjs": {
|
||||
"mtime": 1791878418.4889727,
|
||||
"ast_hash": "299b3a47e9212d7e758fc89cfffc39f8",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/raw-skin-age.js": {
|
||||
"mtime": 1791992633.351766,
|
||||
"ast_hash": "df1c2fc506e3976e14365b7c8c01958b",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u539f\u59cb\u808c\u9f84\u663e\u793a\u4e0e\u9e3f\u8499\u6eda\u8f6e\u517c\u5bb9-2026-10-13.md": {
|
||||
"mtime": 1791879146.5129797,
|
||||
"ast_hash": "99e4254133761034e9057465d4edeb6f",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u5fae\u4fe1\u9000\u51fa\u767b\u5f55-2026-10-13.md": {
|
||||
"mtime": 1791879146.5129797,
|
||||
"ast_hash": "e0fe3daeaa1eeeacf18ff18d8eee69e4",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/camera-flip-browser.mjs": {
|
||||
"mtime": 1791881142.0210207,
|
||||
"ast_hash": "b64cb9f61c398ce1aa12f59bc27cb481",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u76f8\u673a\u7ffb\u8f6c\u6027\u80fd\u4f18\u5316-2026-10-13.md": {
|
||||
"mtime": 1791885450.2930646,
|
||||
"ast_hash": "5d45e9c3fdde5ef718721caf5dbc88a7",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/splash-brand-browser.mjs": {
|
||||
"mtime": 1791901607.7048087,
|
||||
"ast_hash": "1ae44d528f227ea038a27d13fca81c75",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u62cd\u6444\u89d2\u5ea6\u4e0d\u5408\u683c\u9707\u52a8\u63d0\u9192-2026-10-13.md": {
|
||||
"mtime": 1791885450.2930646,
|
||||
"ast_hash": "fcbbc5db7241f97579efaf14eaa5135e",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/weighted-total.js": {
|
||||
"mtime": 1792066095.1604717,
|
||||
"ast_hash": "3e07e35851719cd8c61571ecb06852a6",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u7efc\u5408\u5f97\u5206\u4e09\u7ef4\u5ea6\u52a0\u6743-2026-10-14.md": {
|
||||
"mtime": 1791959087.1679206,
|
||||
"ast_hash": "f1d733b0970ac8756c01b11cf0e3fb1f",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/beijing-time.js": {
|
||||
"mtime": 1791961186.2461162,
|
||||
"ast_hash": "57be1d3fac9f980da30b144dce9fc467",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/admin-beijing-browser.mjs": {
|
||||
"mtime": 1791961274.587497,
|
||||
"ast_hash": "73d57935a0f54160ed123d9bc931e1fe",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/beijing-time.js": {
|
||||
"mtime": 1791961274.587497,
|
||||
"ast_hash": "6a99016695257e9200a3770aeb5395e2",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u540e\u53f0\u5317\u4eac\u65f6\u95f4-2026-10-14.md": {
|
||||
"mtime": 1791961354.4159153,
|
||||
"ast_hash": "947edc7a913eb62e48c935b8c255a293",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/wrinkle-case-browser.mjs": {
|
||||
"mtime": 1791993447.323378,
|
||||
"ast_hash": "f345329b4f7ec49f1b160abc846c1566",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/wrinkle-overall.js": {
|
||||
"mtime": 1792066095.1604717,
|
||||
"ast_hash": "213fbac73cb20131edb507c46b9766e9",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u76b1\u7eb9\u6539\u7528\u63a5\u53e3\u6574\u4f53\u8bc4\u5206-2026-10-14.md": {
|
||||
"mtime": 1791980169.9924085,
|
||||
"ast_hash": "d7e46261e97d5c1cb26266e62906740e",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/acne-three-view.js": {
|
||||
"mtime": 1792047898.477616,
|
||||
"ast_hash": "4481d834dfa5a74b0bea44285846cfcc",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u75d8\u75d8\u4e09\u89c6\u56fe\u5206\u7c7b\u6700\u5927\u6570\u91cf\u8ba1\u5206-2026-10-14.md": {
|
||||
"mtime": 1791987357.9701402,
|
||||
"ast_hash": "b185279405eba19190641896d6bc20d8",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/tone-dark-circle.js": {
|
||||
"mtime": 1792046293.4017394,
|
||||
"ast_hash": "2c783245bf43ef7173210418a76de600",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/tone-overlay-browser.mjs": {
|
||||
"mtime": 1791993770.7910676,
|
||||
"ast_hash": "6a75358301b737e260a5fd90e1184ced",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u80a4\u8272\u9ed1\u773c\u570820\u4e0e\u8272\u7d20\u56fe30-2026-10-14.md": {
|
||||
"mtime": 1791991792.9998333,
|
||||
"ast_hash": "43555335e06c1265799452de60f7ad8b",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/countdown-capture-browser.mjs": {
|
||||
"mtime": 1792137441.2704568,
|
||||
"ast_hash": "7ed7c448052132231301648fe78ca3b1",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/pigmentation-case-browser.mjs": {
|
||||
"mtime": 1791993770.7950265,
|
||||
"ast_hash": "07be5d3aa98e1efbd912d22c5909f2e2",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u6062\u590d321\u4e0e\u8272\u6591\u6848\u4f8b\u590d\u6d4b-2026-10-14.md": {
|
||||
"mtime": 1791992536.840095,
|
||||
"ast_hash": "ea1c0026e386003822802018b7fa9403",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u5c55\u793a\u808c\u9f84\u9650\u5236\u6b63\u8d1f5\u5c81-2026-10-14.md": {
|
||||
"mtime": 1791992669.0724173,
|
||||
"ast_hash": "5f8dee6cadcf4e782ce904c1ca1a7d31",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/color-copy.js": {
|
||||
"mtime": 1792052385.2846854,
|
||||
"ast_hash": "51d4c544f23d7c9235689f1f863befba",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u80a4\u8272\u6587\u6848\u4e0e\u5b9e\u9645\u9ed1\u773c\u5708\u7b49\u7ea7-2026-10-14.md": {
|
||||
"mtime": 1791993479.4296343,
|
||||
"ast_hash": "d883b001f777509857a96f02b872a6de",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/report-bottom-figma.mjs": {
|
||||
"mtime": 1792065167.0812862,
|
||||
"ast_hash": "7f998e93116d4e9e5cee58f5f8c7f357",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u62a5\u544a\u5e95\u90e8Figma\u8fd8\u539f-2026-10-15.md": {
|
||||
"mtime": 1791999287.71825,
|
||||
"ast_hash": "b72dae0ed84a076c0d8f99dcebe9bbeb",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/assets/img/ui-20261015/product-25.png": {
|
||||
"mtime": 1791997501.3216276,
|
||||
"ast_hash": "0a1542dbb3c5feb3267fafb1df3123f7",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/assets/img/ui-20261015/product-35.png": {
|
||||
"mtime": 1791997504.752126,
|
||||
"ast_hash": "ff34af3a983eefe9ec93802a525b4ed5",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/assets/img/ui-20261015/product-50.png": {
|
||||
"mtime": 1791997508.2898638,
|
||||
"ast_hash": "9e0d71ba4397d9dbbec27b1ad48fbe9a",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/assets/img/ui-20261015/product-badge.png": {
|
||||
"mtime": 1791997415.2784162,
|
||||
"ast_hash": "4ec7b754fe30aac6248e7f8f990ac078",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/contour-preview.html": {
|
||||
"mtime": 1792045886.5890932,
|
||||
"ast_hash": "a98beeb64d55d5b41969f2312afbe63e",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/assets/mock/contour-demo/front.jpg": {
|
||||
"mtime": 1792033394.649536,
|
||||
"ast_hash": "3b3ba2bbe11175c76f25ea63730106bd",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/assets/mock/contour-demo/left.jpg": {
|
||||
"mtime": 1792033394.6534245,
|
||||
"ast_hash": "af8c5642cfa8b4a4af1f48e4bb02f602",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/assets/mock/contour-demo/right.jpg": {
|
||||
"mtime": 1792033394.6534245,
|
||||
"ast_hash": "65e145520cf438df78b985ddddebe6fd",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/private-case-preview.mjs": {
|
||||
"mtime": 1792058682.390061,
|
||||
"ast_hash": "0085c0f9d7ed9105a918cc0253d6d238",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/water-copy.js": {
|
||||
"mtime": 1792064498.2456841,
|
||||
"ast_hash": "7e235bde0f51e74b4c911ecb157ed5ed",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u6c34\u5206\u6587\u6848\u72ec\u7acb\u5206\u6863-2026-10-15.md": {
|
||||
"mtime": 1792043836.616439,
|
||||
"ast_hash": "66bc24a457dced194579fc9b926f0a26",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/care-selection.js": {
|
||||
"mtime": 1792047749.1986089,
|
||||
"ast_hash": "82fe6a6b3a04f9520648b7d6c6424f3d",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u4e03\u9879\u62a4\u7406\u9009\u4e09\u4e0e\u5173\u6ce8\u4f18\u5148-2026-10-15.md": {
|
||||
"mtime": 1792044443.0636487,
|
||||
"ast_hash": "baf56556b9963af9bc77943fcca94989",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/vita-independent.js": {
|
||||
"mtime": 1792064570.7705622,
|
||||
"ast_hash": "72885089afbd4351c14fb47d97c650ae",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u900f\u4eae\u611f\u6539\u4e3a\u4e09\u9879\u72ec\u7acb\u6700\u4f4e\u5206-2026-10-15.md": {
|
||||
"mtime": 1792046412.3041933,
|
||||
"ast_hash": "48d0853b95e0b36ce79e3f2e3519826f",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/display-grade.js": {
|
||||
"mtime": 1792148470.2383204,
|
||||
"ast_hash": "ea3627208bb0545b926085a7daacc668",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u62a5\u544a\u6587\u6848\u6309\u5341\u5206\u5236\u5206\u6863-2026-10-15.md": {
|
||||
"mtime": 1792052437.0090034,
|
||||
"ast_hash": "39e6d98a95ee55d832e601851f03e90c",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/factual-copy.js": {
|
||||
"mtime": 1792052385.3735242,
|
||||
"ast_hash": "04a7bb0d5c58e7919eb9ef0626f47954",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/report-copy-cases.js": {
|
||||
"mtime": 1792052385.2846854,
|
||||
"ast_hash": "840047c867b82ed203dae3493147f613",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u62a5\u544a\u6587\u6848\u5b9e\u6d4b\u63cf\u8ff0\u4e0e\u65e7\u62a4\u7406\u517c\u5bb9\u590d\u6d4b-2026-10-15.md": {
|
||||
"mtime": 1792052437.0051305,
|
||||
"ast_hash": "2b5709c0f527e9cb3187bed2e6a12864",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u7ebf\u4e0a10\u4f8b\u62a5\u544a\u6587\u6848\u5bf9\u7167-2026-10-15.md": {
|
||||
"mtime": 1792048486.081423,
|
||||
"ast_hash": "0f935d92e8059deaadab2d86f5f2f333",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/fixtures/report-copy-before-two-fixes.cjs": {
|
||||
"mtime": 1792052385.3271735,
|
||||
"ast_hash": "ac8f1225149a88902b2b35476e1258f5",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u6587\u6848\u6700\u7ec8\u8303\u56f4-\u4ec5\u76b1\u7eb9\u4f18\u79c0\u4e0e\u75d8\u75d8\u68c0\u51fa-2026-10-15.md": {
|
||||
"mtime": 1792055645.4742913,
|
||||
"ast_hash": "cf11232413c36c4a299d092d37bb1804",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/contour-sides.mjs": {
|
||||
"mtime": 1792066380.5569,
|
||||
"ast_hash": "daccbed8eecef1089f061af539ae63c8",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/lib/wrinkle-views.js": {
|
||||
"mtime": 1792058493.3015623,
|
||||
"ast_hash": "5066fa726b330eb7e9658c5b741ab0d8",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/wrinkle-three-views.mjs": {
|
||||
"mtime": 1792066380.5569,
|
||||
"ast_hash": "39946726ecf37cb72652584f1d71811d",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/wrinkle-views.js": {
|
||||
"mtime": 1792058682.390061,
|
||||
"ast_hash": "0a35c79c3613c5198d977cf22d99fdbc",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u76b1\u7eb9\u4e09\u89c6\u56fe\u6807\u6ce8\u8bf4\u660e-2026-10-15.md": {
|
||||
"mtime": 1792068382.5214024,
|
||||
"ast_hash": "ca11237b7151b01ca87bc10923a2527b",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/dimension-cap.js": {
|
||||
"mtime": 1792066181.2678514,
|
||||
"ast_hash": "a00ea81951025f34b39ca9f3b4248251",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/contour-eye-copy.js": {
|
||||
"mtime": 1792119867.8840585,
|
||||
"ast_hash": "085d40c6e51ae1ef8d65f41cd414ad1f",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/zoom-fill.js": {
|
||||
"mtime": 1792068382.5214024,
|
||||
"ast_hash": "eab466eedfa8a5f70ef71ca995e1be04",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/lib/capture-funnel.js": {
|
||||
"mtime": 1792136431.3330622,
|
||||
"ast_hash": "11c141449642273af76eb2827dcad349",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/capture-funnel-browser.mjs": {
|
||||
"mtime": 1792136490.2513402,
|
||||
"ast_hash": "e58be6c0acb89ed299db7abf526e9451",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/capture-funnel.js": {
|
||||
"mtime": 1792136431.3368118,
|
||||
"ast_hash": "8d14b4df58066e0aff67f24873925d00",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u62cd\u7167\u62a5\u544a\u8f6c\u5316\u7edf\u8ba1-2026-10-16.md": {
|
||||
"mtime": 1792136431.3368118,
|
||||
"ast_hash": "a6263a5842743851ebb245afd63d587e",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u62cd\u6444\u5149\u7ebf\u4ec5\u63d0\u793a-2026-10-16.md": {
|
||||
"mtime": 1792137024.177549,
|
||||
"ast_hash": "16047b931716f23d0477bf0ca9435e09",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/capture-no-recheck.js": {
|
||||
"mtime": 1792137543.797617,
|
||||
"ast_hash": "7c496103ec30686842f1d48a0155a0ba",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u53d6\u6d88\u62cd\u540e\u6821\u9a8c-2026-10-16.md": {
|
||||
"mtime": 1792137543.797617,
|
||||
"ast_hash": "736cc241f069b7b95b4735c3fb099c3b",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/scan-wait-timeout.js": {
|
||||
"mtime": 1792137936.2202272,
|
||||
"ast_hash": "96d24336d28d6a6e4491062d3c7d2501",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u626b\u63cf\u7b49\u5f8530\u79d2\u515c\u5e95-2026-10-16.md": {
|
||||
"mtime": 1792138004.0861843,
|
||||
"ast_hash": "578ea3c34745606239eda2476662d546",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/lib/wrinkle-jobs.js": {
|
||||
"mtime": 1792139119.6377332,
|
||||
"ast_hash": "635306bfa84bf6d8181f18742e260a44",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/wrinkle-async-browser.mjs": {
|
||||
"mtime": 1792140157.130621,
|
||||
"ast_hash": "8df41ff28959b6fb450f3629d6b1f826",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/wrinkle-jobs.js": {
|
||||
"mtime": 1792139246.8972948,
|
||||
"ast_hash": "6bc66a7cd09973e8bc99ed1d1b56414d",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u4fa7\u8138\u76b1\u7eb9\u6d6e\u5c42\u5f02\u6b65\u52a0\u8f7d-2026-10-16.md": {
|
||||
"mtime": 1792140269.8649895,
|
||||
"ast_hash": "8b08cc58ff8e2b9341ea7afa2ce71e59",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/display-age-recommendation.js": {
|
||||
"mtime": 1792148364.4246356,
|
||||
"ast_hash": "73c329016fafb4073c6c6418f9ac071a",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u4ea7\u54c1\u63a8\u8350\u6539\u7528\u5c55\u793a\u808c\u9f84-2026-10-16.md": {
|
||||
"mtime": 1792148470.2383204,
|
||||
"ast_hash": "505317dc4f33f2a9ce53d31e2de3851f",
|
||||
"semantic_hash": ""
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ node server.js # 默认 8080,可用 PORT=xxx 修改
|
||||
- **取景蒙版**:内联 SVG(`#cam-mask`)按步骤切换轮廓——正脸步为正脸剪影(含耳部),侧脸步为对应方向的侧脸剪影;**轮廓路径由 Figma 拍照示意图(`design/assets/images/*-拍照示意.png` 的 alpha 镂空区)逐像素描摹生成**,坐标为 Figma 375×812 整帧,viewBox 只取 y 86–812;框外 85% 黑,轮廓线 2.5px 半透明白。检测运行时轮廓内有自上而下的扫描带(`#cam-clip` 由 app.js 复制轮廓生成);未检测到人脸时轮廓呼吸动画;条件合格时轮廓变绿。
|
||||
- **预览铺满**:`#cam-video` 保持现有 `object-fit:cover` 铺满与镜像策略,不改取景视觉。
|
||||
- **动态提示**:根据真机难拍反馈适度放宽(参数版本 `2026-10-12-fourth-v2`),单人、检测置信度≥0.4、人脸框高占原始画面 30%~68%、水平中心偏离≤16% 屏宽、完整入镜且距原始帧边缘≥1%。正脸 |yaw|≤15°/|pitch|≤18°,侧脸转头幅度 30°~70°/|pitch|≤20°,两者 |roll|≤15°;脸部平均亮度 65~235。参数集中在 `js/capture-quality.js` 与 `js/head-pose.js`,均为经验值,不等价于服务端质量分。
|
||||
- **合格即拍**:连续稳定至少 450ms 且至少 2 个合格检测帧;容许 yaw/pitch 漂移≤8°、中心移动≤原帧 5.5%、框高占比变化≤6.5 个百分点。漏检、超限漂移或帧间隔超过 2 秒均重置稳定累计;白屏抓拍后在本地复验实际 JPEG,合格才推进。**无倒计时、无手动快门**(已有降级模式除外)。
|
||||
- **合格后倒计时拍摄**:连续稳定至少 450ms 且至少 2 个合格检测帧;容许 yaw/pitch 漂移≤8°、中心移动≤原帧 5.5%、框高占比变化≤6.5 个百分点。漏检、超限漂移或帧间隔超过 2 秒均重置稳定累计;每张照片播放原有 `countdown-321.mp3`,等音频完整结束后再白屏抓拍,本地复验实际 JPEG,合格才推进。倒计时期间锁定快门及翻转,退出或隐藏页面取消未完成拍摄。网页内自动/降级手动抓拍均适用;系统相机降级的原生快门由操作系统控制。音频不支持、未解锁或加载失败时沿用无声抓拍,避免阻塞流程。
|
||||
- **两张之间停顿**:正脸、右脸拍完后中央弹出停顿卡约 2s(「正脸拍摄完成 / 下一张:右脸出镜,请向左转头」),停顿期间不检测,结束后再切到下一步并播下一步引导,让用户对下一张有心理预期。
|
||||
- **实时检测**:face-api 从本地 `vendor/faceapi/` 加载(`tinyFaceDetector` + `faceLandmark68Net`),进入拍照页开始加载(状态提示「检测引擎加载中…」)。检测循环 ~150ms 一帧,特征点取自原始帧(预览 CSS 镜像不影响),个别前置帧已镜像的机型由侧脸自校准翻转 `sideYawFlip`;连续漏检 ≥3 帧才报「未检测到人脸」。
|
||||
- **镜像策略**:前置镜头预览镜像(照镜子习惯);后置预览不镜像,侧脸轮廓随之翻转(`.cam-mask.flip`)。**抓帧一律不镜像**,三张照片都是摄像头原始视角。
|
||||
@@ -79,6 +79,8 @@ node server.js # 默认 8080,可用 PORT=xxx 修改
|
||||
|
||||
## 档案页与报告页
|
||||
|
||||
2026-10-13 当前留存规则:仅微信登录记住本机基础信息、在后端保留最近 5 份完整报告并支持跨设备查看。游客使用仅内存的临时会话,不持久化身份/基础信息/历史报告,也不新增后台检测记录、照片或原始结果;本次报告仍可正常生成并查看,刷新后基础信息不回填。旧服务器数据不自动删除。详见 [游客不留存与样式调整](../docs/游客不留存与弹窗护理间距调整-2026-10-13.md)。下方早期的 localStorage 报告历史说明已被此规则替代。
|
||||
|
||||
- **档案页**:出生年月 / 性别 / 所在地区三个滚轮选择器同一风格(`vendor/mobile-select`,MIT),出生年月自动换算年龄;地区为省–市两级联动,数据为 `js/region-data.js`(由 china-division 2.7.0 `pc.json` 生成,另含港澳台与「海外」)。当前账号在本机浏览器记住已确认的基础信息和日常肤质,再次进入自动回填,滚轮也定位到原值;年龄按出生年月重新计算。肤质支持跟手拖动、松手吸附、轻扫切换及边界回弹,纵向手势仍滚动页面。详见 [基础信息记忆与肤质拖动](../docs/基础信息记忆与肤质拖动-2026-10-12.md)。
|
||||
- **报告页**:无返回按钮与页头标题;总分环形图 + 肌肤年龄与「比实际年龄年轻/大 N 岁」;三维度分数 + 进度条(最低分标红并挂「优先关注」);维度详情为原布局——一张卡内紧塑力/活力感/稳定力三个 Tab,每个 Tab 两条「竖排标题 + 左图右文」;点检测图全屏放大,再点或按返回键回到报告。
|
||||
|
||||
@@ -87,12 +89,14 @@ node server.js # 默认 8080,可用 PORT=xxx 修改
|
||||
1. 三视图送检:POST `/api/skin-analyze`,请求 `{ front_image, left_image, right_image, profile? }`(base64 可带 dataURL 前缀;正脸=photos[0],左=photos[2],右=photos[1];`profile` 为可选档案 `{real_age,gender,region,daily_skin_type}`,后台记录用)。
|
||||
2. 服务端转发 Face++ **专业版 `skinanalyze_pro`**(三视图必须走 `image_file` 文件形式),请求 `return_side_results=jawline_info` 取左右下颌线角度,`return_maps`/`return_marks` 取叠加图与标注;响应 `{ ok:true, result, face_rectangle, side:{ left, right } }`,失败 `{ ok:false, code, message }`(message 已是用户可读文案)。`result.face_maps` 为对象,含报告页所需图层的完整 data URL(`red_area`/`brown_area`/`texture_enhanced_lines`/`texture_enhanced_pores`/`texture_enhanced_blackheads`,由 server.js 的 `REPORT_MAPS` 常量控制);`marks` 坐标(`acne`/`brown_spot`/`mole`/`closed_comedones`/`acne_pustule`/`acne_nodule`/`wrinkle_mark`/`melanin_mark`/`dark_circle_mark`)随 `result` 原样透传,与上传图片同一像素空间。
|
||||
3. 评分由 `js/skin-score.js` 引擎完成,公式/锁值/话术/产品推荐以 `docs/三维度评分与话术生成需求文档_V2.3.docx` 为最高契约(V2.3 口径,含 §8.5 护理建议五段):
|
||||
- 6 分类:轮廓 = min(眼袋映射分, 下颌线角度分);皱纹(12 分区);光滑(毛孔/黑头/纹理);肤色水润(色素×水润);敏感(泛红面积取 `red_spot.red_spot_area`/红区分);痘痘(分级映射×丘疹 `acne.count` 计数扣分);分组按实际年龄;
|
||||
- 6 分类:轮廓 = min(眼袋映射分, 下颌线角度分);皱纹直接采用 `score_info.wrinkle_score` 整体分(不反向、不按分区严重度限分;分区仅作说明/可视化,缺失整体分则不回退旧规则);光滑(毛孔/黑头/纹理);肤色水润(色素×水润);敏感(泛红面积取 `red_spot.red_spot_area`/红区分);痘痘(分级映射×丘疹 `acne.count` 计数扣分);
|
||||
- 三维度木桶 min:紧塑力(轮廓+皱纹)、活力感(光滑+肤色水润)、稳定力(敏感+痘痘);
|
||||
- S = 7 + 3×P/100;档位:良好(P≥90)/改善(80–90)/预警(<80);肤龄钳制到实际年龄 ±3 并留痕;三视图版 18 条话术;
|
||||
- 痘痘计数:闭口、丘疹、脓包、结节、痘印分别取正/左/右三视图最大数量(不相加),权重1/2/4/6/1.5、合计扣分封顶50;整体 `acne_score` 仍取正脸。报告文案使用相同计分数量;缺失不当0,后台详情显示三视图及最终取值、扣分。
|
||||
- 肤色项:色素扣斑分80%+`dark_circle_score`黑眼圈评分20%,与水分项各占50%得到肤色水润P;缺项按有效权重归一。肤色页在原照片上以30%强度叠加色素图(缺图时用真实色素坐标),黄褐斑/雀斑显示检出状态,没有对应坐标不画伪造定位框。
|
||||
- S = 7 + 3×P/100;档位:良好(P≥90)/改善(80–90)/预警(<80);生物可视年龄限制在用户实际年龄±5岁:`max(实际年龄−5, min(实际年龄+5, 原始肌龄))`,如实际25、接口40展示30;原始肌龄和推荐规则不变。缺少实际年龄展示原始肌龄,缺少肌龄显示`--`;三视图版 18 条话术;
|
||||
- 精华推荐三重判据(均分档/最差项约束/年龄组档)取最强档,triggers 记录命中判据。
|
||||
4. 前端 `SkinScore.buildViewModel(data.result, data.side, { realAge, skinType })` → viewModel 直渲报告页:
|
||||
- 头部:综合评分(`vm.total`,缺失回退三维度 S 均分×10)、肤龄 `vm.skinAgeDisplay` + 钳制标记 `vm.ageClampNote`;三维度 S(1 位小数)+ 档位配色(良好绿/改善琥珀/预警红);
|
||||
- 头部:综合得分通过 `SkinScore.weightedTotal(vm.dims)` 计算,紧塑力×4+透亮感×3+稳韧力×3,使用维度S分数(两位小数)加权、四舍五入取整数,不使用接口 `total_score`;任一维度缺失显示 `--`。后台列表、详情、CSV采用相同公式,历史报告按已保存维度计算展示,不改写原文件;肤龄读取 `SkinScore.reportSkinAge(vm, result)`,原值缺失显示 `--`;三维度 S(1 位小数)+ 档位配色(良好绿/改善琥珀/预警红);
|
||||
- 三个 Tab 面板各展示所辖两分类:分类名、S + 档位徽章、话术正文(`vm.cats.*.text`);P 值不直接展示,`P=null` 的分类显示"本次未检出有效数据";
|
||||
- 报告底部:降级标注(`vm.notes` 中与侧脸/下颌线/补拍相关的条目,去掉接口质量码后展示)、护理建议五段(`vm.careAdvice`)、精华推荐卡(`vm.essence.name` + 命中判据;推荐正文已在护理建议第 4 段,卡内不重复);
|
||||
- 近期优先关注 = 最差维度(`vm.focus`);6 分类各配检测可视化图:轮廓=正脸实拍、皱纹=正脸压暗+`texture_enhanced_lines` 皱纹增强线(线图层几乎全透明,直挂白底不可读,故与压暗沉脸合成)、光滑=正脸+色斑/痣/痤疮标记叠加、肤色=`brown_area` 色沉图、敏感=`red_area` 红区图、痘痘=正脸+闭口粉刺/痤疮标记叠加(marks 按原像素坐标叠加到正脸实拍)。
|
||||
@@ -108,6 +112,7 @@ node server.js # 默认 8080,可用 PORT=xxx 修改
|
||||
|
||||
- **数据落库**:每次有效调用(正脸照片校验通过起)生成 `record_id`,照片写 `data/photos/<record_id>/`,记录(档案/照片地址/拍摄与上传状态/检测状态与失败原因/评分/文案/精华/耗时/raw 文件名/重试关系)追加到 `data/records.jsonl`。C 端响应结构不变。
|
||||
- **记录列表**:多条件筛选(时间区间/检测状态/失败原因/年龄/产品组别/维度+档位/记录ID),行点击开详情抽屉(照片大图与下载、状态、六分类与三维度评分表、文案全文、护理建议、精华与触发判据、计算备注、接口原始 JSON 折叠查看、手动重试)。
|
||||
- **时间口径**:后台列表/异常/详情显示、纯日期筛选、今日及每日统计、CSV时间统一为北京时间(UTC+8),由 `js/beijing-time.js` 共享处理,不依赖服务器或浏览器时区。原始记录及JSON接口时间仍为UTC ISO;完整ISO筛选保留显式时区对应的时间点。
|
||||
- **失败与异常**:失败原因分布条 + 失败列表(每条可一键重试:用原照片重新检测,生成新记录并挂版本链)。
|
||||
- **统计概览**:今日/累计/成功率/平均耗时卡片,三维度档位分布,产品组别分布(纯 CSS 饼图),近 14 天每日检测量柱状图,按日期区间导出 CSV(UTF-8 BOM,不含 raw_json)。
|
||||
- **演示数据**:`node h5/data/seed.js` 生成约 10 条演示记录(覆盖重写 records.jsonl,照片复制自 `reference/facepp-skinanalyze/examples/images/` 真实三视图)。
|
||||
@@ -129,6 +134,10 @@ node server.js # 默认 8080,可用 PORT=xxx 修改
|
||||
本机浏览器只缓存随机身份凭据,报告保存在服务器;微信需公网 HTTPS、具备网页授权权限的服务号及相应环境变量。
|
||||
详见 [新版实施与验收](../docs/10.09新版H5实施与验收.md) 和 [微信登录接入与公网部署说明](../docs/微信登录接入与公网部署说明.md)。下方旧功能说明以这两份更新文档为准。
|
||||
|
||||
2026-10-15 护理展示更新:`ReportUI.build` 从七项独立小维度分中选最低三项,再将属于当前优先关注大维度的入选项前置;同分按关注优先、文档顺序确定。肤色、水分分析及护理各自独立分档,不再共用肤色水润合成分。新版护理全文以 `reference/AI测肤报告页话术修改.md` 为准;不改变原三维度/综合得分/产品推荐,不批量改写历史presentation。详见 [七项护理选三与关注优先](../docs/七项护理选三与关注优先-2026-10-15.md)。
|
||||
|
||||
随后用户确认取消合成分参与维度计分:透亮感改为独立肤色、水分、平滑度三项P的最低有效值,不再平均肤色和水分。新分数用于总分、优先关注及原有边界推荐判断。后台新记录展示七项独立分数,历史记录不重算;旧tone字段仅为兼容。详见 [新版透亮感公式](../docs/透亮感改为三项独立最低分-2026-10-15.md)。
|
||||
|
||||
## 部署
|
||||
|
||||
需要 Node ≥ 18 运行 `server.js`(负责静态与 API 中转),**不能用纯静态托管**(密钥不能进前端)。
|
||||
|
||||
@@ -17,10 +17,16 @@
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="filters channel-filter">
|
||||
<label>渠道
|
||||
<select id="f-channel"><option value="">全部渠道</option><option value="h5">H5</option><option value="taobao">淘宝小程序</option></select>
|
||||
</label>
|
||||
<span class="hint" id="channel-summary">正在读取渠道数据…</span>
|
||||
</div>
|
||||
<!-- ─── 记录列表(文档 4.1/4.2) ─── -->
|
||||
<section id="tab-records" class="panel on">
|
||||
<div class="filters">
|
||||
<label>创建时间 <input type="date" id="f-from"> ~ <input type="date" id="f-to"></label>
|
||||
<label>创建时间(北京时间) <input type="date" id="f-from"> ~ <input type="date" id="f-to"></label>
|
||||
<label>检测状态
|
||||
<select id="f-status">
|
||||
<option value="">全部</option>
|
||||
@@ -71,8 +77,8 @@
|
||||
<table id="tbl-records">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>照片</th><th>记录ID</th><th>年龄/性别/地区/肤质</th><th>拍摄状态</th><th>上传状态</th>
|
||||
<th>检测状态</th><th>总分</th><th>肌龄(原始 / 展示)</th><th>三维度评分 / 档位</th><th>推荐精华</th><th>耗时</th><th>创建时间</th>
|
||||
<th>照片</th><th>记录ID / 来源</th><th>年龄/性别/地区/肤质</th><th>拍摄状态</th><th>上传状态</th>
|
||||
<th>检测状态</th><th title="紧塑力×4+透亮感×3+稳韧力×3,四舍五入取整数">综合得分</th><th>肌龄(原始 / 展示)</th><th>三维度评分 / 档位</th><th>推荐精华</th><th>耗时</th><th>创建时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
@@ -92,7 +98,7 @@
|
||||
<table id="tbl-fails">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>记录ID</th><th>失败环节</th><th>失败码</th><th>失败信息</th><th>年龄</th><th>拍摄状态</th><th>创建时间</th><th>操作</th>
|
||||
<th>记录ID / 来源</th><th>失败环节</th><th>失败码</th><th>失败信息</th><th>年龄</th><th>拍摄状态</th><th>创建时间</th><th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
@@ -103,6 +109,12 @@
|
||||
<!-- ─── 统计概览(文档 4.5) ─── -->
|
||||
<section id="tab-stats" class="panel">
|
||||
<div class="cards" id="stat-cards"></div>
|
||||
<div class="block"><h3>全部渠道检测概览</h3><div id="channel-stats"></div><p class="hint">此表展示全部渠道对照,上方指标按当前渠道筛选。按检测记录统计,不是跨渠道去重人数;游客临时报告不进入已保存检测记录统计。</p></div>
|
||||
<div class="block">
|
||||
<h3>拍照 → 报告页转化(上线后新流程)</h3>
|
||||
<div id="capture-funnel"></div>
|
||||
<p class="hint">按每次点击开始拍照的流程去重,不是人数;进入成功仅在本次报告渲染并切屏后上报,历史报告浏览与演示不计入。今日按开始拍照的北京时间归属。客户端埋点可能受断网、关闭页面影响,未完成不等于检测失败。</p>
|
||||
</div>
|
||||
<div class="grid2">
|
||||
<div class="block">
|
||||
<h3>三维度档位分布</h3>
|
||||
@@ -114,7 +126,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="block">
|
||||
<h3>近 14 天每日检测量</h3>
|
||||
<h3>近 14 天每日检测量(北京时间)</h3>
|
||||
<div id="daily-chart" class="daily-chart"></div>
|
||||
</div>
|
||||
<div class="block">
|
||||
@@ -123,7 +135,7 @@
|
||||
<label>起始日期 <input type="date" id="exp-from"></label>
|
||||
<label>结束日期 <input type="date" id="exp-to"></label>
|
||||
<button type="button" class="btn primary" id="btn-export">导出 CSV</button>
|
||||
<span class="hint">UTF-8 BOM,Excel 直接打开;不含接口原始 JSON</span>
|
||||
<span class="hint">沿用当前渠道及记录筛选条件;导出日期可另选。北京时间,UTF-8 BOM,不含接口原始 JSON。</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -151,6 +163,7 @@
|
||||
|
||||
<script src="js/report-copy.js"></script>
|
||||
<script src="js/report-ui.js"></script>
|
||||
<script src="js/beijing-time.js"></script>
|
||||
<script src="js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 391 KiB |
|
After Width: | Height: | Size: 407 KiB |
|
After Width: | Height: | Size: 496 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 736 KiB |
|
After Width: | Height: | Size: 790 KiB |
|
After Width: | Height: | Size: 809 KiB |
@@ -0,0 +1,129 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>轮廓与皱纹三视图渐变 · 测试预览</title>
|
||||
<style>
|
||||
* { box-sizing:border-box; }
|
||||
html,body { margin:0; height:100%; background:#161616; font-family:system-ui,sans-serif; }
|
||||
body { display:flex; flex-direction:column; }
|
||||
header { flex:none; min-height:48px; background:#fff; display:flex; align-items:center; justify-content:center; gap:12px; padding:8px 12px; font-size:12px; }
|
||||
header span { color:#666; }
|
||||
button { border:0; border-radius:5px; background:#F5F078; color:#282828; padding:7px 12px; cursor:pointer; }
|
||||
iframe { display:block; border:0; width:100%; flex:1; min-height:0; }
|
||||
#error { display:none; color:#fff; padding:20px; text-align:center; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header><strong>轮廓/皱纹预览</strong><span>每3.8秒切换 · 渐变0.8秒</span><button id="toggle" disabled>暂停</button></header>
|
||||
<p id="error" role="alert"></p>
|
||||
<iframe id="report" title="测试报告:正脸、左脸、右脸自动渐变" src="./?mock=1&screen=report"></iframe>
|
||||
<script>
|
||||
// Standalone local demo. Only alters this iframe's DOM; no analysis or save calls.
|
||||
(function () {
|
||||
var observer=null, paused=false, disposed=false, groups=[];
|
||||
var iframe=document.getElementById('report'),toggle=document.getElementById('toggle');
|
||||
var names=['正脸','左脸','右脸'];
|
||||
// Repeating cycle: front → left → front → right → front → left …
|
||||
var sequence=[0,1,0,2];
|
||||
// Same face-centered crop formula as app.js vizFaceCrop. The demo's
|
||||
// three sample photos share dimensions/framing; use their reference box.
|
||||
function focusFace(img,referenceBox,referencePhoto,zoomFraction){
|
||||
var width=img.naturalWidth,height=img.naturalHeight;
|
||||
var sx=width/referencePhoto.naturalWidth,sy=height/referencePhoto.naturalHeight;
|
||||
var rectangle=referenceBox&&{left:referenceBox.left*sx,top:referenceBox.top*sy,width:referenceBox.width*sx,height:referenceBox.height*sy};
|
||||
if(!rectangle||rectangle.left<0||rectangle.top<0||rectangle.width<=0||rectangle.height<=0||rectangle.left+rectangle.width>width||rectangle.top+rectangle.height>height)return;
|
||||
var ratio=215/278;
|
||||
var ch=Math.min(Math.max(rectangle.height*1.55,rectangle.width*1.18/ratio),height,width/ratio),cw=ch*ratio;
|
||||
if(cw<rectangle.width||ch<rectangle.height)return;
|
||||
var left=Math.max(0,Math.min(width-cw,rectangle.left+rectangle.width/2-cw/2));
|
||||
var top=Math.max(0,Math.min(height-ch,rectangle.top-rectangle.height*.3));
|
||||
if(top+ch<rectangle.top+rectangle.height)top=rectangle.top+rectangle.height-ch;
|
||||
if(zoomFraction===.5){
|
||||
// Half of the added magnification, not half of the resulting scale.
|
||||
// Start with a centered, contain-fit crop at the same frame ratio.
|
||||
var baseHeight=Math.max(height,width/ratio),baseWidth=baseHeight*ratio;
|
||||
var baseScale=1/baseWidth,fullScale=1/cw;
|
||||
var halfWidth=1/(baseScale+(fullScale-baseScale)*.5),halfHeight=halfWidth/ratio;
|
||||
var centerX=width*.5+(left+cw*.5-width*.5)*.5;
|
||||
var centerY=height*.5+(top+ch*.5-height*.5)*.5;
|
||||
cw=halfWidth;ch=halfHeight;left=centerX-cw*.5;top=centerY-ch*.5;
|
||||
img.dataset.zoomFraction='0.5';
|
||||
}
|
||||
img.style.inset='auto';img.style.width=(width/cw*100)+'%';img.style.height=(height/ch*100)+'%';
|
||||
img.style.left=(-left/cw*100)+'%';img.style.top=(-top/ch*100)+'%';
|
||||
img.style.objectFit='fill';img.style.maxWidth='none';img.dataset.crop='face';
|
||||
}
|
||||
function stop(group){clearInterval(group.timer);group.timer=null;}
|
||||
function next(group){
|
||||
var previous=group.layers[group.current];
|
||||
previous.style.zIndex='1';
|
||||
group.step=(group.step+1)%sequence.length;
|
||||
group.current=sequence[group.step];
|
||||
var incoming=group.layers[group.current];
|
||||
incoming.style.zIndex='2';
|
||||
incoming.classList.add('active');
|
||||
// Keep the old image fully opaque underneath the incoming fade. This
|
||||
// yields a real cross-dissolve without flashing the white background.
|
||||
clearTimeout(group.fadeTimer);
|
||||
group.fadeTimer=setTimeout(function(){previous.classList.remove('active');previous.style.zIndex='0';},850);
|
||||
group.caption.textContent=names[group.current]+' · '+(group.current+1)+' / 3';
|
||||
}
|
||||
function sync(){groups.forEach(function(group){stop(group);if(!disposed&&!paused&&group.inView&&!document.hidden&&group.ready)group.timer=setInterval(function(){next(group);},3800);});}
|
||||
toggle.onclick=function(){paused=!paused;toggle.textContent=paused?'继续播放':'暂停';toggle.setAttribute('aria-pressed',String(paused));sync();};
|
||||
document.addEventListener('visibilitychange',sync);
|
||||
window.addEventListener('pagehide',function(){disposed=true;groups.forEach(function(group){stop(group);clearTimeout(group.fadeTimer);group.layers.forEach(function(img,i){img.classList.toggle('active',i===group.current);img.style.zIndex=i===group.current?'2':'0';});});});
|
||||
window.addEventListener('pageshow',function(){disposed=false;sync();});
|
||||
iframe.onload=async function(){
|
||||
groups.forEach(function(group){stop(group);clearTimeout(group.fadeTimer);});if(observer)observer.disconnect();groups=[];
|
||||
try{
|
||||
var doc=iframe.contentDocument,card=doc.querySelector('.card-detail');
|
||||
var referencePhoto=doc.createElement('img');
|
||||
await new Promise(function(resolve,reject){referencePhoto.onload=resolve;referencePhoto.onerror=reject;referencePhoto.src=doc.defaultView.MOCK_FIXTURE.photos[0];});
|
||||
// Preserve the real front-view wrinkle composite rendered by the
|
||||
// report (including its original dimming and detection overlay).
|
||||
var wrinkleImage=doc.getElementById('ditem-img2');
|
||||
await new Promise(function(resolve,reject){
|
||||
var started=Date.now(),poll=setInterval(function(){
|
||||
if(wrinkleImage&&!wrinkleImage.hidden&&wrinkleImage.complete&&wrinkleImage.naturalWidth){clearInterval(poll);resolve();}
|
||||
else if(Date.now()-started>8000){clearInterval(poll);reject(new Error('皱纹浮层未加载'));}
|
||||
},50);
|
||||
});
|
||||
var style=doc.createElement('style');
|
||||
style.textContent='.contour-demo-frame .ditem-img:not(.demo-face),.contour-demo-frame .viz-missing{display:none!important}.contour-demo-frame .demo-face{position:absolute;inset:0;display:block;opacity:0;transition:opacity .8s linear!important;pointer-events:none;object-fit:cover}.contour-demo-frame .demo-face.active{opacity:1;pointer-events:auto}.contour-demo-caption{text-align:center;color:#707070;font-size:11px;line-height:20px;margin:-4px 0 12px}';
|
||||
doc.head.appendChild(style);
|
||||
var sources=['front','left','right'];
|
||||
var ready=[];
|
||||
card.querySelectorAll('.viz-frame').forEach(function(frame){
|
||||
frame.classList.add('contour-demo-frame');
|
||||
var isWrinkles=!!frame.querySelector('#ditem-img2');
|
||||
var group={frame:frame,layers:[],current:0,step:0,inView:false,ready:false,timer:null,fadeTimer:null};
|
||||
group.caption=doc.createElement('p');group.caption.className='contour-demo-caption';group.caption.textContent='正脸 · 1 / 3';frame.after(group.caption);
|
||||
groups.push(group);
|
||||
sources.forEach(function(source,i){ready.push(new Promise(function(resolve,reject){
|
||||
var img=doc.createElement('img');img.className='ditem-img demo-face'+(i===0?' active':'');img.alt=names[i]+'拍摄照片';
|
||||
var useWrinkleOverlay=isWrinkles&&i===0;
|
||||
img.onload=function(){
|
||||
if(isWrinkles&&!useWrinkleOverlay)focusFace(img,doc.defaultView.MOCK_FIXTURE.face_rectangle,referencePhoto);
|
||||
if(!isWrinkles)focusFace(img,doc.defaultView.MOCK_FIXTURE.face_rectangle,referencePhoto,.5);
|
||||
resolve();
|
||||
};img.onerror=function(){reject(new Error('示例照片加载失败'));};
|
||||
img.dataset.wrinkleOverlay=useWrinkleOverlay?'original-front':'none';
|
||||
frame.appendChild(img);group.layers.push(img);
|
||||
img.src=useWrinkleOverlay?wrinkleImage.src:(doc.defaultView.MOCK_FIXTURE.photos[i]||'assets/mock/contour-demo/'+source+'.jpg');
|
||||
}));});
|
||||
});
|
||||
await Promise.all(ready);if(disposed)return;
|
||||
toggle.disabled=false;
|
||||
card.previousElementSibling.scrollIntoView({block:'start'});
|
||||
groups.forEach(function(group){group.ready=true;});
|
||||
observer=new IntersectionObserver(function(entries){entries.forEach(function(entry){var group=groups.find(function(g){return g.frame===entry.target;});group.inView=entry.isIntersecting&&entry.intersectionRatio>=.2;});sync();},{threshold:.2});
|
||||
groups.forEach(function(group){observer.observe(group.frame);});
|
||||
sync();
|
||||
}catch(e){var error=document.getElementById('error');error.style.display='block';error.textContent='预览未就绪,请刷新重试。';}
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
/* Loaded only by the rednote instance: persistence disclosure needs natural height. */
|
||||
#p-privacy .popup {
|
||||
width: min(308px, calc(100vw - 24px));
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
max-height: calc(100vh - 24px);
|
||||
overflow-y: auto;
|
||||
padding: 36px 32px 32px;
|
||||
top: 0;
|
||||
}
|
||||
#p-privacy .popup-title,
|
||||
#p-privacy .popup-body,
|
||||
#p-privacy .privacy-row,
|
||||
#p-privacy .popup-btn {
|
||||
position: static;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
#p-privacy .popup-title { margin: 0 0 16px; }
|
||||
#p-privacy .popup-body { width: auto; margin: 0 0 18px; }
|
||||
#p-privacy .privacy-row { margin: 0; }
|
||||
#p-privacy .popup-btn { margin: 26px auto 0; }
|
||||
@@ -220,6 +220,11 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
|
||||
#cam-video.mirror { transform: scaleX(-1); } /* 前置镜头镜像预览 */
|
||||
#s-cam.cam-live #cam-video { display: block; }
|
||||
#s-cam.cam-live .cam-bg { display: none; }
|
||||
#cam-frozen { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; display:none; z-index:0; pointer-events:none; }
|
||||
#cam-frozen.mirror { transform:scaleX(-1); }
|
||||
#s-cam.cam-switching #cam-frozen { display:block; }
|
||||
#s-cam.cam-switching .cam-bg { display:none; }
|
||||
.cam-flip:disabled { opacity:.5; }
|
||||
/* 纵向定位按 WebView 口径:Figma 帧 812 高扣掉顶部 86px 微信导航栏 = 726 高视口 */
|
||||
.cam-step {
|
||||
position: absolute; left: 0; right: 0; top: 7.1%;
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
}
|
||||
.guide-description { font-size: 12px; line-height: 1.6; margin-bottom: calc(35 * var(--u)); }
|
||||
.cam-feature-lines { fill: none; stroke: #fff; stroke-width: 2.5; stroke-dasharray: 5 5; filter: drop-shadow(0 1px 1px #000); }
|
||||
#s-cam:not(.cam-live) .cam-feature-lines { stroke: #111; filter: none; }
|
||||
#s-cam:not(.cam-live):not(.cam-switching) .cam-feature-lines { stroke: #111; filter: none; }
|
||||
.font-credit { display: block; text-align: center; color: #888; font-size: 10px; margin: 24px auto 0; }
|
||||
.account-status { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 14px; border-radius: 8px; font-size: 12px; }
|
||||
.account-status span::before { content: ''; display: inline-block; width: 7px; height: 7px; margin-right: 7px; border-radius: 50%; background: #b7bac0; }
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
/* Third-round PSD, 2026-10-12. Measurements include text transforms.
|
||||
Native 248px WeChat header is excluded. Fixed text sizes/line breaks are
|
||||
intentional; flexible photo backgrounds and safe areas accommodate phones. */
|
||||
.account-logout { font-size: 11px; opacity: .8; white-space: nowrap; }
|
||||
.cam-countdown { position:absolute; left:50%; top:45%; transform:translate(-50%,-50%); z-index:4; pointer-events:none; color:#fff; font:600 88px/1 Arial,sans-serif; text-shadow:0 2px 16px rgba(0,0,0,.5); }
|
||||
.cam-countdown-tick { display:block; animation:camera-countdown-tick .6s ease-out both; }
|
||||
@keyframes camera-countdown-tick { from { opacity:.3; transform:scale(1.25); } to { opacity:1; transform:scale(1); } }
|
||||
@media (prefers-reduced-motion:reduce) { .cam-countdown-tick { animation:none; } }
|
||||
.splash-slogan {
|
||||
left: calc(50% - 1.041667px); right: auto;
|
||||
width: 249.305556px; height: 75.347222px;
|
||||
@@ -8,6 +13,15 @@
|
||||
top: max(6.838833%, calc(env(safe-area-inset-top) + 48px));
|
||||
}
|
||||
#s-splash .splash-slogan { top: max(6.838833%, calc(env(safe-area-inset-top) + 48px)); }
|
||||
/* Keep the PSD Chinese line; compensate the lowercase brand's smaller x-height. */
|
||||
.splash-slogan-updated .splash-slogan-copy { width:100%; height:100%; clip-path:inset(68% 0 0); }
|
||||
.splash-slogan-brand, .splash-slogan-english { position:absolute; top:0; left:50%; transform:translateX(-50%); font-family:Arial,sans-serif; font-size:23px; font-weight:400; line-height:1; white-space:nowrap; }
|
||||
.splash-slogan-brand { top:-4px; font-size:28px; }
|
||||
.splash-slogan-english { top:26.736111px; }
|
||||
#s-splash [data-action="open-privacy"],
|
||||
#s-splash [data-action="open-privacy"]:focus,
|
||||
#s-splash [data-action="open-privacy"]:active { border:0; outline:none; box-shadow:none; -webkit-appearance:none; appearance:none; }
|
||||
#s-splash [data-action="open-privacy"]:focus-visible { filter:brightness(.94); }
|
||||
.splash-bottom { bottom: max(5.882353%, env(safe-area-inset-bottom)); gap: calc(7.291667 * var(--u)); padding-bottom: 0; }
|
||||
.brand-row { margin-bottom: calc(10.416667 * var(--u)); gap: 0; }
|
||||
.brand-lockup { width: 179.513889px; height: 20.486111px; }
|
||||
@@ -72,12 +86,42 @@
|
||||
.prod-scroll { flex: 0 0 51.219512%; }
|
||||
.prod-scroll img { width: 100%; height: 100%; aspect-ratio: auto; object-fit: fill; }
|
||||
|
||||
/* Figma 1:65, 1080px canvas. One algorithm-selected product, not a catalogue. */
|
||||
/* User-confirmed: both lines centered; use the same font size/line height
|
||||
as the stability heading rather than scaling text on wide previews. */
|
||||
.prod-title { width:calc(206.597222 * var(--u)); max-width:100%; text-align:center; margin:calc(18 * var(--u)) auto; padding:0; transform:translateX(calc(.347222 * var(--u))); font-size:22.106597px; line-height:28.738542px; font-weight:700; white-space:nowrap; }
|
||||
.report-care-products { width:calc(340.625 * var(--u)); margin-left:auto; margin-right:auto; padding:calc(21.180556 * var(--u)) calc(24.652778 * var(--u)) calc(7.986111 * var(--u)); }
|
||||
.report-care-products .card-heading,
|
||||
.product-section-heading { font-size:13.888889px; line-height:19.444444px; font-weight:700; text-align:left; }
|
||||
.report-care-products .card-heading { margin-bottom:calc(5.208333 * var(--u)); }
|
||||
.report-care-products .care-item { font-size:11.574074px; line-height:14.467593px; margin-bottom:calc(14.467593 * var(--u)); }
|
||||
.report-care-products .care-item:last-child { margin-bottom:0; }
|
||||
.product-section-heading { position:relative; margin:calc(24.0625 * var(--u)) 0 calc(5.555556 * var(--u)); }
|
||||
.report-care-products .product-section-heading::before { content:""; position:absolute; left:0; right:0; top:calc(-12.03125 * var(--u)); border-top:1px solid #DDD; }
|
||||
.report-care-products .card-product { width:auto; margin:0 calc(-18.402778 * var(--u)); border-radius:calc(3.472222 * var(--u)); background:#F6F6F6; }
|
||||
.report-care-products .prod-card { height:auto; aspect-ratio:945 / 511; }
|
||||
.report-care-products .prod-info { flex-basis:49.100529%; }
|
||||
.report-care-products .prod-scroll { flex-basis:50.899471%; }
|
||||
.report-care-products #product-title { left:8.571429%; top:29.158513%; font-size:14.467593px; line-height:18.062790px; font-weight:700; white-space:nowrap; }
|
||||
.report-care-products .prod-name { left:8.677249%; top:41.095890%; font-size:12.013166px; line-height:15.070408px; font-weight:700; color:#989898; white-space:nowrap; }
|
||||
.report-care-products .prod-badge { left:8.783069%; top:56.751468%; width:10.687831%; }
|
||||
.report-care-products + .report-disclaimer { margin:calc(12.152778 * var(--u)) calc(5.208333 * var(--u)) calc(20 * var(--u)); font-size:7.233681px; line-height:10.127431px; }
|
||||
/* Equal whitespace above/below all three headings; keep the original total gap. */
|
||||
.dimension-title { margin:calc(18 * var(--u)) 0; }
|
||||
.dimension-stability-title { font-size:22.106597px; line-height:28.738542px; }
|
||||
.stability-card .dtab-tx { font-size:11.055903px; }
|
||||
.stability-card .ditem-para { font-size:11.574074px; line-height:14.467593px; }
|
||||
|
||||
/* Fourth feedback adds a fast one-second round trip to original PSD pixels.
|
||||
The stage/mask share one contain transform, including short/landscape screens. */
|
||||
#p-analyzing { background: #000; overflow: hidden; }
|
||||
.scan-stage { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); width: min(100vw, 480px, calc(100dvh * 1080 / 2091)); height: min(100dvh, calc(min(100vw, 480px) * 2091 / 1080)); }
|
||||
@supports not (height: 100dvh) { .scan-stage { width: min(100vw, 480px, calc(100vh * 1080 / 2091)); height: min(100vh, calc(min(100vw, 480px) * 2091 / 1080)); } }
|
||||
.scan-artwork { width: 100%; height: 100%; object-fit: contain; }
|
||||
/* Cover the old flip icon baked into the PSD raster (843,1846–921,1924).
|
||||
Share the camera's replacement asset; decorative only, never a control. */
|
||||
.scan-flip-icon { position:absolute; left:77.314815%; top:87.900526%; width:8.888889%; height:4.591105%; background:#000; display:flex; align-items:center; justify-content:center; pointer-events:none; }
|
||||
.scan-flip-icon img { width:83.333333%; height:83.333333%; object-fit:contain; }
|
||||
.scan-face { position: absolute; inset: 0; -webkit-mask-size: 100% 100%; mask-size: 100% 100%; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; overflow: hidden; }
|
||||
.scan-beam { position: absolute; left: 0; right: 0; top: 20%; height: 18%; background: linear-gradient(to bottom, #F5F078 0%, #F5F078 35%, rgba(245,240,120,0) 100%); }
|
||||
#p-analyzing:not(.scan-artwork-ready) .scan-stage { visibility: hidden; }
|
||||
@@ -120,7 +164,7 @@
|
||||
#stat-score { display: flex; align-items: baseline; justify-content: center; }
|
||||
#stat-score b { display: block; letter-spacing: 0; }
|
||||
#stat-score i { position: static; margin-left: 2px; }
|
||||
.card-care .card-heading { text-align: left; }
|
||||
.card-care:not(.report-care-products) .card-heading { text-align: left; margin-bottom: calc(9 * var(--u)); }
|
||||
|
||||
/* The profile is a fixed page, not a scroll container. Keep normal-height PSD
|
||||
spacing; shrink only vertical whitespace/photo height on shorter viewports.
|
||||
@@ -159,6 +203,20 @@
|
||||
.detail-panels.dragging { transition: none; will-change: transform; }
|
||||
.detail-panels .dpanel { display: block; flex: 0 0 100%; min-width: 0; padding-top: calc(10 * var(--u)); animation: none; }
|
||||
.detail-panels .ditem-img { -webkit-user-drag: none; }
|
||||
/* Keep the existing front-photo scale/top edge; clip only its bottom 10%.
|
||||
The image source stays intact for the existing full-image zoom overlay. */
|
||||
.contour-front-frame { height: calc(250.2 * var(--u)); margin-bottom: calc(7.56 * var(--u)); }
|
||||
.contour-front-frame .ditem-img { height: calc(278 * var(--u)); }
|
||||
.contour-sides { width: calc(215 * var(--u)); margin: 0 auto calc(12 * var(--u)); display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: calc(6.3 * var(--u)); }
|
||||
.contour-sides[hidden], .contour-sides figure[hidden] { display: none; }
|
||||
.contour-sides figure { margin: 0; min-width: 0; }
|
||||
.contour-sides figure[data-view="right"] { grid-column: 2; }
|
||||
.contour-side-photo, .wrinkle-side-photo { display: block; position: relative; width: 100%; height: calc(92.25 * var(--u)); padding: 0; border: 0; border-radius: calc(5 * var(--u)); overflow: hidden; background: #f6f6f6; cursor: zoom-in; }
|
||||
.contour-side-photo img, .wrinkle-side-photo img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center; transform: translateY(-25%) scale(2.8); transform-origin: center; -webkit-user-drag: none; }
|
||||
.contour-side-photo:focus-visible, .wrinkle-side-photo:focus-visible { outline: 2px solid #333; outline-offset: 3px; }
|
||||
.wrinkle-layer-note { width:calc(215 * var(--u)); margin:0 auto calc(12 * var(--u)); color:#888; font-size:10px; line-height:1.4; }
|
||||
.wrinkle-loading { position:absolute; inset:0; z-index:1; display:flex; align-items:center; justify-content:center; background:rgba(0,0,0,.4); color:#fff; text-align:center; font-size:calc(10 * var(--u)); line-height:1.6; pointer-events:none; }
|
||||
.wrinkle-loading[hidden] { display:none; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.detail-panels, .detail-viewport { transition-duration: .01ms; }
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<img class="login-brand" src="assets/img/ui-20261009/brand-login.png" alt="1’me 以我">
|
||||
<div class="login-bottom">
|
||||
<button class="login-button login-wechat" data-action="login-wechat"><img src="assets/img/ui-20261009/icon-wechat.png" alt="">微信登录</button>
|
||||
<button class="login-button login-browser" data-action="login-browser"><img src="assets/img/ui-20261009/icon-guest.png" alt="">游客登录</button>
|
||||
<button class="login-button login-browser" data-action="guest-experience"><img src="assets/img/ui-20261009/icon-guest.png" alt="">游客登陆</button>
|
||||
<label class="login-consent"><input type="checkbox" id="login-consent"><span>同意<button type="button" data-action="open-policy">用户协议、隐私政策和儿童保护政策</button></span></label>
|
||||
<p class="login-message" id="login-message" role="status">正在检查登录状态…</p>
|
||||
</div>
|
||||
@@ -32,7 +32,7 @@
|
||||
<section class="screen" id="s-history">
|
||||
<div class="history-content">
|
||||
<h1 class="report-title">您的报告</h1>
|
||||
<button class="account-status account-status-history" data-action="switch-login" aria-label="切换登录方式"><span data-auth-status></span><small>切换登录方式 ›</small></button>
|
||||
<div class="account-status account-status-history"><span data-auth-status></span><button type="button" class="account-logout" data-action="logout-wechat" hidden>退出登录</button></div>
|
||||
<div class="card history-count"><span>测试数量</span><p id="history-count">正在读取报告…</p></div>
|
||||
<p class="identity-note" id="identity-note"></p>
|
||||
<div id="history-list" aria-live="polite"></div>
|
||||
@@ -42,8 +42,12 @@
|
||||
</section>
|
||||
<section class="screen" id="s-splash">
|
||||
<div class="splash-bg"></div>
|
||||
<button class="account-status account-status-home" data-action="switch-login" aria-label="切换登录方式"><span data-auth-status></span><small>切换 ›</small></button>
|
||||
<img class="splash-slogan" src="assets/img/ui-20261012/splash-slogan.png" width="718" height="217" loading="lazy" alt="ONE ME,FROM THE INSIDE OUT;以我为始 自有表现">
|
||||
<div class="account-status account-status-home"><span data-auth-status></span><button type="button" class="account-logout" data-action="logout-wechat" hidden>退出登录</button></div>
|
||||
<div class="splash-slogan splash-slogan-updated" role="img" aria-label="1'me,FROM THE INSIDE OUT;以我为始 自有表现">
|
||||
<img class="splash-slogan-copy" src="assets/img/ui-20261012/splash-slogan.png" width="718" height="217" loading="lazy" alt="">
|
||||
<span class="splash-slogan-brand" aria-hidden="true">1'me,</span>
|
||||
<span class="splash-slogan-english" aria-hidden="true">FROM THE INSIDE OUT</span>
|
||||
</div>
|
||||
<div class="splash-bottom">
|
||||
<div class="brand-row">
|
||||
<img src="assets/img/ui-20261012/brand-lockup.png" width="517" height="59" loading="lazy" alt="1'me 肌肤状态检测" class="brand-lockup">
|
||||
@@ -140,6 +144,7 @@
|
||||
</svg>
|
||||
<p class="cam-status" id="cam-status">检测引擎加载中…</p>
|
||||
<p class="cam-hint" id="cam-hint"></p>
|
||||
<div class="cam-countdown" id="cam-countdown" aria-hidden="true" hidden></div>
|
||||
<p class="cam-nudge" id="cam-nudge">识别不到?请正对镜头,并到光线充足处</p>
|
||||
<div class="cam-next" id="cam-next" aria-live="polite">
|
||||
<i class="cam-next-ok"></i>
|
||||
@@ -159,7 +164,7 @@
|
||||
<img src="assets/img/ui-20261009/icon-shutter.png" alt="快门">
|
||||
</button>
|
||||
<button class="cam-side cam-flip" data-action="flip" aria-label="切换">
|
||||
<img src="assets/img/ui-20261009/icon-switch-camera.png" alt="切换">
|
||||
<img src="assets/img/ui-20261009/icon-switch-camera.png?v=20261013" alt="切换">
|
||||
</button>
|
||||
</div>
|
||||
<div class="cam-fill" id="cam-fill"></div>
|
||||
@@ -206,11 +211,20 @@
|
||||
<div class="detail-tabs"><button class="dtab active" data-tab="0"><span class="dtab-tx">轮廓</span></button><button class="dtab" data-tab="1"><span class="dtab-tx">皱纹</span></button></div>
|
||||
<div class="detail-viewport"><div class="detail-panels">
|
||||
<div class="dpanel active" data-panel="0">
|
||||
<div class="ditem"><div class="viz-frame"><img id="ditem-img1" class="ditem-img" alt="轮廓检测图" hidden><span class="viz-missing">本次未获取轮廓检测图</span></div>
|
||||
<div class="ditem"><div class="viz-frame contour-front-frame"><img id="ditem-img1" class="ditem-img" alt="轮廓检测图" hidden><span class="viz-missing">本次未获取轮廓检测图</span></div>
|
||||
<div class="contour-sides" id="contour-sides" hidden>
|
||||
<figure data-view="left" hidden><button type="button" class="contour-side-photo" aria-label="查看左脸完整大图"><img id="contour-left" alt="左脸拍摄照片"></button></figure>
|
||||
<figure data-view="right" hidden><button type="button" class="contour-side-photo" aria-label="查看右脸完整大图"><img id="contour-right" alt="右脸拍摄照片"></button></figure>
|
||||
</div>
|
||||
<div class="ditem-text"><p class="ditem-para" id="ditem-para1">检测完成后显示分析结果。</p></div></div>
|
||||
</div>
|
||||
<div class="dpanel" data-panel="1">
|
||||
<div class="ditem"><div class="viz-frame"><img id="ditem-img2" class="ditem-img" alt="皱纹检测图" hidden><span class="viz-missing">本次未获取皱纹检测图</span></div>
|
||||
<div class="ditem"><div class="viz-frame contour-front-frame"><img id="ditem-img2" class="ditem-img" alt="皱纹检测图" hidden><span class="viz-missing">本次未获取皱纹检测图</span></div>
|
||||
<div class="contour-sides wrinkle-sides" id="wrinkle-sides" hidden>
|
||||
<figure data-view="left" hidden><button type="button" class="wrinkle-side-photo" aria-label="查看左脸皱纹检测大图"><img id="wrinkle-left" alt="左脸皱纹检测照片"><span class="wrinkle-loading" role="status" aria-live="polite" hidden><span>皱纹标注<br>加载中…</span></span></button></figure>
|
||||
<figure data-view="right" hidden><button type="button" class="wrinkle-side-photo" aria-label="查看右脸皱纹检测大图"><img id="wrinkle-right" alt="右脸皱纹检测照片"><span class="wrinkle-loading" role="status" aria-live="polite" hidden><span>皱纹标注<br>加载中…</span></span></button></figure>
|
||||
</div>
|
||||
<p class="wrinkle-layer-note" id="wrinkle-layer-note" hidden>部分侧脸未获取皱纹标注,未标注的侧脸仅展示原图。</p>
|
||||
<div class="ditem-text"><p class="ditem-para" id="ditem-para2">检测完成后显示分析结果。</p></div></div>
|
||||
</div>
|
||||
</div></div>
|
||||
@@ -234,8 +248,8 @@
|
||||
</div>
|
||||
</div></div>
|
||||
</div>
|
||||
<h2 class="dimension-title">稳韧力</h2>
|
||||
<div class="card card-detail">
|
||||
<h2 class="dimension-title dimension-stability-title">稳韧力</h2>
|
||||
<div class="card card-detail stability-card">
|
||||
<div class="detail-tabs"><button class="dtab active" data-tab="0"><span class="dtab-tx">泛红</span></button><button class="dtab" data-tab="1"><span class="dtab-tx">痘痘</span></button></div>
|
||||
<div class="detail-viewport"><div class="detail-panels">
|
||||
<div class="dpanel active" data-panel="0">
|
||||
@@ -250,33 +264,28 @@
|
||||
</div>
|
||||
<p class="vm-notes" id="vm-notes" style="display:none"></p>
|
||||
|
||||
<p class="report-disclaimer">*您的肌肤状态会随年龄、环境、作息和生活节奏动态变化。<br>*本次检测仅代表当前肌肤状态参考。</p>
|
||||
|
||||
<div class="card card-care">
|
||||
<h2 class="card-heading">护理建议</h2>
|
||||
<h2 class="prod-title">感受高光时刻<br>从日常的好状态开始</h2>
|
||||
<div class="card card-care report-care-products">
|
||||
<h2 class="card-heading">日常护理</h2>
|
||||
<div id="care-list">
|
||||
<p class="care-item">检测完成后显示分析结果。</p>
|
||||
</div>
|
||||
<p class="care-summary">好状态,是内外同调的长期节奏。</p>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 产品推荐大标题独立于白卡之外,直接落在灰底上(Figma 08) -->
|
||||
<h2 class="prod-title">感受高光时刻<br>从日常的好状态开始</h2>
|
||||
<h2 class="product-section-heading">推荐产品</h2>
|
||||
<p id="product-unavailable" class="care-item" hidden>本次未获取有效肌龄,暂无法推荐产品,建议重新检测。</p>
|
||||
<div class="card card-product">
|
||||
<div class="card-product">
|
||||
<div class="prod-card">
|
||||
<div class="prod-info">
|
||||
<h3 id="product-title"></h3>
|
||||
<p class="prod-name" id="product-name"></p>
|
||||
<img class="prod-badge" src="assets/img/ui-20261009/product-badge.png" alt="1’me">
|
||||
<img class="prod-badge" src="assets/img/ui-20261015/product-badge.png" alt="1’me">
|
||||
</div>
|
||||
<div class="prod-scroll" id="prod-scroll">
|
||||
<img id="product-image" alt="算法推荐精华">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<p class="report-disclaimer">*您的肌肤状态会随年龄、环境、作息和生活节奏动态变化。<br>*本次检测仅代表当前肌肤状态参考。<br>*仅根据您最近的测肤记录推荐。</p>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
@@ -291,7 +300,7 @@
|
||||
<p class="popup-body">我们将采集您的面部影像,为您生成专属肌肤分析与状态管理方案。所有数据严格保密,仅用于本次检测分析。</p>
|
||||
<label class="privacy-row" id="privacy-row">
|
||||
<span class="checkbox" id="privacy-check"></span>
|
||||
<span class="privacy-text">我已阅读并同意<a class="privacy-link" id="privacy-policy-link">相关隐私协议</a></span>
|
||||
<span class="privacy-text">我已阅读并同意相关<a class="privacy-link" id="privacy-policy-link">隐私协议</a></span>
|
||||
</label>
|
||||
<button class="btn btn-primary popup-btn" data-action="agree-continue">继续</button>
|
||||
</div>
|
||||
@@ -300,7 +309,13 @@
|
||||
<!-- ═══════════ 弹窗:拍照引导 ═══════════ -->
|
||||
<!-- 背景只保留启动页底图 + Slogan(不叠档案表单),对齐设计稿 04 -->
|
||||
<div class="overlay overlay-guide" id="p-photoguide">
|
||||
<div class="guide-bg"><img class="splash-slogan" src="assets/img/ui-20261012/splash-slogan.png" width="718" height="217" loading="lazy" alt="ONE ME,FROM THE INSIDE OUT;以我为始 自有表现"></div>
|
||||
<div class="guide-bg">
|
||||
<div class="splash-slogan splash-slogan-updated" role="img" aria-label="1'me,FROM THE INSIDE OUT;以我为始 自有表现">
|
||||
<img class="splash-slogan-copy" src="assets/img/ui-20261012/splash-slogan.png" width="718" height="217" loading="lazy" alt="">
|
||||
<span class="splash-slogan-brand" aria-hidden="true">1'me,</span>
|
||||
<span class="splash-slogan-english" aria-hidden="true">FROM THE INSIDE OUT</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="popup">
|
||||
<button class="popup-close" data-action="close-photoguide" aria-label="关闭">
|
||||
<img src="assets/img/ui-20261009/icon-close.png" alt="关闭">
|
||||
@@ -457,6 +472,7 @@
|
||||
<div class="scan-stage" aria-hidden="true">
|
||||
<img id="scan-artwork" class="scan-artwork" data-src="assets/img/ui-20261012/scan-base.webp" width="1080" height="2091" alt="" draggable="false">
|
||||
<div id="scan-face" class="scan-face" data-mask="assets/img/ui-20261012/scan-face-mask.png"><i class="scan-beam"></i></div>
|
||||
<span class="scan-flip-icon"><img src="assets/img/ui-20261009/icon-switch-camera.png?v=20261013" width="80" height="80" alt="" draggable="false"></span>
|
||||
</div>
|
||||
<p class="scan-status" role="status">检测中…</p>
|
||||
<button class="scan-cancel" type="button" data-action="cancel-analysis">取消</button>
|
||||
|
||||