426 lines
15 KiB
PHP
426 lines
15 KiB
PHP
<?php
|
|
|
|
namespace app\api\logic\tcm;
|
|
|
|
use app\common\model\tcm\BloodRecord;
|
|
use app\common\model\tcm\DailyGamify;
|
|
use app\common\model\tcm\DietRecord;
|
|
use app\common\model\tcm\ExerciseRecord;
|
|
|
|
/**
|
|
* 稳糖分 / 勋章 / 浇水领奖
|
|
*/
|
|
class DailyGamifyLogic
|
|
{
|
|
protected static string $error = '';
|
|
|
|
/** @var array<string,array{name:string,points:int}> */
|
|
protected static array $taskDefs = [
|
|
'glucose' => ['name' => '测血糖', 'points' => 10],
|
|
'bp' => ['name' => '测血压', 'points' => 10],
|
|
'diet' => ['name' => '饮食', 'points' => 10],
|
|
'exercise' => ['name' => '运动', 'points' => 10],
|
|
];
|
|
|
|
public static function getError(): string
|
|
{
|
|
return self::$error;
|
|
}
|
|
|
|
protected static function setError(string $msg): bool
|
|
{
|
|
self::$error = $msg;
|
|
return false;
|
|
}
|
|
|
|
protected static function assertOwned(int $userId, int $diagnosisId): bool
|
|
{
|
|
if ($userId <= 0) {
|
|
return self::setError('请先登录') ? false : false;
|
|
}
|
|
if ($diagnosisId <= 0) {
|
|
return self::setError('诊单ID不能为空') ? false : false;
|
|
}
|
|
$owned = \think\facade\Db::name('diagnosis_view_records')
|
|
->where('user_id', $userId)
|
|
->where('diagnosis_id', $diagnosisId)
|
|
->where('delete_time', null)
|
|
->find();
|
|
if (!$owned) {
|
|
return self::setError('无权操作该诊单') ? false : false;
|
|
}
|
|
$diagnosis = \app\common\model\tcm\Diagnosis::where('id', $diagnosisId)
|
|
->where('delete_time', null)
|
|
->field('show_card')
|
|
->find();
|
|
if (!$diagnosis || (int) ($diagnosis['show_card'] ?? 1) !== 1) {
|
|
return self::setError('该就诊卡已在统计端隐藏') ? false : false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
protected static function todayRange(): array
|
|
{
|
|
return [
|
|
strtotime(date('Y-m-d 00:00:00')),
|
|
strtotime(date('Y-m-d 23:59:59')),
|
|
];
|
|
}
|
|
|
|
protected static function hasValue($v): bool
|
|
{
|
|
if ($v === null || $v === '' || $v === '0' || $v === 0) {
|
|
return false;
|
|
}
|
|
if (is_string($v)) {
|
|
return trim($v) !== '';
|
|
}
|
|
return is_numeric($v) && (float) $v > 0;
|
|
}
|
|
|
|
/**
|
|
* 今日任务是否已完成(依据真实业务记录)
|
|
*/
|
|
public static function evaluateTaskCompletion(int $diagnosisId): array
|
|
{
|
|
[$start, $end] = self::todayRange();
|
|
|
|
$blood = BloodRecord::where('diagnosis_id', $diagnosisId)
|
|
->where('source', 1)
|
|
->where('record_date', '>=', $start)
|
|
->where('record_date', '<=', $end)
|
|
->find();
|
|
|
|
$glucoseDone = false;
|
|
$bpDone = false;
|
|
if ($blood) {
|
|
$glucoseDone = self::hasValue($blood['fasting_blood_sugar'] ?? null)
|
|
|| self::hasValue($blood['postprandial_blood_sugar'] ?? null)
|
|
|| self::hasValue($blood['other_blood_sugar'] ?? null);
|
|
$bpDone = self::hasValue($blood['systolic_pressure'] ?? null)
|
|
|| self::hasValue($blood['diastolic_pressure'] ?? null);
|
|
}
|
|
|
|
$diet = DietRecord::where('diagnosis_id', $diagnosisId)
|
|
->where('record_date', '>=', $start)
|
|
->where('record_date', '<=', $end)
|
|
->order('id', 'desc')
|
|
->find();
|
|
|
|
$dietDone = false;
|
|
if ($diet) {
|
|
$dietDone = self::hasValue($diet['breakfast_foods'] ?? null)
|
|
|| self::hasValue($diet['lunch_foods'] ?? null)
|
|
|| self::hasValue($diet['dinner_foods'] ?? null);
|
|
}
|
|
|
|
$exercise = ExerciseRecord::where('diagnosis_id', $diagnosisId)
|
|
->where('record_date', '>=', $start)
|
|
->where('record_date', '<=', $end)
|
|
->order('id', 'desc')
|
|
->find();
|
|
|
|
$exerciseDone = false;
|
|
if ($exercise) {
|
|
$exerciseDone = self::hasValue($exercise['exercise_type'] ?? null)
|
|
|| self::hasValue($exercise['duration'] ?? null);
|
|
}
|
|
|
|
return [
|
|
'glucose' => $glucoseDone,
|
|
'bp' => $bpDone,
|
|
'diet' => $dietDone,
|
|
'exercise' => $exerciseDone,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string,array<string,bool>> $taskAwards
|
|
* @return array<int,array{id:string,name:string,points:int,completed:bool,claimed:bool}>
|
|
*/
|
|
public static function buildTodayTasks(int $diagnosisId, array $taskAwards): array
|
|
{
|
|
$today = date('Y-m-d');
|
|
$awards = isset($taskAwards[$today]) && is_array($taskAwards[$today]) ? $taskAwards[$today] : [];
|
|
$completion = self::evaluateTaskCompletion($diagnosisId);
|
|
$list = [];
|
|
|
|
foreach (self::$taskDefs as $id => $def) {
|
|
$list[] = [
|
|
'id' => $id,
|
|
'name' => $def['name'],
|
|
'points' => $def['points'],
|
|
'completed' => !empty($completion[$id]),
|
|
'claimed' => self::isTaskClaimed($id, $awards, $completion),
|
|
];
|
|
}
|
|
|
|
return $list;
|
|
}
|
|
|
|
/**
|
|
* 任务是否已领取(兼容旧版 blood 合并任务)
|
|
*
|
|
* @param array<string,bool> $awards
|
|
* @param array<string,bool> $completion
|
|
*/
|
|
protected static function isTaskClaimed(string $id, array $awards, array $completion = []): bool
|
|
{
|
|
if (!empty($awards[$id])) {
|
|
return true;
|
|
}
|
|
// 旧版 blood 一次性领取:对应分项当日已有记录则视为已领,避免拆分后重复领奖/轮换引导
|
|
if (!empty($awards['blood'])) {
|
|
if ($id === 'glucose' && !empty($completion['glucose'])) {
|
|
return true;
|
|
}
|
|
if ($id === 'bp' && !empty($completion['bp'])) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** 与前端 tongji/utils/treeLevels.js 保持一致 */
|
|
protected const TREE_MAX_LEVEL = 9;
|
|
protected const TREE_XP_PER_LEVEL = 50;
|
|
|
|
protected static function treeMeta(int $points): array
|
|
{
|
|
$points = max(0, (int) $points);
|
|
$level = min(self::TREE_MAX_LEVEL, (int) floor($points / self::TREE_XP_PER_LEVEL));
|
|
$names = ['种子眠', '破土芽', '展两叶', '小树苗', '青枝繁', '拔节高', '稳糖冠', '初绽香', '漫开花', '圆满树'];
|
|
$xpIn = $level >= self::TREE_MAX_LEVEL ? self::TREE_XP_PER_LEVEL : ($points % self::TREE_XP_PER_LEVEL);
|
|
$progress = $level >= self::TREE_MAX_LEVEL
|
|
? 100
|
|
: (int) round(($xpIn / self::TREE_XP_PER_LEVEL) * 100);
|
|
$nextName = $level < self::TREE_MAX_LEVEL ? ($names[$level + 1] ?? '') : '';
|
|
$pointsToNext = $level >= self::TREE_MAX_LEVEL
|
|
? 0
|
|
: (self::TREE_XP_PER_LEVEL - $xpIn);
|
|
|
|
return [
|
|
'tree_level' => $level,
|
|
'tree_progress' => $progress,
|
|
'tree_level_name' => $names[$level] ?? '种子眠',
|
|
'tree_xp_in_level' => $xpIn,
|
|
'tree_xp_need' => self::TREE_XP_PER_LEVEL,
|
|
'tree_points_next' => $pointsToNext,
|
|
'tree_next_name' => $nextName,
|
|
'tree_is_max' => $level >= self::TREE_MAX_LEVEL,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 获取稳糖乐园状态(含今日任务)
|
|
*/
|
|
public static function getState(int $userId, int $diagnosisId): array|false
|
|
{
|
|
self::$error = '';
|
|
if (!self::assertOwned($userId, $diagnosisId)) {
|
|
return false;
|
|
}
|
|
|
|
$row = DailyGamify::where('diagnosis_id', $diagnosisId)
|
|
->where('user_id', $userId)
|
|
->find();
|
|
|
|
$points = $row ? (int) $row['points'] : 0;
|
|
$badges = $row ? self::decodeJsonArray((string) ($row['badges'] ?? '')) : [];
|
|
$taskAwards = $row ? self::decodeJsonObject((string) ($row['task_awards'] ?? '')) : [];
|
|
|
|
$todayTasks = self::buildTodayTasks($diagnosisId, $taskAwards);
|
|
$claimable = 0;
|
|
foreach ($todayTasks as $t) {
|
|
if ($t['completed'] && !$t['claimed']) {
|
|
$claimable += (int) $t['points'];
|
|
}
|
|
}
|
|
|
|
return array_merge([
|
|
'points' => $points,
|
|
'badges' => $badges,
|
|
'task_awards' => $taskAwards,
|
|
'today_tasks' => $todayTasks,
|
|
'claimable_points' => $claimable,
|
|
], self::treeMeta($points));
|
|
}
|
|
|
|
/**
|
|
* 浇水:领取今日已完成且未领取的任务积分
|
|
*/
|
|
public static function waterTree(int $userId, int $diagnosisId): array|false
|
|
{
|
|
self::$error = '';
|
|
if (!self::assertOwned($userId, $diagnosisId)) {
|
|
return false;
|
|
}
|
|
|
|
$row = DailyGamify::where('diagnosis_id', $diagnosisId)
|
|
->where('user_id', $userId)
|
|
->find();
|
|
|
|
$points = $row ? (int) $row['points'] : 0;
|
|
$badges = $row ? self::decodeJsonArray((string) ($row['badges'] ?? '')) : [];
|
|
$taskAwards = $row ? self::decodeJsonObject((string) ($row['task_awards'] ?? '')) : [];
|
|
|
|
$today = date('Y-m-d');
|
|
$todayTasks = self::buildTodayTasks($diagnosisId, $taskAwards);
|
|
$addedPoints = 0;
|
|
$claimedIds = [];
|
|
$pending = [];
|
|
|
|
if (!isset($taskAwards[$today]) || !is_array($taskAwards[$today])) {
|
|
$taskAwards[$today] = [];
|
|
}
|
|
|
|
foreach ($todayTasks as $task) {
|
|
if ($task['completed'] && !$task['claimed']) {
|
|
$id = (string) $task['id'];
|
|
$taskAwards[$today][$id] = true;
|
|
$addedPoints += (int) $task['points'];
|
|
$claimedIds[] = $id;
|
|
} elseif (!$task['completed']) {
|
|
$pending[] = [
|
|
'id' => $task['id'],
|
|
'name' => $task['name'],
|
|
];
|
|
}
|
|
}
|
|
|
|
if ($addedPoints <= 0) {
|
|
$claimable = 0;
|
|
foreach ($todayTasks as $t) {
|
|
if ($t['completed'] && !$t['claimed']) {
|
|
$claimable += (int) $t['points'];
|
|
}
|
|
}
|
|
$refreshedTasks = self::buildTodayTasks($diagnosisId, $taskAwards);
|
|
return [
|
|
'added_points' => 0,
|
|
'claimed_tasks' => [],
|
|
'claimable_points' => $claimable,
|
|
'pending_tasks' => $pending,
|
|
'points' => $points,
|
|
'badges' => $badges,
|
|
'task_awards' => $taskAwards,
|
|
'today_tasks' => $refreshedTasks,
|
|
'message' => $claimable > 0 ? '请先点击浇水领取积分' : (count($pending) ? '请先完成今日任务再浇水' : '今日奖励已全部领取'),
|
|
] + self::treeMeta($points);
|
|
}
|
|
|
|
$newPoints = $points + $addedPoints;
|
|
$saved = self::saveState($userId, $diagnosisId, $newPoints, $badges, $taskAwards);
|
|
if ($saved === false) {
|
|
return false;
|
|
}
|
|
|
|
$refreshed = self::buildTodayTasks($diagnosisId, $taskAwards);
|
|
|
|
return [
|
|
'added_points' => $addedPoints,
|
|
'claimed_tasks' => $claimedIds,
|
|
'claimable_points' => 0,
|
|
'pending_tasks' => $pending,
|
|
'points' => $newPoints,
|
|
'badges' => $badges,
|
|
'task_awards' => $taskAwards,
|
|
'today_tasks' => $refreshed,
|
|
'message' => "浇水成功,获得 {$addedPoints} 稳糖积分",
|
|
] + self::treeMeta($newPoints);
|
|
}
|
|
|
|
/**
|
|
* @param array<int,string> $badges
|
|
* @param array<string,array<string,bool>> $taskAwards
|
|
*/
|
|
public static function saveState(int $userId, int $diagnosisId, int $points, array $badges, array $taskAwards): array|false
|
|
{
|
|
self::$error = '';
|
|
if (!self::assertOwned($userId, $diagnosisId)) {
|
|
return false;
|
|
}
|
|
|
|
$points = max(0, (int) $points);
|
|
$badges = array_values(array_unique(array_filter(array_map('strval', $badges))));
|
|
if (!is_array($taskAwards)) {
|
|
$taskAwards = [];
|
|
}
|
|
|
|
$now = time();
|
|
$row = DailyGamify::where('diagnosis_id', $diagnosisId)
|
|
->where('user_id', $userId)
|
|
->find();
|
|
|
|
$data = [
|
|
'points' => $points,
|
|
'badges' => json_encode($badges, JSON_UNESCAPED_UNICODE),
|
|
'task_awards' => json_encode($taskAwards, JSON_UNESCAPED_UNICODE),
|
|
'update_time' => $now,
|
|
];
|
|
|
|
if ($row) {
|
|
DailyGamify::where('id', (int) $row['id'])->update($data);
|
|
} else {
|
|
$data['diagnosis_id'] = $diagnosisId;
|
|
$data['user_id'] = $userId;
|
|
$data['create_time'] = $now;
|
|
DailyGamify::create($data);
|
|
}
|
|
|
|
return [
|
|
'points' => $points,
|
|
'badges' => $badges,
|
|
'task_awards' => $taskAwards,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 本地缓存迁到服务端:取 points/badges/task_awards 的较大合并
|
|
*/
|
|
public static function mergeFromClient(int $userId, int $diagnosisId, int $points, array $badges, array $taskAwards): array|false
|
|
{
|
|
$server = self::getState($userId, $diagnosisId);
|
|
if ($server === false) {
|
|
return false;
|
|
}
|
|
|
|
$mergedPoints = max((int) $server['points'], max(0, $points));
|
|
$mergedBadges = array_values(array_unique(array_merge($server['badges'], $badges)));
|
|
$mergedAwards = $server['task_awards'];
|
|
foreach ($taskAwards as $date => $tasks) {
|
|
if (!is_array($tasks)) {
|
|
continue;
|
|
}
|
|
if (!isset($mergedAwards[$date]) || !is_array($mergedAwards[$date])) {
|
|
$mergedAwards[$date] = [];
|
|
}
|
|
foreach ($tasks as $taskId => $flag) {
|
|
if ($flag) {
|
|
$mergedAwards[$date][(string) $taskId] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return self::saveState($userId, $diagnosisId, $mergedPoints, $mergedBadges, $mergedAwards);
|
|
}
|
|
|
|
protected static function decodeJsonArray(string $json): array
|
|
{
|
|
if ($json === '') {
|
|
return [];
|
|
}
|
|
$data = json_decode($json, true);
|
|
return is_array($data) ? array_values(array_map('strval', $data)) : [];
|
|
}
|
|
|
|
protected static function decodeJsonObject(string $json): array
|
|
{
|
|
if ($json === '') {
|
|
return [];
|
|
}
|
|
$data = json_decode($json, true);
|
|
return is_array($data) ? $data : [];
|
|
}
|
|
}
|