feat: add community post categories and channels
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
CREATE TABLE IF NOT EXISTS `la_community_post_category` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(50) NOT NULL DEFAULT '' COMMENT '分类名称',
|
||||
`icon` varchar(500) NOT NULL DEFAULT '' COMMENT '分类图标',
|
||||
`sort` int unsigned NOT NULL DEFAULT 0 COMMENT '排序',
|
||||
`post_count` int unsigned NOT NULL DEFAULT 0 COMMENT '帖子数',
|
||||
`status` tinyint unsigned NOT NULL DEFAULT 1 COMMENT '0禁用1启用',
|
||||
`create_time` int unsigned NOT NULL DEFAULT 0,
|
||||
`update_time` int unsigned NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_name` (`name`),
|
||||
KEY `idx_status_sort` (`status`, `sort`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='社区帖子分类';
|
||||
|
||||
SET @has_category_id = (
|
||||
SELECT COUNT(*)
|
||||
FROM `information_schema`.`columns`
|
||||
WHERE `table_schema` = DATABASE()
|
||||
AND `table_name` = 'la_community_post'
|
||||
AND `column_name` = 'category_id'
|
||||
);
|
||||
SET @sql = IF(
|
||||
@has_category_id = 0,
|
||||
'ALTER TABLE `la_community_post` ADD COLUMN `category_id` int unsigned NOT NULL DEFAULT 0 COMMENT ''帖子主分类ID'' AFTER `post_type`',
|
||||
'SELECT ''la_community_post.category_id already exists'''
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @has_is_topic = (
|
||||
SELECT COUNT(*)
|
||||
FROM `information_schema`.`columns`
|
||||
WHERE `table_schema` = DATABASE()
|
||||
AND `table_name` = 'la_community_post'
|
||||
AND `column_name` = 'is_topic'
|
||||
);
|
||||
SET @sql = IF(
|
||||
@has_is_topic = 0,
|
||||
'ALTER TABLE `la_community_post` ADD COLUMN `is_topic` tinyint unsigned NOT NULL DEFAULT 0 COMMENT ''是否话题 0否 1是'' AFTER `is_recommend`',
|
||||
'SELECT ''la_community_post.is_topic already exists'''
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @has_category_index = (
|
||||
SELECT COUNT(*)
|
||||
FROM `information_schema`.`statistics`
|
||||
WHERE `table_schema` = DATABASE()
|
||||
AND `table_name` = 'la_community_post'
|
||||
AND `index_name` = 'idx_category'
|
||||
);
|
||||
SET @sql = IF(
|
||||
@has_category_index = 0,
|
||||
'ALTER TABLE `la_community_post` ADD INDEX `idx_category` (`category_id`)',
|
||||
'SELECT ''la_community_post.idx_category already exists'''
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @has_recommend_index = (
|
||||
SELECT COUNT(*)
|
||||
FROM `information_schema`.`statistics`
|
||||
WHERE `table_schema` = DATABASE()
|
||||
AND `table_name` = 'la_community_post'
|
||||
AND `index_name` = 'idx_status_recommend'
|
||||
);
|
||||
SET @sql = IF(
|
||||
@has_recommend_index = 0,
|
||||
'ALTER TABLE `la_community_post` ADD INDEX `idx_status_recommend` (`status`, `is_recommend`, `create_time`)',
|
||||
'SELECT ''la_community_post.idx_status_recommend already exists'''
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @has_hot_index = (
|
||||
SELECT COUNT(*)
|
||||
FROM `information_schema`.`statistics`
|
||||
WHERE `table_schema` = DATABASE()
|
||||
AND `table_name` = 'la_community_post'
|
||||
AND `index_name` = 'idx_status_hot'
|
||||
);
|
||||
SET @sql = IF(
|
||||
@has_hot_index = 0,
|
||||
'ALTER TABLE `la_community_post` ADD INDEX `idx_status_hot` (`status`, `is_hot`, `create_time`)',
|
||||
'SELECT ''la_community_post.idx_status_hot already exists'''
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @has_topic_index = (
|
||||
SELECT COUNT(*)
|
||||
FROM `information_schema`.`statistics`
|
||||
WHERE `table_schema` = DATABASE()
|
||||
AND `table_name` = 'la_community_post'
|
||||
AND `index_name` = 'idx_status_topic'
|
||||
);
|
||||
SET @sql = IF(
|
||||
@has_topic_index = 0,
|
||||
'ALTER TABLE `la_community_post` ADD INDEX `idx_status_topic` (`status`, `is_topic`, `create_time`)',
|
||||
'SELECT ''la_community_post.idx_status_topic already exists'''
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
INSERT IGNORE INTO `la_community_post_category`
|
||||
(`id`, `name`, `icon`, `sort`, `post_count`, `status`, `create_time`, `update_time`)
|
||||
SELECT
|
||||
`id`, `name`, `icon`, `sort`, `post_count`, `status`, `create_time`, UNIX_TIMESTAMP()
|
||||
FROM `la_community_tag`;
|
||||
|
||||
UPDATE `la_community_post` post
|
||||
INNER JOIN (
|
||||
SELECT ranked.`post_id`, ranked.`tag_id` AS `category_id`
|
||||
FROM (
|
||||
SELECT
|
||||
post_tag.`post_id`,
|
||||
post_tag.`tag_id`,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY post_tag.`post_id`
|
||||
ORDER BY tag.`sort` DESC, post_tag.`id` ASC
|
||||
) AS row_number
|
||||
FROM `la_community_post_tag` post_tag
|
||||
INNER JOIN `la_community_tag` tag ON tag.`id` = post_tag.`tag_id`
|
||||
INNER JOIN `la_community_post_category` category ON category.`id` = post_tag.`tag_id`
|
||||
) ranked
|
||||
WHERE ranked.row_number = 1
|
||||
) selected_category ON selected_category.`post_id` = post.`id`
|
||||
SET post.`category_id` = selected_category.`category_id`
|
||||
WHERE post.`category_id` = 0;
|
||||
|
||||
UPDATE `la_community_post` post
|
||||
SET post.`is_topic` = 1
|
||||
WHERE post.`is_topic` = 0
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM `la_community_post_tag` post_tag
|
||||
WHERE post_tag.`post_id` = post.`id`
|
||||
);
|
||||
|
||||
UPDATE `la_community_post_category` category
|
||||
LEFT JOIN (
|
||||
SELECT `category_id`, COUNT(*) AS `post_count`
|
||||
FROM `la_community_post`
|
||||
WHERE `delete_time` IS NULL
|
||||
AND `category_id` > 0
|
||||
GROUP BY `category_id`
|
||||
) post_stat ON post_stat.`category_id` = category.`id`
|
||||
SET category.`post_count` = COALESCE(post_stat.`post_count`, 0),
|
||||
category.`update_time` = UNIX_TIMESTAMP();
|
||||
|
||||
SET @community_parent_id = COALESCE(
|
||||
(SELECT `pid` FROM `la_system_menu` WHERE `perms` = 'community.communityPost/lists' LIMIT 1),
|
||||
(SELECT `pid` FROM `la_system_menu` WHERE `component` = 'community/post/index' LIMIT 1),
|
||||
(SELECT `pid` FROM `la_system_menu` WHERE `perms` = 'community.communityTag/lists' LIMIT 1),
|
||||
(SELECT `id` FROM `la_system_menu` WHERE `type` = 'M' AND `paths` = 'community' LIMIT 1),
|
||||
0
|
||||
);
|
||||
|
||||
INSERT INTO `la_system_menu`
|
||||
(`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_cache`, `is_show`, `is_disable`, `create_time`, `update_time`)
|
||||
SELECT
|
||||
@community_parent_id, 'C', '帖子分类', '', 0, 'community.communityCategory/lists', 'category', 'community/category/index', '', '', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
|
||||
WHERE @community_parent_id > 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `la_system_menu` WHERE `perms` = 'community.communityCategory/lists'
|
||||
);
|
||||
|
||||
SET @category_menu_id = (
|
||||
SELECT `id` FROM `la_system_menu`
|
||||
WHERE `perms` = 'community.communityCategory/lists'
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
INSERT INTO `la_system_menu`
|
||||
(`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_cache`, `is_show`, `is_disable`, `create_time`, `update_time`)
|
||||
SELECT @category_menu_id, 'A', permission.`name`, '', 0, permission.`perms`, '', '', '', '', 0, 0, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
|
||||
FROM (
|
||||
SELECT '分类选项' AS `name`, 'community.communityCategory/all' AS `perms`
|
||||
UNION ALL SELECT '分类详情', 'community.communityCategory/detail'
|
||||
UNION ALL SELECT '分类新增', 'community.communityCategory/add'
|
||||
UNION ALL SELECT '分类编辑', 'community.communityCategory/edit'
|
||||
UNION ALL SELECT '分类删除', 'community.communityCategory/delete'
|
||||
) permission
|
||||
WHERE @category_menu_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `la_system_menu` existing_menu WHERE existing_menu.`perms` = permission.`perms`
|
||||
);
|
||||
|
||||
SET @post_menu_id = COALESCE(
|
||||
(SELECT `id` FROM `la_system_menu` WHERE `perms` = 'community.communityPost/lists' LIMIT 1),
|
||||
(SELECT `id` FROM `la_system_menu` WHERE `component` = 'community/post/index' LIMIT 1)
|
||||
);
|
||||
|
||||
INSERT INTO `la_system_menu`
|
||||
(`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_cache`, `is_show`, `is_disable`, `create_time`, `update_time`)
|
||||
SELECT @post_menu_id, 'A', permission.`name`, '', 0, permission.`perms`, '', '', '', '', 0, 0, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
|
||||
FROM (
|
||||
SELECT '设置推荐' AS `name`, 'community.communityPost/setRecommend' AS `perms`
|
||||
UNION ALL SELECT '设置话题', 'community.communityPost/setTopic'
|
||||
UNION ALL SELECT '设置分类', 'community.communityPost/setCategory'
|
||||
) permission
|
||||
WHERE @post_menu_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `la_system_menu` existing_menu WHERE existing_menu.`perms` = permission.`perms`
|
||||
);
|
||||
|
||||
INSERT INTO `la_system_role_menu` (`role_id`, `menu_id`)
|
||||
SELECT 1, menu.`id`
|
||||
FROM `la_system_menu` menu
|
||||
WHERE menu.`perms` IN (
|
||||
'community.communityCategory/lists',
|
||||
'community.communityCategory/all',
|
||||
'community.communityCategory/detail',
|
||||
'community.communityCategory/add',
|
||||
'community.communityCategory/edit',
|
||||
'community.communityCategory/delete',
|
||||
'community.communityPost/setRecommend',
|
||||
'community.communityPost/setTopic',
|
||||
'community.communityPost/setCategory'
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM `la_system_role_menu` role_menu
|
||||
WHERE role_menu.`role_id` = 1
|
||||
AND role_menu.`menu_id` = menu.`id`
|
||||
);
|
||||
@@ -0,0 +1,170 @@
|
||||
# Community Post Channels and Categories Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 为社区帖子补齐推荐、热门、话题频道管理,并新增独立帖子分类表、管理后台和前台频道筛选。
|
||||
|
||||
**Architecture:** 复用 `la_community_post.is_recommend` 与 `is_hot`,只新增 `is_topic` 和单选 `category_id`。分类通过独立 `la_community_post_category` 管理,初始化时复制标签并回填历史帖子;标签多选关系继续保留。后台使用 likeadmin Controller/Logic/Validate/Lists 分层,用户端列表通过现有 `/api/community/postLists` 参数筛选。
|
||||
|
||||
**Tech Stack:** PHP 8、ThinkPHP/likeadmin、MySQL 8、Vue 3、Element Plus、uni-app、TypeScript
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 数据库结构、初始化数据与菜单权限
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/sql/create_community_post_categories.sql`
|
||||
- Modify: `sbnews.sql`
|
||||
|
||||
- [x] **Step 1: 创建独立分类表和帖子字段**
|
||||
|
||||
SQL 必须幂等创建 `la_community_post_category`,字段为 `id/name/icon/sort/post_count/status/create_time/update_time`,并在 `la_community_post` 增加:
|
||||
|
||||
```sql
|
||||
`category_id` int unsigned NOT NULL DEFAULT 0 COMMENT '帖子主分类ID',
|
||||
`is_topic` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '是否话题 0否 1是'
|
||||
```
|
||||
|
||||
同时添加 `category_id`、`status + is_recommend`、`status + is_hot`、`status + is_topic` 查询索引。
|
||||
|
||||
- [x] **Step 2: 从标签初始化分类并回填历史帖子**
|
||||
|
||||
分类初始化使用 `INSERT IGNORE ... SELECT` 保留标签 ID,不覆盖后续独立维护的分类。历史帖子只在 `category_id=0` 时回填,按标签 `sort DESC, community_post_tag.id ASC` 选取一个主分类;已有标签的帖子初始化为 `is_topic=1` 以保持原话题流可见性,最后按实际帖子数重算 `post_count`。
|
||||
|
||||
- [x] **Step 3: 初始化管理菜单与按钮权限**
|
||||
|
||||
新增 `community.communityCategory/lists` 页面及 `detail/add/edit/delete` 按钮权限;补充帖子 `setRecommend/setTopic/setCategory` 权限。所有菜单通过已有社区帖子或标签菜单反查父级,使用 `NOT EXISTS` 幂等插入,并授权角色 `role_id=1`。
|
||||
|
||||
### Task 2: 后端分类管理和帖子管理接口
|
||||
|
||||
**Files:**
|
||||
- Create: `server/app/common/model/community/CommunityCategory.php`
|
||||
- Modify: `server/app/common/model/community/CommunityPost.php`
|
||||
- Create: `server/app/adminapi/controller/community/CommunityCategoryController.php`
|
||||
- Create: `server/app/adminapi/logic/community/CommunityCategoryLogic.php`
|
||||
- Create: `server/app/adminapi/validate/community/CommunityCategoryValidate.php`
|
||||
- Create: `server/app/adminapi/lists/community/CommunityCategoryLists.php`
|
||||
- Modify: `server/app/adminapi/controller/community/CommunityPostController.php`
|
||||
- Modify: `server/app/adminapi/logic/community/CommunityPostLogic.php`
|
||||
- Modify: `server/app/adminapi/validate/community/CommunityPostValidate.php`
|
||||
- Modify: `server/app/adminapi/lists/community/CommunityPostLists.php`
|
||||
|
||||
- [x] **Step 1: 实现分类 CRUD**
|
||||
|
||||
分类接口沿用标签管理风格:
|
||||
|
||||
```text
|
||||
GET /adminapi/community.communityCategory/lists
|
||||
GET /adminapi/community.communityCategory/all
|
||||
GET /adminapi/community.communityCategory/detail
|
||||
POST /adminapi/community.communityCategory/add
|
||||
POST /adminapi/community.communityCategory/edit
|
||||
POST /adminapi/community.communityCategory/delete
|
||||
```
|
||||
|
||||
分类图标使用 `FileService::getFileUrl/setFileUrl`;删除前查询 `CommunityPost::where('category_id', id)->count()`,存在引用时返回“该分类下存在帖子,不能删除”。
|
||||
|
||||
- [x] **Step 2: 扩展帖子管理字段**
|
||||
|
||||
帖子列表筛选增加:
|
||||
|
||||
```php
|
||||
'=' => ['status', 'post_type', 'category_id', 'is_top', 'is_hot', 'is_recommend', 'is_topic', 'user_id']
|
||||
```
|
||||
|
||||
列表和详情批量补充 `category_name`。新增 `setTopic()` 与 `setCategory()`;复用已有 `setRecommend()`。设置分类和删除帖子后重算受影响分类的 `post_count`。
|
||||
|
||||
- [x] **Step 3: 补齐参数校验**
|
||||
|
||||
`is_topic`、`is_hot`、`is_recommend` 只允许 `0/1`;`category_id` 允许 `0`,大于 `0` 时必须存在于分类表。控制器继续使用 likeadmin 统一成功/失败响应。
|
||||
|
||||
### Task 3: 前台帖子频道筛选
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/app/api/lists/community/CommunityPostLists.php`
|
||||
- Modify: `uniapp/src/pages/community/community.vue`
|
||||
|
||||
- [x] **Step 1: 后端列表接收频道参数**
|
||||
|
||||
`queryWhere()` 对明确传入的值进行过滤:
|
||||
|
||||
```php
|
||||
foreach (['is_recommend', 'is_hot', 'is_topic', 'category_id'] as $field) {
|
||||
if (isset($this->params[$field]) && $this->params[$field] !== '') {
|
||||
$where[] = [$field, '=', (int) $this->params[$field]];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
列表字段返回 `category_id/is_topic`,并批量补充 `category_name`;排序保持 `is_top DESC, is_hot DESC, create_time DESC`。
|
||||
|
||||
- [x] **Step 2: 用户端频道发送真实筛选条件**
|
||||
|
||||
社区页请求参数调整为:
|
||||
|
||||
```ts
|
||||
if (activeFeed.value === 'recommend') params.is_recommend = 1
|
||||
if (activeFeed.value === 'follow') params.follow = 1
|
||||
if (activeFeed.value === 'hot') params.is_hot = 1
|
||||
if (activeFeed.value === 'topic') params.is_topic = 1
|
||||
if (activeTagId.value > 0) params.tag_id = activeTagId.value
|
||||
```
|
||||
|
||||
移除仅对当前页进行热门分数排序的 `rankHotPosts()`,防止分页结果与后台热门开关不一致。
|
||||
|
||||
### Task 4: 管理端分类页面和帖子字段管理
|
||||
|
||||
**Files:**
|
||||
- Create: `admin/src/views/community/category/index.vue`
|
||||
- Modify: `admin/src/api/community.ts`
|
||||
- Modify: `admin/src/views/community/post/index.vue`
|
||||
|
||||
- [x] **Step 1: 新增分类 API 和管理页**
|
||||
|
||||
API 增加分类 `lists/all/detail/add/edit/delete`。分类页复用标签页布局,支持名称搜索、图标、排序、状态、新增、编辑和受保护删除。
|
||||
|
||||
- [x] **Step 2: 补齐帖子 API**
|
||||
|
||||
新增调用:
|
||||
|
||||
```ts
|
||||
communityPostSetRecommend({ id, is_recommend })
|
||||
communityPostSetTopic({ id, is_topic })
|
||||
communityPostSetCategory({ id, category_id })
|
||||
```
|
||||
|
||||
- [x] **Step 3: 扩展帖子列表管理**
|
||||
|
||||
搜索区增加分类、推荐、热门、话题筛选;表格增加分类单选框,以及推荐、热门、话题三个开关。接口失败时重新获取列表恢复服务端真实值,详情弹窗展示分类和三个频道状态。
|
||||
|
||||
### Task 5: Review、静态验证、提交与测试服同步
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/业务进度管理.md`
|
||||
- Create: `docs/superpowers/plans/2026-08-02-community-post-channels-and-categories.md`
|
||||
|
||||
- [x] **Step 1: 静态检查**
|
||||
|
||||
运行所有修改 PHP 文件的 `php -l`、`git diff --check`、SQL 未完成标记扫描,并检查 Controller/Logic/Validate/Lists/Model/Admin API 字段名称一致。项目未授权执行完整构建或自动化测试。
|
||||
|
||||
- [x] **Step 2: 前端代码 review**
|
||||
|
||||
检查管理端请求失败回滚、权限标识、选择器数值类型、分页筛选参数和用户端频道切换请求,确认不存在客户端分页内伪排序或未使用导入。
|
||||
|
||||
- [ ] **Step 3: 提交源码**
|
||||
|
||||
只暂存本任务文件,排除工作区原有 `AGENTS.md` 和共享文档混合改动,提交信息:
|
||||
|
||||
```text
|
||||
feat: add community post categories and channels
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 同步测试服并应用数据库初始化**
|
||||
|
||||
先执行项目规定部署命令:
|
||||
|
||||
```powershell
|
||||
Set-Location D:\www\gs-sport-era; powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -AutoIncremental -AutoIncrementalCommits 3 -RemoteDir /www/wwwroot/test-server.sbnews.net
|
||||
```
|
||||
|
||||
随后在测试服运行幂等 SQL `docs/sql/create_community_post_categories.sql`,复核分类表、帖子新字段和社区分类菜单已经存在。
|
||||
Reference in New Issue
Block a user