Files
zyt/server/app/api/logic/tcm/DailyGamifyLogic.php
T
2026-05-27 15:50:54 +08:00

374 lines
12 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 = [
'blood' => ['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;
}
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();
$bloodDone = false;
if ($blood) {
$bloodDone = self::hasValue($blood['fasting_blood_sugar'] ?? null)
|| self::hasValue($blood['postprandial_blood_sugar'] ?? null)
|| self::hasValue($blood['other_blood_sugar'] ?? null)
|| 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 [
'blood' => $bloodDone,
'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' => !empty($awards[$id]),
];
}
return $list;
}
protected static function treeMeta(int $points): array
{
$level = min(4, (int) floor($points / 100));
$names = ['嫩芽期', '成长雏形', '枝繁叶茂', '含苞待放', '花开稳糖'];
return [
'tree_level' => $level,
'tree_progress' => $level >= 4 ? 100 : ($points % 100),
'tree_level_name' => $names[$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 : [];
}
}