six-shop / news
资讯,文章,新闻,公告发布
Requires
- php: >=8.3
- six-shop/core: >=0.5.8 <1.0
README
模块功能
本模块为 SixShop 系统提供资讯管理功能。其核心业务逻辑(数据库 CRUD)完全由 Go 语言实现,并通过 PHP FFI 进行调用。这种方式可以有效保护核心业务逻辑代码,同时利用 Go 的高性能特性。
主要特性
- 后台管理:
- 资讯分类管理 (增删改查)
- 资讯文章管理 (增删改查)
- API 接口:
- 获取资讯分类列表
- 获取指定分类下的文章列表(支持分页)
- 获取文章详情
- Go FFI 实现:
- 所有数据库操作均在编译好的 Go 共享库 (
.so
) 中执行。 - PHP 服务层仅作为调用 FFI 接口的桥梁。
- 所有数据库操作均在编译好的 Go 共享库 (
目录结构
backend/extension/news/
├── config/
│ ├── install.sql # 数据库表结构
│ └── uninstall.sql # 卸载表结构
├── ffi/
│ ├── main.go # Go FFI 业务逻辑和导出函数
│ ├── go.mod # Go 依赖管理
│ ├── Makefile # 一键构建 Go 共享库的脚本
│ ├── model/
│ │ ├── category.go # 资讯分类 GORM 模型
│ │ └── news.go # 资讯文章 GORM 模型
│ ├── lib_news.so # [构建产物] Go 编译的共享库
│ └── lib_news.h # [构建产物] Go 编译的头文件
├── route/
│ ├── admin.php # 后台路由
│ └── api.php # 前端API路由
├── src/
│ ├── Controller/
│ ├── Model/ # ThinkPHP 模型 (仅用于框架集成, 不含业务逻辑)
│ ├── Service/
│ │ ├── NewsCategoryService.php # 分类服务的 FFI 调用封装
│ │ ├── NewsService.php # 资讯服务的 FFI 调用封装
│ │ └── NewsFfiService.php # [核心] FFI 单例服务, 负责加载和调用 .so 文件
│ └── Hook/
├── news.info.yml # 模块信息
└── README.md # 模块说明文档
数据库安装
- 在插件安装时,需执行
config/install.sql
文件,以创建cy_news_category
和cy_news
两张表。
核心依赖
- Go 1.20+
- Docker (用于执行
Makefile
中的构建命令) - PHP 7.4+ 并已启用 FFI 扩展
构建 Go 共享库
所有 Go 源代码的编译都通过 Makefile
完成。
# 进入 ffi 目录
cd backend/extension/news/ffi
# 执行构建
make build
该命令会使用 Docker 容器来编译 Go 代码,确保构建环境的一致性。成功后,会在当前目录下生成 lib_news.so
和 lib_news.h
两个文件。
PHP FFI 调用流程
NewsFfiService
通过单例模式加载lib_news.so
文件,并根据lib_news.h
的内容定义 C 函数签名。NewsService
和NewsCategoryService
在实例化时会获取NewsFfiService
的单例。- 当控制器调用
NewsService
的方法时(如getList
),NewsService
会将请求参数(如查询条件、分页信息)打包成 JSON 字符串。 NewsService
调用NewsFfiService
的相应方法,将 JSON 字符串作为参数传递给 Go 函数。- Go 函数接收参数,执行数据库操作,并将结果打包成 JSON 字符串返回。
NewsFfiService
接收返回的 JSON 字符串,并将其解码为 PHP 数组,最终返回给控制器。
FFI 接口清单 (Go -> PHP)
所有业务逻辑都已封装在以下 Go 函数中,并通过 FFI 导出:
GetCategoryList(char* whereJson)
GetCategoryByID(int id)
CreateCategory(char* dataJson)
UpdateCategory(int id, char* dataJson)
DeleteCategory(int id)
GetNewsList(char* paramsJson)
GetNewsByID(int id)
CreateNews(char* dataJson)
UpdateNews(int id, char* dataJson)
DeleteNews(int id)
注意事项
- 路径依赖:
NewsFfiService.php
中硬编码了lib_news.so
的相对路径。如果移动文件,需要同步更新。 - 重新编译: 任何对
ffi/
目录下.go
文件的修改,都必须重新执行make build
才能生效。 - 错误处理: Go FFI 函数中的错误会以包含
error
键的 JSON 字符串形式返回,PHP 服务层应进行相应的检查和处理。
服务层依赖注入分析
在 news
模块的控制器中,存在两种不同的服务层依赖注入(或实例化)方式。这两种方式在设计上存在差异,体现了不同的架构思路。
方式一:通过适配器实例化 (Admin 控制器)
此方式应用于 src/Controller/Admin/
目录下的所有控制器。
实现: 控制器在其构造函数中,通过
new
关键字直接实例化一个适配器 (NewsServiceAdapter
或NewsCategoryServiceAdapter
)。// 文件: backend/extension/news/src/Controller/Admin/NewsController.php use SixShop\News\Service\NewsServiceAdapter; class NewsController { protected $service; public function __construct() { // 直接实例化适配器 $this->service = new NewsServiceAdapter(); } // ... }
- 优点:
- 松耦合: 控制器本身不关心底层的具体实现。所有的业务逻辑切换(例如,在开发模式下使用纯 PHP 服务,在生产模式下使用 FFI 服务)都封装在适配器内部。
- 符合依赖倒置原则: 控制器依赖于一个稳定的"适配器"抽象,而不是一个多变的具体服务。
- 易于维护和扩展: 如果未来需要增加新的服务实现,只需要修改适配器,控制器代码无需改动。
- 结论: 这是项目推荐的、更健壮、更灵活的设计模式。
方式二:通过构造函数依赖注入 (Api 控制器)
此方式应用于 src/Controller/Api/
目录下的控制器。
实现: 控制器在其构造函数的参数中,声明需要注入的具体服务 (
NewsService
),由框架的依赖注入容器自动实例化并传入。// 文件: backend/extension/news/src/Controller/Api/NewsController.php use SixShop\News\Service\NewsService; class NewsController { private $service; public function __construct(NewsService $service) { // 由框架注入具体的 NewsService 实例 $this->service = $service; } // ... }
- 缺点:
- 紧耦合: 控制器与具体的
NewsService
类紧密绑定。 - 缺乏灵活性: 它无法利用适配器模式带来的好处。如果想为 API 控制器也实现开发/生产模式的切换,就必须重构其构造函数和依赖关系。
- 设计不一致: 与 Admin 控制器的实现方式不统一,增加了项目的维护成本和认知负担。
- 紧耦合: 控制器与具体的
- 改进建议: 为了保持整个模块架构的一致性,建议未来将 Api 控制器的实现方式重构为方式一(使用适配器)。
API 接口说明
本插件提供两组符合 RESTful 规范的资源路由,分别用于后台管理和前端调用。
后台管理 API
路由前缀: /admin/extension/news
所有后台接口均会通过 Auth
中间件进行权限验证。
1. 资讯分类管理 (/category
)
- 获取分类列表:
GET /category
- 说明: 获取所有资讯分类,支持
name
字段模糊查询。 - 示例:
GET /admin/extension/news/category?name=技术
- 说明: 获取所有资讯分类,支持
- 获取单个分类:
GET /category/{id}
- 说明: 获取指定 ID 的分类详情。
- 新增分类:
POST /category
- 说明: 创建一个新的资讯分类。
- 请求体 (JSON):
{ "name": "新分类", "sort": 100, "status": 1 }
- 更新分类:
PUT /category/{id}
- 说明: 更新指定 ID 的分类信息。
- 请求体 (JSON):
{ "name": "更新后的分类", "sort": 99 }
- 删除分类:
DELETE /category/{id}
- 说明: 删除指定 ID 的分类。
2. 资讯文章管理 (/news
)
- 获取文章列表:
GET /news
- 说明: 获取所有资讯文章,支持分页,支持按
title
和category_id
查询。 - 示例:
GET /admin/extension/news/news?title=六店&category_id=1&page=1&limit=10
- 说明: 获取所有资讯文章,支持分页,支持按
- 获取单篇文章:
GET /news/{id}
- 说明: 获取指定 ID 的文章详情。
- 新增文章:
POST /news
- 说明: 创建一篇新的资讯文章。
- 请求体 (JSON):
{ "category_id": 1, "title": "文章标题", "content": "文章内容...", "status": 1 }
- 更新文章:
PUT /news/{id}
- 说明: 更新指定 ID 的文章信息。
- 请求体 (JSON):
{ "title": "更新后的标题" }
- 删除文章:
DELETE /news/{id}
- 说明: 删除指定 ID 的文章。
移动端 API
路由前缀: /api/extension/news
1. 资讯分类接口 (/category
)
- 获取分类列表:
GET /category
- 说明: 获取所有已启用的资讯分类列表。
2. 资讯文章接口 (/news
)
- 获取文章列表:
GET /news
- 说明: 获取所有已发布的资讯文章,支持按
category_id
筛选和分页。 - 示例:
GET /api/extension/news/news?category_id=1&page=1&limit=10
- 说明: 获取所有已发布的资讯文章,支持按
- 获取文章详情:
GET /news/{id}
- 说明: 获取一篇已发布的文章详情。
数据库安装与卸载
- 安装表结构:
- 执行
config/install.sql
,自动创建资讯分类表(cy_news_category)和资讯文章表(cy_news)。
- 执行
- 卸载表结构:
- 执行
config/uninstall.sql
,自动删除本模块相关表结构。
- 执行
推荐在模块安装、升级、卸载时自动调用对应 SQL,便于独立管理和维护。
FFI 动态库能力说明
本模块内置 Go FFI 动态库(ffi/
目录),用于高性能、可闭源的资讯业务处理,PHP 可通过 FFI 调用。
依赖安装
- 需 Go 1.20+
- 依赖 GORM、yaml.v2
- 安装依赖:
cd ffi
go mod tidy
数据库配置
- 配置文件:
config/db.yaml
- 支持 MySQL,参数与 PHP 项目一致
构建动态库
- 推荐用 Makefile 一键构建:
cd ffi
make build
# 生成 lib_news.so 和 lib_news.h
- 也可用 Dockerfile 构建
主要接口
GetList()
获取资讯列表(JSON)GetById(id)
获取单条资讯详情(JSON)Create(json)
新增资讯,参数为 JSONUpdate(id, json)
编辑资讯Delete(id)
删除资讯GenerateSummary(content)
生成摘要CheckContent(content)
内容审核ExtractKeywords(content)
关键词提取
PHP FFI 调用示例
$ffi = FFI::cdef(file_get_contents('lib_news.h'), 'lib_news.so');
$list = json_decode(FFI::string($ffi->GetList()), true);
$info = json_decode(FFI::string($ffi->GetById(1)), true);
$id = $ffi->Create(json_encode($data));
$ok = $ffi->Update($id, json_encode($data));
$ok = $ffi->Delete($id);
$summary = FFI::string($ffi->GenerateSummary($content));
$isIllegal = $ffi->CheckContent($content);
$keywords = explode(',', FFI::string($ffi->ExtractKeywords($content)));
注意事项
- 动态库需与 PHP FFI 路径一致
- 数据库表结构需提前安装(见 config/install.sql)
- 推荐配合 PHP Service 层切换调用
插件编译与分发
一键编译与分发
- 推荐使用 shell 脚本(需 bash 环境):
cd backend/extension/news ./build.sh
- 或使用 PHP 脚本(适合 PHP-only 环境):
cd backend/extension/news php build.php
编译/分发脚本高级特性
- 增量编译:自动检测 ffi/ 下所有 Go 源码变更,无变更则跳过 make build,极大提升效率。
- 编译日志:所有编译和打包输出写入
build/build.log
,便于排查问题。 - 自动打包:编译完成后自动将
build/
目录内容打包为build/package.zip
,方便分发和备份。 - 产物一致:无论用 shell 还是 PHP 脚本,体验和产物完全一致。
编译产物
build/lib_news.so
、build/lib_news.h
:Go FFI 动态库及头文件build/NewsServiceFFI.php
:PHP FFI 适配层,控制器/接口直接调用build/package.zip
:自动打包的分发包build/build.log
:编译和打包日志
开发与分发模式
- 开发模式:保留全部源码,便于调试和测试
- 分发模式:只保留 build 目录和必要接口,核心业务完全闭源
控制器调用 FFI 适配层示例
require_once __DIR__ . '/../build/NewsServiceFFI.php';
$service = new NewsService();
$list = $service->getList();
服务适配层(ServiceAdapter)设计说明
为实现开发模式(PHP Service)与分发模式(FFI 适配层)的无缝切换,所有控制器均依赖适配层(如 NewsServiceAdapter、NewsCategoryServiceAdapter),而不直接依赖具体 Service 实现。
- 开发模式:适配层自动加载并调用 PHP Service,便于本地开发和调试。
- 分发模式:适配层自动加载并调用 FFI 适配层(build/NewsServiceFFI.php),核心逻辑闭源,安全分发。
- 控制器层代码无需变动,适配层自动切换底层实现。
控制器用法示例
$service = new NewsServiceAdapter();
$list = $service->getList();
适配层代码位于 src/Service/NewsServiceAdapter.php
、src/Service/NewsCategoryServiceAdapter.php
,可根据实际业务扩展更多方法。