request->get('keyword', '')); $dateRange = trim((string) $this->request->get('date_range', 'all')); $likesSort = trim((string) $this->request->get('likes_sort', 'all')); $categories = trim((string) $this->request->get('categories', '')); $catArr = array_values(array_filter(array_map('trim', explode(',', $categories)))); $q = Db::name('work')->alias('w') ->leftJoin('mp_user u', 'u.id = w.user_id') ->whereRaw('LOWER(TRIM(COALESCE(w.status, \'\'))) = ?', ['approved']) ->field('w.*,u.nickname as author_name'); if ($keyword !== '') { $q->whereLike('w.title|w.description', '%' . addcslashes($keyword, '%_\\') . '%'); } if ($dateRange !== '' && $dateRange !== 'all') { $today = date('Y-m-d'); $weekStartTs = strtotime('monday this week'); if ($weekStartTs === false) { $weekStartTs = strtotime($today) ?: time(); } $weekStart = date('Y-m-d', $weekStartTs); $monthStart = date('Y-m-01'); $yearStart = date('Y-01-01'); // 与前端卡片的 date 字段保持一致:优先 publish_date,缺失时回落 create_time 的日期部分 $dateExpr = "DATE(COALESCE(NULLIF(TRIM(w.publish_date), ''), w.create_time))"; switch ($dateRange) { case 'today': $q->whereRaw($dateExpr . ' >= ?', [$today]); break; case 'week': $q->whereRaw($dateExpr . ' >= ?', [$weekStart]); break; case 'month': $q->whereRaw($dateExpr . ' >= ?', [$monthStart]); break; case 'year': $q->whereRaw($dateExpr . ' >= ?', [$yearStart]); break; } } if ($catArr !== []) { $workIds = Db::name('work_category')->whereIn('category', $catArr)->group('work_id')->column('work_id'); if ($workIds === []) { return json(['code' => 0, 'msg' => 'ok', 'data' => ['list' => []]]); } $q->whereIn('w.id', $workIds); } // 赞数相同时:发布时间/创建时间越晚越靠前(再按 id 降序兜底) if ($likesSort === 'desc') { $q->order('w.likes_count', 'desc') ->order('w.create_time', 'desc') ->order('w.id', 'desc'); } elseif ($likesSort === 'asc') { $q->order('w.likes_count', 'asc') ->order('w.create_time', 'desc') ->order('w.id', 'desc'); } else { // 瀑布流默认:仅按创建时间倒序(如 4月1日 在前、3月31日 在后) $q->order('w.create_time', 'desc')->order('w.id', 'desc'); } $rows = $q->select()->toArray(); $mp = $this->optionalMpUser(); $list = $this->toCardList($rows, $mp ? (int) $mp['id'] : 0); return json(['code' => 0, 'msg' => 'ok', 'data' => ['list' => $list]]); } /** 星空榜:仅按赞数,不受 likes_sort 参数影响 */ public function rank() { $rows = Db::name('work')->alias('w') ->leftJoin('mp_user u', 'u.id = w.user_id') ->whereRaw('LOWER(TRIM(COALESCE(w.status, \'\'))) = ?', ['approved']) ->field('w.*,u.nickname as author_name') ->order('w.likes_count', 'desc') ->order('w.create_time', 'desc') ->order('w.id', 'desc') ->select() ->toArray(); $mp = $this->optionalMpUser(); $list = $this->toCardList($rows, $mp ? (int) $mp['id'] : 0); return json(['code' => 0, 'msg' => 'ok', 'data' => ['list' => $list]]); } public function detail(int $id) { $mp = $this->optionalMpUser(); $w = Db::name('work')->alias('w') ->leftJoin('mp_user u', 'u.id = w.user_id') ->where('w.id', $id) ->field('w.*,u.nickname as author_name') ->find(); if (!$w) { return json(['code' => 404, 'msg' => '作品不存在', 'data' => []]); } $mpId = $mp ? (int) $mp['id'] : 0; // 已通过:任何人可看(与首页列表一致);未通过仅作者本人可看 if (!$this->workRowIsApproved($w) && (int) $w['user_id'] !== $mpId) { $st = strtolower(trim((string) ($w['status'] ?? ''))); $msg = match ($st) { 'pending' => '作品审核中,请使用提交该作品的账号登录后再查看详情', 'rejected' => '作品未通过审核,请使用提交该作品的账号登录后查看', default => '作品不可见', }; return json(['code' => 403, 'msg' => $msg, 'data' => []]); } $imgRows = Db::name('work_image') ->where('work_id', $id) ->order('sort', 'asc') ->order('id', 'asc') ->column('url'); $cats = Db::name('work_category')->where('work_id', $id)->column('category'); $liked = false; if ($mpId > 0) { $liked = Db::name('work_like')->where('work_id', $id)->where('user_id', $mpId)->count() > 0; } $pub = $w['publish_date'] ?: date('Y-m-d', strtotime((string) $w['create_time'])); $cover = trim((string) ($w['cover_image'] ?? '')); /** 与首页封面一致:封面图放在第一位,再按排序接其余图并去重 */ $urls = []; foreach ($imgRows as $u) { $u = trim((string) $u); if ($u !== '') { $urls[] = $u; } } if ($cover !== '') { $urls = array_values(array_unique(array_merge([$cover], $urls))); } $stNorm = strtolower(trim((string) ($w['status'] ?? ''))); $data = [ 'id' => (int) $w['id'], 'userId' => (int) $w['user_id'], 'imageUrl' => $cover, 'images' => $urls !== [] ? $urls : ($cover !== '' ? [$cover] : []), 'title' => (string) ($w['title'] ?? ''), 'description' => (string) ($w['description'] ?? ''), 'likes' => (int) $w['likes_count'], 'author' => $w['author_name'] ? (string) $w['author_name'] : '用户', 'date' => $pub, 'shootDate' => $w['shoot_date'] ? (string) $w['shoot_date'] : '', 'categories' => $cats, 'liked' => $liked, 'status' => (string) ($w['status'] ?? ''), ]; if ($stNorm === 'rejected' && $mpId > 0 && (int) $w['user_id'] === $mpId) { $data['rejectReason'] = trim((string) ($w['reject_reason'] ?? '')); } return json(['code' => 0, 'msg' => 'ok', 'data' => $data]); } public function create() { $user = AuthContext::mpUser(); if (!$user) { return json(['code' => 401, 'msg' => '未授权', 'data' => []]); } $title = trim((string) $this->request->post('title', '')); $description = trim((string) $this->request->post('description', '')); $shootDate = trim((string) $this->request->post('shoot_date', '')); $categories = $this->request->post('categories', []); $imageUrls = $this->request->post('image_urls', []); if (!is_array($categories)) { $categories = []; } if (!is_array($imageUrls)) { $imageUrls = []; } if ((int) ($user['status'] ?? 1) !== 1) { return json(['code' => 403, 'msg' => '账号已被禁用', 'data' => []]); } if (mb_strlen($title) < 1 || mb_strlen($title) > 10) { return json(['code' => 400, 'msg' => '标题限 1~10 字', 'data' => []]); } if (mb_strlen($description) < 1 || mb_strlen($description) > 100) { return json(['code' => 400, 'msg' => '描述限 1~100 字', 'data' => []]); } if ($categories === []) { return json(['code' => 400, 'msg' => '请选择作品类别', 'data' => []]); } $allowedCat = ['landscape', 'humanity', 'ecology', 'other']; foreach ($categories as $c) { if (!is_string($c) || !in_array($c, $allowedCat, true)) { return json(['code' => 400, 'msg' => '作品类别须为:风景、人文、生态、其他', 'data' => []]); } } if (count($imageUrls) < 1 || count($imageUrls) > 3) { return json(['code' => 400, 'msg' => '请上传 1~3 张图片', 'data' => []]); } $now = date('Y-m-d H:i:s'); $sd = null; if ($shootDate !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $shootDate)) { $sd = $shootDate; } $cover = (string) $imageUrls[0]; $wid = Db::name('work')->insertGetId([ 'user_id' => (int) $user['id'], 'title' => $title, 'description' => $description, 'location' => '', 'shoot_date' => $sd, 'status' => 'pending', 'sort_order' => 9999, 'recommended' => 0, 'views' => 0, 'likes_count' => 0, 'comments_count' => 0, 'cover_image' => $cover, 'camera' => '', 'lens' => '', 'exif_settings' => '', 'publish_date' => null, 'create_time' => $now, 'update_time' => $now, ]); foreach ($categories as $c) { if (!is_string($c) || $c === '') { continue; } Db::name('work_category')->insert([ 'work_id' => $wid, 'category' => $c, ]); } foreach ($imageUrls as $i => $url) { if (!is_string($url) || $url === '') { continue; } Db::name('work_image')->insert([ 'work_id' => $wid, 'url' => $url, 'sort' => (int) $i, ]); } return json(['code' => 0, 'msg' => '提交成功,请等待审核', 'data' => ['id' => $wid]]); } /** 已拒绝作品修改后重新提交,进入待审核 */ public function resubmit(int $id) { $user = AuthContext::mpUser(); if (!$user) { return json(['code' => 401, 'msg' => '未授权', 'data' => []]); } $w = Db::name('work')->where('id', $id)->find(); if (!$w || (int) $w['user_id'] !== (int) $user['id']) { return json(['code' => 404, 'msg' => '作品不存在', 'data' => []]); } if (strtolower(trim((string) ($w['status'] ?? ''))) !== 'rejected') { return json(['code' => 400, 'msg' => '仅已拒绝的作品可修改后重新提交', 'data' => []]); } $title = trim((string) $this->request->post('title', '')); $description = trim((string) $this->request->post('description', '')); $shootDate = trim((string) $this->request->post('shoot_date', '')); $categories = $this->request->post('categories', []); $imageUrls = $this->request->post('image_urls', []); if (!is_array($categories)) { $categories = []; } if (!is_array($imageUrls)) { $imageUrls = []; } if ((int) ($user['status'] ?? 1) !== 1) { return json(['code' => 403, 'msg' => '账号已被禁用', 'data' => []]); } if (mb_strlen($title) < 1 || mb_strlen($title) > 10) { return json(['code' => 400, 'msg' => '标题限 1~10 字', 'data' => []]); } if (mb_strlen($description) < 1 || mb_strlen($description) > 100) { return json(['code' => 400, 'msg' => '描述限 1~100 字', 'data' => []]); } if ($categories === []) { return json(['code' => 400, 'msg' => '请选择作品类别', 'data' => []]); } $allowedCat = ['landscape', 'humanity', 'ecology', 'other']; foreach ($categories as $c) { if (!is_string($c) || !in_array($c, $allowedCat, true)) { return json(['code' => 400, 'msg' => '作品类别须为:风景、人文、生态、其他', 'data' => []]); } } if (count($imageUrls) < 1 || count($imageUrls) > 3) { return json(['code' => 400, 'msg' => '请上传 1~3 张图片', 'data' => []]); } $now = date('Y-m-d H:i:s'); $sd = null; if ($shootDate !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $shootDate)) { $sd = $shootDate; } $cover = (string) $imageUrls[0]; Db::transaction(function () use ($id, $title, $description, $sd, $cover, $categories, $imageUrls, $now): void { Db::name('work_category')->where('work_id', $id)->delete(); Db::name('work_image')->where('work_id', $id)->delete(); Db::name('work')->where('id', $id)->update([ 'title' => $title, 'description' => $description, 'shoot_date' => $sd, 'status' => 'pending', 'reject_reason' => '', 'cover_image' => $cover, 'sort_order' => 9999, 'publish_date' => null, 'update_time' => $now, ]); foreach ($categories as $c) { if (!is_string($c) || $c === '') { continue; } Db::name('work_category')->insert([ 'work_id' => $id, 'category' => $c, ]); } foreach ($imageUrls as $i => $url) { if (!is_string($url) || $url === '') { continue; } Db::name('work_image')->insert([ 'work_id' => $id, 'url' => $url, 'sort' => (int) $i, ]); } }); return json(['code' => 0, 'msg' => '已重新提交,请等待审核', 'data' => ['id' => $id]]); } public function toggleLike(int $id) { $user = AuthContext::mpUser(); if (!$user) { return json(['code' => 401, 'msg' => '未授权', 'data' => []]); } $w = Db::name('work')->where('id', $id)->find(); if (!$w || !$this->workRowIsApproved($w)) { return json(['code' => 404, 'msg' => '作品不可点赞', 'data' => []]); } $uid = (int) $user['id']; $ex = Db::name('work_like')->where('work_id', $id)->where('user_id', $uid)->find(); if ($ex) { Db::name('work_like')->where('id', $ex['id'])->delete(); Db::name('work')->where('id', $id)->dec('likes_count')->update(); $likes = max(0, (int) $w['likes_count'] - 1); return json(['code' => 0, 'msg' => 'ok', 'data' => ['liked' => false, 'likes' => $likes]]); } Db::name('work_like')->insert([ 'work_id' => $id, 'user_id' => $uid, 'create_time' => date('Y-m-d H:i:s'), ]); Db::name('work')->where('id', $id)->inc('likes_count')->update(); $likes = (int) $w['likes_count'] + 1; return json(['code' => 0, 'msg' => 'ok', 'data' => ['liked' => true, 'likes' => $likes]]); } protected function toCard(array $w): array { $dt = trim((string) ($w['publish_date'] ?? '')); if ($dt === '') { $dt = date('Y-m-d', strtotime((string) ($w['create_time'] ?? 'now'))); } return [ 'id' => (int) $w['id'], 'userId' => (int) $w['user_id'], 'imageUrl' => (string) ($w['cover_image'] ?? ''), 'title' => (string) ($w['title'] ?? ''), 'description' => (string) ($w['description'] ?? ''), 'likes' => (int) $w['likes_count'], 'author' => $w['author_name'] ? (string) ($w['author_name']) : '用户', 'date' => $dt, ]; } /** * @param array> $rows * @param int $mpUserId 当前请求带有效 Bearer 时传入,用于标记 liked */ protected function toCardList(array $rows, int $mpUserId = 0): array { if ($rows === []) { return []; } $ids = array_map(fn ($r) => (int) ($r['id'] ?? 0), $rows); $catMap = $this->categoriesByWorkIds($ids); $likedSet = []; if ($mpUserId > 0) { $idList = array_values(array_unique(array_filter($ids, static fn (int $id) => $id > 0))); if ($idList !== []) { $likedRows = Db::name('work_like') ->where('user_id', $mpUserId) ->whereIn('work_id', $idList) ->column('work_id'); foreach ($likedRows as $wid) { $likedSet[(int) $wid] = true; } } } return array_map(function (array $r) use ($catMap, $likedSet) { $card = $this->toCard($r); $wid = (int) ($r['id'] ?? 0); $card['categories'] = $catMap[$wid] ?? []; $card['liked'] = isset($likedSet[$wid]); return $card; }, $rows); } /** @param array $workIds */ protected function categoriesByWorkIds(array $workIds): array { $workIds = array_values(array_unique(array_filter($workIds))); if ($workIds === []) { return []; } // 表无自增 id,复合主键 (work_id, category) $rows = Db::name('work_category') ->whereIn('work_id', $workIds) ->order('work_id', 'asc') ->order('category', 'asc') ->select() ->toArray(); $out = []; foreach ($rows as $row) { $wid = (int) ($row['work_id'] ?? 0); $c = trim((string) ($row['category'] ?? '')); if ($wid <= 0 || $c === '') { continue; } if (!isset($out[$wid])) { $out[$wid] = []; } if (!in_array($c, $out[$wid], true)) { $out[$wid][] = $c; } } return $out; } protected function optionalMpUser(): ?array { $auth = $this->request->header('Authorization', ''); if (!is_string($auth) || !str_starts_with($auth, 'Bearer ')) { return null; } $token = trim(substr($auth, 7)); if ($token === '') { return null; } $row = Db::name('mp_user')->where('api_token', $token)->find(); return $row ?: null; } }