常见的数据列表查询:同时支持置顶、锁定位置、移动排序、分页的实现逻辑

2023-09-05 16:18:51 浏览数 (2)

需求描述

  1. 假设有个操作后台,可以获取某个分类下的所有数据列表
  2. 针对当前这个分类的列表,可以进行如下操作:置顶、锁定在当前位置、拖动排序(锁定的不可改变排序、如果是置顶的,必须同为置顶的数据)

实现逻辑

每页动态算出offset,然后替换,分页就实现了锁定

逻辑: 假设每页50条数据,查出当页的lock值的数据,比如第一页就是lock为1-50的,第二页就是lock为51-100的,同时查出前面几页所有的lock数据的数量,这个数量就是当前页需要往前偏移的offset,根据这个offset获取的列表,进行当前页有lock的进行替换。这样满足任意一页的数据。

算法:

  • 当前页lock值 = (page-1)limit ~ pagelimit (即需要替换到list的数据)
  • 当前页前面所有的lock值为 1 ~ (page-1)*limit,查出数据数量n,这个n就是要偏移的值,第一页是0就不查了,少一次请求
  • 当前列表的数据list = offset((page-1)*limit - n)->limit()

示例: 第一页,查出所有lock为0的正常排序的数据列表等待替换,此时n值为0,offset为(0-0=0),需要替换的数据是所有lock值为1-50的,从上往下塞入list中。

第二页,查出所有lock值是前面的数据数量n,即lock=1-50的数据数量,n即为需要往前的偏移量,offset=(page-1)limit-n=50-n,根据这个offset查出所有lock为0的第二页正常排序数据等待替换,查出lock值为第二页的即50-100,从上往下塞入list中。lock值需要减去(page-1)limit即50, 按照这个计算后的顺序塞入list。

第三页,查出所有lock值是前面的数据数量n,即lock=1-100的数据数量,n即为需要往前的偏移量,offset=(page-1)limit-n=100-n,根据这个offset查出所有lock为0的第三页正常排序数据等待替换,查出lock值为第三页的即100-150,从上往下塞入list中。lock值需要减去(page-1)limit即100, 按照这个计算后的顺序塞入list。

示例代码

代码语言:javascript复制
    /**
     * 分类下的问题列表
     *
     * @param QuestionType $questionType
     * @param $questionId
     * @param $questionTitle
     * @param $limit
     * @param $page
     * @param $isAdmin
     * @param $hasAnswer
     * @return array
     */
    public function getTypeQuestions(QuestionType $questionType, $questionId, $questionTitle, $limit, $page, $isAdmin, $hasAnswer = false)
    {
        // 总数
        $count = $this->getTypeQuestionsCount($questionType, $questionId, $questionTitle, $isAdmin, $hasAnswer);

        // 当前页的lock值 双边包括 即将要进行替换到列表的数据
        $currLockStart = ($page - 1) * $limit   1;
        $currLockEnd = $page * $limit;
        $currLockList = $questionType->questionsLock($currLockStart, $currLockEnd, $isAdmin, $hasAnswer)->get()->toArray();  // 1-50  51-100

        // 偏移量值 前面所有lock值的数据数量n page=1时为0减少一次请求
        $preLockStart = 1;
        $preLockEnd = ($page - 1) * $limit;
        $n = $preLockEnd == 0 ? 0 : $questionType->questionsLock($preLockStart, $preLockEnd, $isAdmin, $hasAnswer)->count();  // 1-0 1-50 1-100 的数据条目

        // 动态计算偏移量offset
        $offset = $limit * ($page - 1) - $n;

        // 为替换之前的数据列表
        $list = $questionType->questions($questionId, $questionTitle, $isAdmin, $hasAnswer)
            ->offset($offset)
            ->limit($limit)
            ->get()->toArray();

        // 指定插入 (当搜索时不处理位置锁定)
        if (!$questionId && !$questionTitle) {
            foreach ($currLockList as $item) {
                array_splice($list, $item['lock'] - $preLockEnd - 1, 0, [$item]);
            }
        }

        // 截取
        $data['total'] = $count;
        $data['data'] = array_slice($list, 0, $limit);
        return $data;
    }

    /**
     * 获取某一lock区间的question列表数据
     *
     * @param $lockStart
     * @param $lockEnd
     * @param bool $isAdmin
     * @param bool $hasAnswer
     * @return IlluminateDatabaseEloquentRelationsBelongsToMany
     */
    public function questionsLock($lockStart, $lockEnd, $isAdmin = true, $hasAnswer = false)
    {
        $table = (new QuestionTypeRelation())->getTable();
        $filed = $this->handleCommonRelationField($isAdmin);

        return $this->belongsToMany(Question::class, $table, 'question_type_id', 'question_id')
            ->select($filed)
            ->with('user')
            ->where($table.'.lock', '>=', $lockStart)

            ->where('questions.audit_status', 1)
//            ->when(!$isAdmin, function ($query) {
//                return $query->where('questions.audit_status', 1);
//            })

            ->when($hasAnswer, function ($query) {
                return $query->where('questions.answer_count', '>' ,0);
            })
            ->where($table.'.lock', '<=', $lockEnd)
            // ->withTrashed()
            ->orderBy($table.'.lock');
    }

    /**
     * 通用模型 获取某个type下的question列表数据
     *
     * @param $questionId
     * @param $questionTitle
     * @param bool $isAdmin
     * @param bool $hasAnswer
     * @return IlluminateDatabaseConcernsBuildsQueries|IlluminateDatabaseEloquentRelationsBelongsToMany|mixed
     */
    public function questions($questionId, $questionTitle, $isAdmin = true, $hasAnswer = false)
    {
        $table = (new QuestionTypeRelation())->getTable();
        $filed = $this->handleCommonRelationField($isAdmin);

        $dbQuery = $this->belongsToMany(Question::class, $table, 'question_type_id', 'question_id')
            ->select($filed)
            ->with('user')
            // ->withTrashed()
            ->when($questionId, function ($query) use ($questionId) {
                return $query->where('questions.id', $questionId);
            })

            ->where('questions.audit_status', 1)
//            ->when(!$isAdmin, function ($query) {
//                return $query->where('questions.audit_status', 1);
//            })

            ->when($hasAnswer, function ($query) {
                return $query->where('questions.answer_count', '>' ,0);
            })
            ->when($questionTitle, function ($query) use ($questionTitle) {
                return $query->where('questions.title', 'like', sprintf('%%%s%%', $questionTitle));
            });

        // 没有任何搜索条件的时候,只查lock=0的,动态计算limit替换数据
        if (!$questionId && !$questionTitle) {
            $dbQuery->where($table.'.lock', 0);
        }

        return $dbQuery->orderByDesc($table.'.top')
        ->orderByDesc($table.'.sort');
    }
代码语言:javascript复制
    /**
     * 置顶 取消置顶 移除
     *
     * @param QuestionType $questionType
     * @param $params
     * @return string
     */
    public function questionsAction(QuestionType $questionType, $params)
    {
        $message = '';
        switch ($params['action']) {
            case 'del':
                QuestionTypeRelation::where(['question_id' => $params['question_id'], 'question_type_id' => $questionType->id])->delete();
                $message = '移除成功';
                OperationLog::writeFeedLog(13, 50, $params['question_id']);
                break;
            case 'top':
                $item = QuestionTypeRelation::where(['question_id' => $params['question_id'], 'question_type_id' => $questionType->id])->first();
                if ($item) {
                    if ($item->top == 0) {
                        $res = QuestionTypeRelation::where('question_type_id', $questionType->id)->orderByDesc('top')->first();
                        $top = $res ? $res->top   1 : 1;
                        $item->top = $top;
                        $item->save() && OperationLog::writeFeedLog(13, 12, $params['question_id']);
                        $message = '置顶成功';
                    } else {
                        $item->top = 0;
                        $item->save() && OperationLog::writeFeedLog(13, 13, $params['question_id']);
                        $message = '已取消置顶';
                    }
                }
                break;
            default:
                break;
        }
        return $message;
    }

    /**
     * 锁定 取消锁定
     *
     * @param QuestionType $questionType
     * @param $params
     * @return string
     */
    public function questionsLock(QuestionType $questionType, $params)
    {
        $message = '';
        $item = QuestionTypeRelation::where(['question_id' => $params['question_id'], 'question_type_id' => $questionType->id])->first();
        if ($item) {
            if ($params['index'] == 0) {
                $item->lock = $params['index'];
                $item->save() && OperationLog::writeFeedLog(13, 49, $params['question_id']);
                $message = '已取消锁定';
            } else {
                // 避免锁定值重复导致数据丢失 避免重复锁定同一个值来减少请求
                if ($params['index'] == $item->lock) {
                    $message = '锁定成功';
                } else {
                    $count = QuestionTypeRelation::where(['lock' => $params['index'], 'question_type_id' => $questionType->id])->count();
                    if ($count) {
                        $message = '当前位置已有锁定数据,请先取消锁定后再进行设置';
                    } else {
                        $item->lock = $params['index'];
                        $item->save() && OperationLog::writeFeedLog(13, 48, $params['question_id']);
                        $message = '锁定成功';
                    }
                }
            }
        }
        return $message;
    }

    /**
     * @param QuestionType $questionType
     * @param $params
     * @return string
     */
    public function questionsSort(QuestionType $questionType, $params)
    {
        $message = '';
        $currItem = QuestionTypeRelation::where(['question_id' => $params['curr_question_id'], 'question_type_id' => $questionType->id])->first();
        $targetItem = QuestionTypeRelation::where(['question_id' => $params['target_question_id'], 'question_type_id' => $questionType->id])->first();

        if ($currItem && $targetItem) {
            switch ($params['is_top']) {
                case 1:
                    // 必须同为置顶数据
                    if ($currItem->top && $targetItem->top) {
                        $message = $this->dragSort($questionType->id, $params['action'], $currItem, $targetItem, true);
                    } else {
                        $message = '必须同为置顶数据';
                    }
                    break;
                case 0:
                    // 必须都没有置顶
                    if (!$currItem->top && !$targetItem->top) {
                        $message = $this->dragSort($questionType->id, $params['action'], $currItem, $targetItem, false);
                    } else {
                        $message = '必须同为非置顶数据';
                    }
                    break;
                default:
                    break;
            }
        }
        return $message;
    }

    /**
     * @param $questionTypeId
     * @param $action
     * @param QuestionTypeRelation $currItem
     * @param QuestionTypeRelation $targetItem
     * @param false $isTop
     * @return string
     */
    private function dragSort($questionTypeId, $action, QuestionTypeRelation $currItem, QuestionTypeRelation $targetItem, $isTop = false)
    {
        if ($isTop) {
            $filed = 'top';
        } else {
            $filed = 'sort';
        }

        $message = '';
        $oldCurrSort = (int) $currItem->$filed;
        $oldTargetSort = (int) $targetItem->$filed;

        switch ($action) {
            case 'up':
                // 移动到谁上面
                if ($oldCurrSort >= $oldTargetSort) {
                    $message = '上移参数错误,无法将上方数据上移置下方数据前,请尝试下移动作';
                } else {
                    QuestionTypeRelation::where('question_type_id', $questionTypeId)
                        ->where($filed, '>', $oldCurrSort)
                        ->where($filed, '<=', $oldTargetSort)
                        ->where($filed, '!=', 0)
                        ->update([$filed => DB::raw($filed . ' - 1')]);

                    $currItem->$filed = $oldTargetSort;
                    $currItem->save() && OperationLog::writeFeedLog(13, 11, $currItem->question_id);
                    $message = '调序成功';
                }
                break;
            case 'down':
                // 移动到谁下面
                if ($oldCurrSort <= $oldTargetSort) {
                    $message = '下移参数错误,无法将下方数据下移置上方数据后,请尝试上移动作';
                } else {
                    QuestionTypeRelation::where('question_type_id', $questionTypeId)
                        ->where($filed, '<', $oldCurrSort)
                        ->where($filed, '>=', $oldTargetSort)
                        ->where($filed, '!=', 0)
                        ->update([$filed => DB::raw($filed . '   1')]);

                    $currItem->$filed = $oldTargetSort;
                    $currItem->save() && OperationLog::writeFeedLog(13, 11, $currItem->question_id);
                    $message = '调序成功';
                }
                break;
            default:
                break;
        }
        return $message;
    }

    /**
     * 更新问题分类关系
     * 维护分类关系的更新,原本的sort不变,新增的才用id,防止拖动排序后的sort被重新赋值成ID
     *
     * @param Question $question
     * @param $questionType
     */
    private function handleEditQuestionRelation(Question $question, $questionType)
    {
        $sync = [];
        $relations = QuestionTypeRelation::where('question_id', $question->id)->get();
        $currTypeIds = $relations->pluck('question_type_id')->toArray();

        $intersects = array_intersect($currTypeIds, $questionType);
        $diffs = array_diff($questionType, $currTypeIds);
        foreach ($intersects as $intersect) {
            foreach ($relations as $relation) {
                if ($relation->question_type_id == $intersect) {
                    $sync[$intersect] = ['sort' => $relation->sort];
                }
            }
        }
        foreach ($diffs as $diff) {
            $sync[$diff] = ['sort' => $question->id];
        }
        $question->questionTypes()->sync($sync);
    }

    /**
     * 问题审核操作日志
     *
     * @param $questionId
     * @param $auditStatus
     */
    private function handleQuestionAuditLogs($questionId, $auditStatus)
    {
        // 添加日志
        $opAction = 0;
        switch ($auditStatus) {
            case 0:
                $opAction = 43;
                break;
            case 1:
                $opAction = 5;
                break;
            case 2:
                $opAction = 6;
                break;
        }
        OperationLog::writeFeedLog(10, $opAction, $questionId);
    }

    /**
     * 审核操作系统通知
     *
     * @param Question $question
     * @param $auditStatus
     */
    private function handleQuestionAuditNotice(Question $question, $auditStatus)
    {
        // 系统通知
        $notice_type = Notice::NOTICE_SYSTEM_QUESTION_AUDIT;
        $toUser = '';
        switch ($auditStatus) {
            case 0:
                break;
            case 1:
                $toUser = '【你于' . (new Times())->utcChangeToPrc($question->created_at) . '提出的标题为“' . $question->title . '”的问题已通过审核】';
                break;
            case 2:
                $toUser = '【你于' . (new Times())->utcChangeToPrc($question->created_at) . '提出的标题为“' . $question->title . '”的问题审核不通过,已删除】';
                break;
        }
        if (!empty($toUser)) {
            (new Notice())->system($question->user_id, $toUser, $notice_type);
        }
    }

    /**
     * 根据id或标题查找问题
     *
     * @param array $params
     * @return mixed
     */
    public function query($params)
    {
        if (!empty($params['light'])) {
            $columns = ['id', 'user_id', 'summary'];
        } else {
            $columns = ['id', 'user_id', 'title', 'summary', 'images', 'video', 'created_at'];
        }

        $questions = Question::query()
            ->with(['user'])
            ->when(!empty($params['id']), function ($query) use ($params) {
                $id = array_values(
                    array_filter(
                        explode(',', $params['id'])
                    )
                );
                return $query->whereIn('id', $id);
            })
            ->when(!empty($params['title']), function ($query) use ($params) {
                return $query->where('title', 'like', sprintf('%%%s%%', $params['title']));
            })
            ->where('audit_status', Question::AUDIT_STATUS_PASS)
            ->get($columns)
            ->toArray();

        return $questions ?: [];
    }

    /**
     * 查询简单版
     *
     * @param array  $ids
     * @return mixed
     */
    public function querySimple($ids)
    {
        $questions = Question::query()
            ->with(['user'])
            ->whereIn('id', $ids)
            ->where('audit_status', Question::AUDIT_STATUS_PASS)
            ->get(['id', 'user_id', 'images', 'video'])
            ->toArray();

        return $questions ?: [];
    }

    /**
     * 处理最佳答案
     *
     * @param $response
     * @return mixed
     */
    public static function handleBestAnswer($response)
    {
        $questionIds = [];
        foreach ($response['data'] as $item) {
            $questionIds[] = $item['id'];
        }

        if (count($questionIds)) {
            $allAnswers = self::getBestAnswer($questionIds);
            foreach ($response['data'] as &$res) {
                $res['best_answer'] = null;
                foreach ($allAnswers as $allAnswer) {
                    if ($res['id'] == $allAnswer['question_id']) {
                        $res['best_answer'] = $allAnswer;
                    }
                }
            }
        }
        return $response;
    }

    /**
     * @param $response
     * @return array
     */
    public static function handleFlycardFormat($response)
    {
        $res = [];
        $res['total'] = $response['total'];
        $appId = Question::APPID_QUESTION;
        foreach ($response['data'] as $datum) {
            $temp['appid'] = $appId;
            $temp['title'] = $datum['title'];
            $temp['extra']['question'] = [
                'id' => $datum['id'],
                'content' => $datum['summary'],
                'images' => $datum['images'],
                'video' => $datum['video'],
                'user' => $datum['user']
            ];

            if ($datum['best_answer']) {
                $temp['extra']['answer'] = [
                    'id' => $datum['best_answer']['id'],
                    'content' => $datum['best_answer']['summary'],
                    'images' => $datum['best_answer']['images'],
                    'video' => $datum['best_answer']['video'],
                    'user' => $datum['best_answer']['user']
                ];
            }
            $res['data'][] = $temp;
        }
        return $res;
    }

    /**
     * pgc接口
     * 获取当前question的额外数据 [question_id, answer_id, answer_count, best_answer_like_count]
     *
     * @param $questionId
     * @param $memberId
     * @return array
     */
    public static function getPgcQuestionData($questionId, $memberId)
    {
        $res = [];
        $question = Question::find($questionId);
        if ($question) {
            $answerCount = $question->answer_count;
            $bestAnswer = self::getBestAnswer([$questionId]);
            if ($bestAnswer->count()) {
                // 查答案点赞状态
                $hasLiked = false;
                if ($memberId) {
                    $user = UserModel::where('member_id', $memberId)->first('id');
                    $hasLiked = Answer::find($bestAnswer[0]->id)->liked($user->id);
                }
                $res = [
                    'question_id' => $questionId,
                    'answer_id' => $bestAnswer[0]->id,
                    'answer_count' => $answerCount,
                    'best_answer_like_count' => $bestAnswer[0]->like_count,
                    'has_liked' => $hasLiked,
                ];
            }
        }
        return $res;
    }

    /**
     * 封装通用获取最佳答案数据
     *
     * @param $questionIds
     * @return array|IlluminateHttpResourcesJsonAnonymousResourceCollection
     */
    private static function getBestAnswer(array $questionIds)
    {
        $allAnswers = [];
        $topAnswer = Answer::query()
            ->select('id', 'question_id')
            ->when($questionIds, function ($query) use ($questionIds) {
                return $query->whereIn('question_id', $questionIds);
            })
            ->where('audit_status', Answer::AUDIT_STATUS_PASS)
            ->where('sort', 1)
            ->pluck('id', 'question_id')->toArray();

        // 有置顶答案的问题ID、答案ID
        $topQuestionIds = array_keys($topAnswer);
        $topAnswerIds = array_values($topAnswer);

        // 没有置顶的问题的答案ID,以及相同排序的时候最大id值
        $normalQuestionIds = array_diff($questionIds, $topQuestionIds);
        $normalAnswerIds = [];
        if (count($normalQuestionIds)) {
            $idStr = implode(',', $normalQuestionIds);
            $sql = '
                    SELECT
                     a1.id,
                     a1.question_id,
                     a2.max_id
                    FROM
                        answers a1
                    JOIN (
                        SELECT
                            question_id,
                            max(view_count   like_count) AS hot_count,max(id) as max_id
                        FROM
                            answers
                        WHERE
                            question_id IN (' . $idStr . ')
                        AND
                            audit_status = 1
                        AND
                            deleted_at IS NULL
                        GROUP BY
                            question_id
                    ) a2 ON a1.question_id = a2.question_id
                    AND (
                        a1.view_count   a1.like_count
                    ) = a2.hot_count

                    WHERE
                        a1.question_id IN (' . $idStr . ')
                    AND
                        audit_status = 1
                    AND
                        deleted_at IS NULL
                    GROUP BY
                      a1.question_id
';
            $normalAnswer = DB::select($sql);

            // 当热度值一样的时候,会有问题,需要判断max_id的hot_count是否等于id的hot_count,相等则取max_id
            // 概率很小,建议省点资源,不然还需要每个不同的ID查询两次
            foreach ($normalAnswer as $normal) {
                $normalAnswerIds[] = $normal->id;
            }
        }
        $allAnswerIds = array_merge($normalAnswerIds, $topAnswerIds);
        $allAnswers = Answer::with('user')
            ->select(['id', 'question_id', 'user_id', 'images', 'video', 'summary', 'audit_status', 'like_count', 'view_count', 'comment_count'])
            ->whereIn('id', $allAnswerIds)
            ->get();

        $allAnswers = AnswerResource::collection($allAnswers);

        return $allAnswers;
    }

0 人点赞