feat(h5): align October UI and add WeChat auth with saved reports
This commit is contained in:
+31
-2
@@ -19,10 +19,12 @@ const PORT = Number(process.env.PORT) || 8080;
|
||||
const HOST = process.env.HOST || '127.0.0.1';
|
||||
const ROOT = __dirname;
|
||||
const FACEPP_URL = 'https://api-cn.faceplusplus.com/facepp/v1/skinanalyze_pro';
|
||||
const DATA_ROOT = path.join(ROOT, 'data');
|
||||
const DATA_ROOT = process.env.JUZI_DATA_ROOT ? path.resolve(process.env.JUZI_DATA_ROOT) : path.join(ROOT, 'data');
|
||||
const DATA_DIR = path.join(DATA_ROOT, 'results'); // 每次检测的原始返回落盘(复核用,已在 .gitignore)
|
||||
const PHOTOS_DIR = path.join(DATA_ROOT, 'photos'); // 检测照片:data/photos/<record_id>/{front,left,right}.jpg
|
||||
const RECORDS_FILE = path.join(DATA_ROOT, 'records.jsonl'); // 检测记录(一行一条 JSON)
|
||||
const ReportUI = require('./js/report-ui.js');
|
||||
const accounts = require('./lib/accounts.js')(path.join(DATA_ROOT, 'accounts'));
|
||||
|
||||
/* 服务端评分引擎:与 C 端同一份公式(禁止复制公式,直接复用) */
|
||||
const SkinScore = require('./js/skin-score.js');
|
||||
@@ -66,12 +68,19 @@ function serveStatic(req, res, pathname) {
|
||||
res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
|
||||
return res.end('Bad Request');
|
||||
}
|
||||
// Authorize the same canonical path that the filesystem will read. Repeated,
|
||||
// encoded and Windows-style slashes must not bypass private-directory checks.
|
||||
p = path.posix.normalize('/' + p.replace(/\\/g, '/'));
|
||||
if (p === '/') p = '/index.html';
|
||||
// /data/ 下是检测照片/原始 JSON/记录库(敏感个人信息),禁止静态直出:
|
||||
// 照片只能走 /api/admin/photos(由 nginx Basic Auth 保护),此处一律 403
|
||||
if (p === '/data' || p.startsWith('/data/')) { res.writeHead(403); return res.end('Forbidden'); }
|
||||
if (p === '/server.js' || /^\/(lib|test)(\/|$)/.test(p) || p.split('/').some(s => s.startsWith('.'))) {
|
||||
res.writeHead(404); return res.end('Not Found');
|
||||
}
|
||||
const file = path.normalize(path.join(ROOT, p));
|
||||
if (file !== ROOT && !file.startsWith(ROOT + path.sep)) { res.writeHead(403); return res.end('Forbidden'); }
|
||||
if (file === DATA_ROOT || file.startsWith(DATA_ROOT + path.sep)) { res.writeHead(403); return res.end('Forbidden'); }
|
||||
fs.readFile(file, (err, data) => {
|
||||
if (err) { res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); return res.end('Not Found'); }
|
||||
const ext = path.extname(file);
|
||||
@@ -342,6 +351,9 @@ async function callFacepp(front, left, right) {
|
||||
async function handleAnalyze(req, res) {
|
||||
let rec = null; // 记录:正脸照片校验通过后创建(此前失败无从落库)
|
||||
try {
|
||||
const owner = accounts.session(req);
|
||||
if (!owner) return sendJson(res, 401, { ok: false, message: '请先登录后检测' });
|
||||
if (!accounts.sameOrigin(req)) return sendJson(res, 403, { ok: false, message: '请求来源不匹配' });
|
||||
const body = JSON.parse((await readBody(req, 20 * 1024 * 1024)).toString('utf8'));
|
||||
// 三个 image 字段先做类型检查:非字符串视为缺失(左/右图缺失时仅发正脸,接口自动降级为单视图)
|
||||
const front = stripDataPrefix(typeof body.front_image === 'string' ? body.front_image : '');
|
||||
@@ -362,6 +374,7 @@ async function handleAnalyze(req, res) {
|
||||
const photos = writePhotos(recordId, { front, left, right });
|
||||
rec = {
|
||||
record_id: recordId,
|
||||
owner_id: owner.id,
|
||||
created_at: nowIso(),
|
||||
updated_at: nowIso(),
|
||||
real_age: profile.real_age,
|
||||
@@ -412,9 +425,24 @@ async function handleAnalyze(req, res) {
|
||||
// 下颌线结果在响应顶层 left_side_result/right_side_result(不在 result 内),
|
||||
// 个别批次可能嵌套在 side_result 下,两种层级都兼容
|
||||
const nested = fpJson.side_result || {};
|
||||
const payload = {
|
||||
result, face_rectangle: fpJson.face_rectangle || null,
|
||||
side: { left: fpJson.left_side_result || nested.left_side_result || null,
|
||||
right: fpJson.right_side_result || nested.right_side_result || null }
|
||||
};
|
||||
let reportSaved = false, presentation = null, snapshotVm = null;
|
||||
try {
|
||||
snapshotVm = SkinScore.buildViewModel(result, payload.side, scoreProfile(rec));
|
||||
presentation = ReportUI.build(snapshotVm, result);
|
||||
accounts.storeReport(owner.id, { id: recordId, created_at: rec.created_at,
|
||||
version: '2026-10-09', profile, vm: snapshotVm, presentation, payload,
|
||||
photos: [front, left, right].map(b64 => b64 ? toDataUrl(b64) : null) });
|
||||
reportSaved = true;
|
||||
} catch (e) { console.error('[report] 完整报告保存失败:', e.message); }
|
||||
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(JSON.stringify({
|
||||
ok: true,
|
||||
record_id: recordId, report_saved: reportSaved, presentation, vm: snapshotVm,
|
||||
result,
|
||||
face_rectangle: fpJson.face_rectangle || null,
|
||||
side: {
|
||||
@@ -752,8 +780,9 @@ async function handleAdmin(req, res, pathname, url) {
|
||||
}
|
||||
}
|
||||
|
||||
http.createServer((req, res) => {
|
||||
http.createServer(async (req, res) => {
|
||||
const pathname = req.url.split('?')[0];
|
||||
if (await accounts.route(req, res, pathname)) return;
|
||||
if (req.method === 'POST' && pathname === '/api/skin-analyze') return handleAnalyze(req, res);
|
||||
if (req.method === 'GET' && pathname === '/api/health') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
|
||||
Reference in New Issue
Block a user