后端(新增 7 控制器 + 2 服务 + 17 张表) - Prepare:排卵预测、BBT(双向体温识别)、试纸识别(规则引擎+额度)、备孕任务、同房指导 - Family:邀请码绑定(≤5)、话术库、爱心积分、私密留言、家庭健康视图 - Health:按周期阶段输出饮食/运动/睡眠/痛经四类建议 - Relation:关系测评、引导卡、冲突工具、课程下单、双人任务(联动记忆)、纪念日 - Parenting:宝宝档案、成长记录、月龄活动匹配、共育任务、育儿情绪、疫苗儿保排期 - Consult:咨询师(资质/定价/时段)、预约下单/取消、量表测评(4 套)、危机干预转介 - Setting:自定义提醒 CRUD、数据备份快照与恢复(保留 5 份) - Mood 增补急救包;Home 提醒合并自定义提醒 - 服务层:ContentService(统一内容/锁态/购买态)、AssessmentService(计分分级+危机识别) 前端(新增 8 页面) - 备孕、家人共护、健康建议、两性关系、亲子教育、心理咨询、测评执行页、情绪急救包 - 首页快捷入口扩至 8 个模块;我的页新增提醒中心与数据备份 - utils/api.js 扩至 84 个接口 数据(可重复执行脚本) - 03_modules.sql:fa_reminder/bbt/lh_test/task/family_bind/care_message/point_log/memory/ assessment(+record)/course_order/baby/growth_record/vaccine/counselor/consult_order/user_backup - 04_module_seed.sql:话术6/引导卡6/冲突3/急救4/同房3/课程6/活动6/育儿情绪4/知识7、 4 套量表、17 项疫苗儿保、3 位咨询师 验证(全部通过) - 接口全链路 82 项断言(M1–M9,含高风险测评危机干预、备份恢复) - 契约校验 84 接口 0 偏差;Vue SFC 19/19、SCSS 18/18 编译通过 - 修复 3 个真实缺陷:action 内调用 action 导致响应被劫持、coTasks 返回数组、 咨询时段跨年错误
338 行
12 KiB
PHP
338 行
12 KiB
PHP
<?php
|
||
|
||
namespace app\api\controller;
|
||
|
||
use app\common\controller\Api;
|
||
use think\Db;
|
||
|
||
/**
|
||
* 隐私与提醒设置(M6)
|
||
*/
|
||
class Setting extends Api
|
||
{
|
||
protected $noNeedLogin = [];
|
||
protected $noNeedRight = '*';
|
||
|
||
protected $defaultRemind = [
|
||
'period' => true, // 经期前提醒
|
||
'ovulation' => true, // 排卵日提醒
|
||
'record' => true, // 每日记录提醒
|
||
'care' => true, // 关怀提醒(家人共护)
|
||
'time' => '21:00',
|
||
];
|
||
|
||
/**
|
||
* 读取设置
|
||
*/
|
||
public function index()
|
||
{
|
||
$setting = Db::name('user_setting')->where('user_id', $this->auth->id)->find();
|
||
if (!$setting) {
|
||
$setting = [
|
||
'app_lock' => 'none',
|
||
'local_only' => 0,
|
||
'remind_json' => json_encode($this->defaultRemind),
|
||
];
|
||
}
|
||
$remind = json_decode($setting['remind_json'], true) ?: $this->defaultRemind;
|
||
$remind = array_merge($this->defaultRemind, $remind);
|
||
|
||
$this->success('', [
|
||
'app_lock' => $setting['app_lock'],
|
||
'has_lock_code' => !empty($setting['app_lock_value']),
|
||
'local_only' => (int)$setting['local_only'],
|
||
'remind' => $remind,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 保存设置
|
||
* POST app_lock=none|password|biometric, app_lock_code=1234, local_only=0|1, remind={"period":true,...}
|
||
*/
|
||
public function save()
|
||
{
|
||
$userId = $this->auth->id;
|
||
$exist = Db::name('user_setting')->where('user_id', $userId)->find();
|
||
|
||
$data = ['updatetime' => time()];
|
||
|
||
$appLock = $this->request->post('app_lock', null);
|
||
if ($appLock !== null) {
|
||
if (!in_array($appLock, ['none', 'password', 'biometric'])) {
|
||
$this->error('应用锁类型不正确');
|
||
}
|
||
$data['app_lock'] = $appLock;
|
||
}
|
||
|
||
$code = (string)$this->request->post('app_lock_code', '');
|
||
if ($code !== '') {
|
||
if (!preg_match('/^\d{4,6}$/', $code)) {
|
||
$this->error('应用锁密码需为 4-6 位数字');
|
||
}
|
||
// 仅存哈希,不落明文
|
||
$data['app_lock_value'] = md5($code . 'huāqī');
|
||
}
|
||
|
||
$localOnly = $this->request->post('local_only', null);
|
||
if ($localOnly !== null) {
|
||
$data['local_only'] = (int)!!$localOnly;
|
||
}
|
||
|
||
$remind = $this->readRemind();
|
||
if ($remind !== null) {
|
||
$merged = array_merge($this->defaultRemind, array_intersect_key($remind, $this->defaultRemind));
|
||
if (isset($merged['time']) && !preg_match('/^\d{2}:\d{2}$/', $merged['time'])) {
|
||
$merged['time'] = '21:00';
|
||
}
|
||
$data['remind_json'] = json_encode($merged, JSON_UNESCAPED_UNICODE);
|
||
}
|
||
|
||
if ($exist) {
|
||
Db::name('user_setting')->where('id', $exist['id'])->update($data);
|
||
} else {
|
||
$data['user_id'] = $userId;
|
||
$data['createtime'] = time();
|
||
Db::name('user_setting')->insert($data);
|
||
}
|
||
|
||
$this->success('已保存');
|
||
}
|
||
|
||
/**
|
||
* 读取提醒配置:支持 remind_xxx 独立字段(推荐,兼容 request filter)与 remind JSON
|
||
* @return array|null
|
||
*/
|
||
protected function readRemind()
|
||
{
|
||
$keys = ['period', 'ovulation', 'record', 'care', 'time'];
|
||
$result = [];
|
||
$hasField = false;
|
||
foreach ($keys as $key) {
|
||
$value = $this->request->post('remind_' . $key, null);
|
||
if ($value !== null) {
|
||
$hasField = true;
|
||
$result[$key] = $key === 'time' ? (string)$value : (bool)(int)$value;
|
||
}
|
||
}
|
||
if ($hasField) {
|
||
return $result;
|
||
}
|
||
|
||
// 兼容 JSON 传参(注意:全局 filter 会把引号转义,需还原)
|
||
$json = $this->request->post('remind', null);
|
||
if ($json === null || $json === '') {
|
||
return null;
|
||
}
|
||
$arr = is_array($json) ? $json : json_decode(htmlspecialchars_decode((string)$json, ENT_QUOTES), true);
|
||
return is_array($arr) ? $arr : null;
|
||
}
|
||
|
||
/**
|
||
* 自定义提醒列表(M1/M6 智能提醒)
|
||
*/
|
||
public function reminderList()
|
||
{
|
||
$rows = \think\Db::name('reminder')
|
||
->where('user_id', $this->auth->id)
|
||
->whereNull('deletetime')
|
||
->order('enabled desc, remind_time asc')
|
||
->select();
|
||
$this->success('', ['list' => array_map([$this, 'formatReminder'], $rows)]);
|
||
}
|
||
|
||
/**
|
||
* 新增 / 修改提醒
|
||
* POST id, title, type, remind_time, repeat_rule, remind_date, enabled
|
||
*/
|
||
public function reminderSave()
|
||
{
|
||
$userId = $this->auth->id;
|
||
$id = (int)$this->request->post('id', 0);
|
||
$title = mb_substr(trim((string)$this->request->post('title', '')), 0, 60);
|
||
if (!$title) {
|
||
$this->error('请填写提醒内容');
|
||
}
|
||
$time = (string)$this->request->post('remind_time', '21:00');
|
||
if (!preg_match('/^\d{2}:\d{2}$/', $time)) {
|
||
$this->error('提醒时间格式不正确');
|
||
}
|
||
$repeat = $this->request->post('repeat_rule', 'daily');
|
||
if (!in_array($repeat, ['daily', 'once', 'weekly'], true)) {
|
||
$repeat = 'daily';
|
||
}
|
||
$date = (string)$this->request->post('remind_date', '');
|
||
$data = [
|
||
'user_id' => $userId,
|
||
'title' => $title,
|
||
'type' => $this->request->post('type', 'custom'),
|
||
'remind_time' => $time,
|
||
'repeat_rule' => $repeat,
|
||
'remind_date' => $date && strtotime($date) ? $date : null,
|
||
'enabled' => (int)$this->request->post('enabled', 1),
|
||
'updatetime' => time(),
|
||
];
|
||
if ($id) {
|
||
$row = \think\Db::name('reminder')->where('id', $id)->where('user_id', $userId)->find();
|
||
if (!$row) {
|
||
$this->error('提醒不存在');
|
||
}
|
||
\think\Db::name('reminder')->where('id', $id)->update($data);
|
||
} else {
|
||
$data['createtime'] = time();
|
||
$id = \think\Db::name('reminder')->insertGetId($data);
|
||
}
|
||
$this->success('已保存', ['id' => (int)$id]);
|
||
}
|
||
|
||
/**
|
||
* 删除提醒
|
||
*/
|
||
public function reminderRemove()
|
||
{
|
||
$id = (int)$this->request->post('id', 0);
|
||
$row = \think\Db::name('reminder')->where('id', $id)->where('user_id', $this->auth->id)->find();
|
||
if (!$row) {
|
||
$this->error('提醒不存在');
|
||
}
|
||
\think\Db::name('reminder')->where('id', $id)->update(['deletetime' => time()]);
|
||
$this->success('已删除');
|
||
}
|
||
|
||
/**
|
||
* 数据备份:创建快照(本地/云端同步的前提能力)
|
||
*/
|
||
public function backupCreate()
|
||
{
|
||
$userId = $this->auth->id;
|
||
$payload = $this->collectData($userId);
|
||
$json = json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||
$id = \think\Db::name('user_backup')->insertGetId([
|
||
'user_id' => $userId,
|
||
'name' => '备份 ' . date('Y-m-d H:i'),
|
||
'size' => strlen($json),
|
||
'payload' => $json,
|
||
'createtime' => time(),
|
||
]);
|
||
|
||
// 仅保留最近 5 份,避免存储膨胀
|
||
$ids = \think\Db::name('user_backup')
|
||
->where('user_id', $userId)
|
||
->order('id', 'desc')
|
||
->limit(100)
|
||
->column('id');
|
||
if (count($ids) > 5) {
|
||
\think\Db::name('user_backup')->whereIn('id', array_slice($ids, 5))->delete();
|
||
}
|
||
|
||
$this->success('备份完成', [
|
||
'id' => (int)$id,
|
||
'size' => strlen($json),
|
||
'counts' => $payload['counts'],
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 备份列表
|
||
*/
|
||
public function backupList()
|
||
{
|
||
$rows = \think\Db::name('user_backup')
|
||
->where('user_id', $this->auth->id)
|
||
->order('id', 'desc')
|
||
->field('id,name,size,createtime')
|
||
->select();
|
||
$list = array_map(function ($row) {
|
||
return [
|
||
'id' => (int)$row['id'],
|
||
'name' => $row['name'],
|
||
'size' => (int)$row['size'],
|
||
'size_text' => round($row['size'] / 1024, 1) . ' KB',
|
||
'date' => date('Y-m-d H:i', (int)$row['createtime']),
|
||
];
|
||
}, $rows);
|
||
$this->success('', ['list' => $list]);
|
||
}
|
||
|
||
/**
|
||
* 从备份恢复(仅恢复本人数据,先清理同范围记录)
|
||
*/
|
||
public function backupRestore()
|
||
{
|
||
$userId = $this->auth->id;
|
||
$id = (int)$this->request->post('id', 0);
|
||
$row = \think\Db::name('user_backup')->where('id', $id)->where('user_id', $userId)->find();
|
||
if (!$row) {
|
||
$this->error('备份不存在');
|
||
}
|
||
$payload = json_decode($row['payload'], true);
|
||
if (!is_array($payload)) {
|
||
$this->error('备份内容损坏');
|
||
}
|
||
|
||
$restored = [];
|
||
$map = [
|
||
'cyle' => 'cycle',
|
||
];
|
||
$this->restoreTable('cycle', $payload['cycle'] ?? [], $userId, $restored);
|
||
$this->restoreTable('cycle_log', $payload['cycle_log'] ?? [], $userId, $restored);
|
||
$this->restoreTable('mood_log', $payload['mood_log'] ?? [], $userId, $restored);
|
||
$this->restoreTable('bbt_record', $payload['bbt_record'] ?? [], $userId, $restored);
|
||
$this->restoreTable('task', $payload['task'] ?? [], $userId, $restored);
|
||
$this->restoreTable('memory', $payload['memory'] ?? [], $userId, $restored);
|
||
|
||
$this->success('恢复完成', ['restored' => $restored]);
|
||
}
|
||
|
||
protected function collectData($userId)
|
||
{
|
||
$data = [
|
||
'cycle' => \think\Db::name('cycle')->where('user_id', $userId)->whereNull('deletetime')->select(),
|
||
'cycle_log' => \think\Db::name('cycle_log')->where('user_id', $userId)->whereNull('deletetime')->select(),
|
||
'mood_log' => \think\Db::name('mood_log')->where('user_id', $userId)->whereNull('deletetime')->select(),
|
||
'bbt_record' => \think\Db::name('bbt_record')->where('user_id', $userId)->select(),
|
||
'task' => \think\Db::name('task')->where('user_id', $userId)->whereNull('deletetime')->select(),
|
||
'memory' => \think\Db::name('memory')->where('user_id', $userId)->whereNull('deletetime')->select(),
|
||
'reminder' => \think\Db::name('reminder')->where('user_id', $userId)->whereNull('deletetime')->select(),
|
||
'setting' => \think\Db::name('user_setting')->where('user_id', $userId)->find(),
|
||
];
|
||
$data['counts'] = [
|
||
'cycle' => count($data['cycle']),
|
||
'cycle_log' => count($data['cycle_log']),
|
||
'mood_log' => count($data['mood_log']),
|
||
'bbt' => count($data['bbt_record']),
|
||
'task' => count($data['task']),
|
||
'memory' => count($data['memory']),
|
||
];
|
||
$data['version'] = 'v1';
|
||
$data['created'] = date('Y-m-d H:i:s');
|
||
return $data;
|
||
}
|
||
|
||
protected function restoreTable($table, $rows, $userId, &$restored)
|
||
{
|
||
if (!is_array($rows)) {
|
||
return;
|
||
}
|
||
\think\Db::name($table)->where('user_id', $userId)->delete();
|
||
foreach ($rows as $row) {
|
||
unset($row['id']);
|
||
$row['user_id'] = $userId;
|
||
\think\Db::name($table)->insert($row);
|
||
}
|
||
$restored[$table] = count($rows);
|
||
}
|
||
|
||
protected function formatReminder($row)
|
||
{
|
||
return [
|
||
'id' => (int)$row['id'],
|
||
'title' => $row['title'],
|
||
'type' => $row['type'],
|
||
'remind_time' => $row['remind_time'],
|
||
'repeat_rule' => $row['repeat_rule'],
|
||
'remind_date' => $row['remind_date'],
|
||
'enabled' => (int)$row['enabled'],
|
||
];
|
||
}
|
||
}
|