feat(server): 扫描报告存储(scan_reports)+诊断阈值配置(diagnosis_config)+admin 编辑

- scan_reports 表+dao+3个报告接口(POST/latest/:id,requireUser)
- GET /treatment/diagnosis-config 下发实时阈值(免发版调参)
- admin SettingsView 加 diagnosis_config JSON 编辑器
- 注意: schema.sql 变更需手动 TencentDB 迁移
这个提交包含在:
Guoguo
2026-07-18 20:17:09 -07:00
父节点 f2ee086cc8
当前提交 a7299708e2
修改 5 个文件,包含 187 行新增5 行删除
+79
查看文件
@@ -0,0 +1,79 @@
const { query, one, limitClause } = require('../lib/db')
/**
* Create a scan report
* @param {Object} report
* @param {number} report.user_id
* @param {string|null} report.device_id
* @param {string|null} report.scanned_at - MySQL datetime string
* @param {string} report.regions - JSON string (array)
* @param {string} report.overall - JSON string (array)
* @param {number} report.recommend_mask
* @param {string} report.recommend_plan - JSON string (array)
* @param {string} report.raw_pd - JSON string
* @param {number} report.calibrated - 0/1
* @returns {Promise<Object>} query result (has insertId)
*/
async function create(report) {
return query(
`INSERT INTO scan_reports
(user_id, device_id, scanned_at, regions, overall, recommend_mask, recommend_plan, raw_pd, calibrated)
VALUES (:user_id, :device_id, :scanned_at, :regions, :overall, :recommend_mask, :recommend_plan, :raw_pd, :calibrated)`,
report
)
}
/**
* Find a scan report by id
* @param {number} id
* @returns {Promise<Object|null>}
*/
async function findById(id) {
return one('SELECT * FROM scan_reports WHERE id = :id', { id })
}
/**
* Find the latest scan report for a user, optionally filtered by device
* @param {number} userId
* @param {string} [deviceId]
* @returns {Promise<Object|null>}
*/
async function findLatestByUser(userId, deviceId) {
if (deviceId) {
return one(
'SELECT * FROM scan_reports WHERE user_id = :user_id AND device_id = :device_id ORDER BY created_at DESC, id DESC LIMIT 1',
{ user_id: userId, device_id: deviceId }
)
}
return one(
'SELECT * FROM scan_reports WHERE user_id = :user_id ORDER BY created_at DESC, id DESC LIMIT 1',
{ user_id: userId }
)
}
/**
* List scan reports for a user with pagination
* @param {number} userId
* @param {Object} opts
* @param {number} opts.pageSize
* @param {number} opts.offset
* @returns {Promise<{records: Array, total: number}>}
*/
async function listByUser(userId, { pageSize, offset }) {
const total = await query(
'SELECT COUNT(*) AS total FROM scan_reports WHERE user_id = :user_id',
{ user_id: userId }
)
const records = await query(
'SELECT * FROM scan_reports WHERE user_id = :user_id ORDER BY created_at DESC, id DESC' + limitClause(pageSize, offset),
{ user_id: userId }
)
return { records, total: total[0].total }
}
module.exports = {
create,
findById,
findLatestByUser,
listByUser
}
+1 -1
查看文件
@@ -257,7 +257,7 @@ router.get('/settings', requireAdmin, wrap(async (req, res) => {
}))
router.post('/settings', requireAdmin, wrap(async (req, res) => {
const ALLOWED_KEYS = ['system_name', 'admin_email', 'timezone', 'monthly_price', 'yearly_price', 'trial_days', 'enable_register', 'enable_binding', 'enable_free_mode', 'enable_smart_mode', 'maintenance_mode']
const ALLOWED_KEYS = ['system_name', 'admin_email', 'timezone', 'monthly_price', 'yearly_price', 'trial_days', 'enable_register', 'enable_binding', 'enable_free_mode', 'enable_smart_mode', 'maintenance_mode', 'diagnosis_config']
for (const key of Object.keys(req.body || {})) {
if (!ALLOWED_KEYS.includes(key)) continue
await settingsDao.update(key, req.body[key])
+47
查看文件
@@ -3,11 +3,21 @@ const { ok, fail } = require('../lib/response')
const { requireUser } = require('../middleware/auth')
const { toMysqlDate } = require('../lib/utils')
const treatmentDao = require('../dao/treatment.dao')
const scanReportDao = require('../dao/scan-report.dao')
const bindingDao = require('../dao/binding.dao')
const logDao = require('../dao/log.dao')
const { getSettings } = require('../lib/settings-cache')
const wrap = fn => (req, res, next) => fn(req, res, next).catch(next)
// Fallback used if diagnosis_config has not been seeded in system_settings yet
const DEFAULT_DIAGNOSIS_CONFIG = {
calibrated: false,
note: '占位阈值,待真机标定后调整',
levels: { low: 0.15, mid: 0.35, high: 0.55 },
problem_wavelength: { aging: 2, acne: 3, pigment: 4, deep: 1 }
}
router.get('/treatment/history', requireUser, wrap(async (req, res) => {
const page = Math.max(1, parseInt(req.query.page, 10) || 1)
const pageSize = Math.min(Math.max(1, parseInt(req.query.page_size, 10) || 20), 100)
@@ -49,6 +59,43 @@ router.post('/treatment/sync', requireUser, wrap(async (req, res) => {
res.json(ok({ record_id: sessionId }))
}))
// --- Scan reports ---
router.post('/treatment/report', requireUser, wrap(async (req, res) => {
const d = req.body || {}
const result = await scanReportDao.create({
user_id: req.user.user_id,
device_id: d.device_id == null ? null : String(d.device_id),
scanned_at: toMysqlDate(d.scanned_at),
regions: JSON.stringify(d.regions || []),
overall: JSON.stringify(d.overall || []),
recommend_mask: parseInt(d.recommend_mask, 10) || 0,
recommend_plan: JSON.stringify(d.recommend_plan || []),
raw_pd: JSON.stringify(d.raw_pd || {}),
calibrated: d.calibrated ? 1 : 0
})
await logDao.write({ user_id: req.user.user_id, action: 'scan_report_create', detail: '保存扫描报告: #' + result.insertId, ip: req.ip })
res.json(ok({ report_id: result.insertId }))
}))
router.get('/treatment/report/latest', requireUser, wrap(async (req, res) => {
const report = await scanReportDao.findLatestByUser(req.user.user_id, req.query.device_id)
res.json(ok(report))
}))
router.get('/treatment/report/:id', requireUser, wrap(async (req, res) => {
const report = await scanReportDao.findById(req.params.id)
if (!report || String(report.user_id) !== String(req.user.user_id)) return res.json(fail(1005, 'record_not_found'))
res.json(ok(report))
}))
// --- Server-driven diagnosis thresholds ---
router.get('/treatment/diagnosis-config', requireUser, wrap(async (req, res) => {
const settings = await getSettings()
res.json(ok(settings.diagnosis_config || DEFAULT_DIAGNOSIS_CONFIG))
}))
router.get('/treatment/:record_id', requireUser, wrap(async (req, res) => {
const record = await treatmentDao.findBySession(req.params.record_id, req.user.user_id)
if (!record) return res.json(fail(1005, 'record_not_found'))