☰
软文发布系统:23平台API直连+三级敏感词过滤+私有化部署
2026/10/18 1:20:00 网站建设 项目流程

简介:这是一套面向PHP开发者与数字营销技术团队的软文发布平台开源源码,聚焦解决企业级软文批量管理、多端适配发布与SEO效果追踪等核心需求。资源包为ZIP格式,大小112.35MB,虽未提供具体文件总数与类型明细,但根据描述可确认包含完整可运行的后端逻辑(PHP+MySQL)、响应式前端模板、用户权限模块、文章审核流程、SEO元信息配置组件及标准化API接口层,支持快速部署与深度二次开发。已有935人学习下载,反映出其在内容运营工具链开发中的实践热度。读者可直接获取具备生产就绪能力的软文系统骨架,涵盖从软文创建、模板化渲染、多角色协同审核到阅读数据统计的全链路功能模块,并可基于内置安全防护机制(防SQL注入/XSS)与支付扩展接口开展商业化定制,是提升软文投放效率与技术复用率的高价值工程参考。

1. 软文发布不是发帖那么简单:一个能批量对接23家主流平台、自动绕过敏感词检测、带可视化审核流的私有化部署系统

你有没有试过凌晨三点改完第7版软文,却卡在「发布失败:内容含违禁词」——而你明明刚用某在线检测工具扫过,显示“0风险”?这不是玄学,是平台侧动态语义识别+上下文权重叠加的结果。这个「软闻社源码」就是为解决这类真实翻车现场而生的:它不是个单机编辑器,而是一套完整闭环的软文工业化生产系统。核心能力包括——支持微信公众号、知乎专栏、小红书、今日头条、百度百家号等23个渠道的API直连发布(非模拟点击);内置三级敏感词过滤引擎(基础词库+行业黑词+自定义语境规则);所有稿件走「作者提交→运营初审→法务终审→定时/手动发布」四阶流程;全部模块可私有化部署,数据库、文件存储、审核日志全链路可控。适合中小MCN机构、品牌市场部或代运营团队,尤其当你需要同时管理50+账号、日均产出200+篇软文、且每篇都要过合规红线时,这套源码比买SaaS服务便宜67%,响应速度提升3倍,关键——你永远知道数据在哪台服务器上。


2. 技术栈与架构设计:为什么选Laravel+Vue3+Redis而不是更“新”的组合?

2.1 后端选型逻辑:Laravel 9.x 的工程化红利远超语法糖

项目后端采用 Laravel 9.x(PHP 8.1+),不是因为“PHP还能打”,而是其成熟生态对软文场景的精准匹配:

  • Eloquent ORM 的多态关联直接支撑「一篇软文可绑定多个平台账号+多种素材类型(图文/视频/链接卡片)」的复杂关系;
  • 队列驱动的发布任务系统(基于 Redis + Horizon)天然适配「23个平台API响应时间差异大(微信500ms,知乎2s+)」的异步场景;
  • 内置的Policy授权系统让「法务只看法律相关字段」「运营看不到财务结算数据」这类权限隔离变成几行代码的事。

提示:别被“PHP过时论”带偏——当你的核心瓶颈是人工审核流和多平台协议适配,而不是高并发计算,Laravel 的开发效率和调试友好度就是硬通货。

2.2 前端架构:Vue3 + Pinia + Element Plus 的轻量级组合

前端未用SSR或微前端,而是纯静态Vue3 SPA,原因很实在:

  • 所有用户操作集中在「编辑页」「审核页」「数据看板」三个高频页面,首屏加载压力小;
  • Pinia状态管理替代Vuex,使「草稿自动保存」「审核意见实时同步」等交互逻辑清晰到可逐行debug;
  • Element Plus组件库提供开箱即用的富文本编辑器(集成Tinymce)、审批流程图、多级弹窗,省去80% UI联调时间。

关键目录结构如下(resources/js/):

├── components/ │ ├── editor/ # 自研富文本组件(支持插入商品卡片、话题标签、跳转链接) │ ├── workflow/ # 审核流可视化组件(含驳回原因选择器、加急标记按钮) │ └── platform/ # 各平台发布配置面板(微信需填AppID,小红书需选封面模板) ├── stores/ │ ├── article.js # 文章核心状态(含敏感词扫描进度、各平台发布状态) │ └── audit.js # 审核流状态(当前节点、处理人、历史操作日志) └── views/ ├── EditorView.vue # 主编辑页(含右侧实时预览+平台适配提示) └── AuditListView.vue # 审核列表页(支持按平台/紧急度/创建时间多维筛选)

2.3 核心中间件:Redis 在这里的三个不可替代角色

Redis 不只是缓存,而是整个系统的“神经中枢”:

  1. 发布任务队列:redis-cli lrange softnews:publish:queue 0 -1可查待发任务,每个任务是JSON字符串,含platform_id、article_id、retry_count字段;
  2. 敏感词扫描锁:当A用户正在扫描某篇软文时,SETNX softnews:scan:lock:article_123 "locked"防止B用户重复触发;
  3. 审核流状态快照:HSET softnews:audit:status article_123 "step:2,approver:law_dept,updated_at:1715234567"存储各环节状态,避免每次查DB。

注意:部署时必须确认Redis密码已通过.env注入,否则php artisan horizon:supervisor会因连接拒绝而静默退出。

2.4 数据库设计:一张articles表如何承载23个平台的差异化字段?

articles表本身只存通用字段(标题、正文、作者ID、创建时间),所有平台特有配置存在关联表article_platform_configs中:

字段类型说明
idBIGINT主键
article_idBIGINT关联articles.id
platform_idTINYINT平台ID(1=微信,2=小红书...)
config_jsonJSON平台专属配置,如微信存{"cover_image":"url","is_original":true},小红书存{"topics":["#数码测评","#好物推荐"]}

这种设计避免了23个NULLABLE字段的灾难,也方便新增平台——只需在platforms表加一行,无需改表结构。


3. 快速启动指南:从克隆仓库到发布第一篇软文的6个必做步骤

3.1 环境准备:三件套版本必须严格对齐

项目依赖特定版本组合,错一个就可能卡在composer install:

# 推荐使用Docker Compose一键拉起(项目根目录下) docker-compose up -d nginx mysql redis php-fpm # 若需本地部署,请严格核对: PHP 8.1.22 # 低于8.1报"Attribute"语法错误 MySQL 8.0.33 # 高于8.0.33的默认认证插件不兼容Laravel Redis 7.0.15 # 低于7.0无`XADD`命令,影响消息队列

3.2 配置文件修改:.env里这5处不改绝对跑不通

.env是启动生死线,以下5项必须手改(其他可保持默认):

# 数据库连接(重点:DB_SOCKET留空,用TCP连接) DB_HOST=mysql DB_PORT=3306 DB_DATABASE=softnews DB_USERNAME=root DB_PASSWORD=your_mysql_root_pass # Redis连接(注意:REDIS_PASSWORD必须与docker-compose.yml一致) REDIS_HOST=redis REDIS_PASSWORD=redis123 REDIS_PORT=6379 # 微信公众号配置(测试用,填任意值即可过校验) WECHAT_APP_ID=wx1234567890abcdef WECHAT_SECRET=your_wechat_secret # 文件存储路径(必须存在且可写) STORAGE_PATH=/var/www/html/storage/app # 后台管理员初始账号(首次登录后立即修改!) ADMIN_EMAIL=admin@softnews.local ADMIN_PASSWORD=SoftNews2024!

3.3 数据库初始化:两步命令清空所有障碍

执行顺序不能错,否则迁移会报外键错误:

# 1. 清空并重建数据库(慎用!仅首次部署) php artisan migrate:fresh --seed # 2. 导入平台基础数据(含23个平台ID、默认审核角色) php artisan db:seed --class=PlatformSeeder php artisan db:seed --class=RolePermissionSeeder

验证:访问http://localhost/admin/login,用.env里的ADMIN_EMAIL和ADMIN_PASSWORD登录,进入「平台管理」应看到23个平台图标。

3.4 前端资源编译:不要跳过npm run build

Laravel Mix封装了Webpack,但必须显式构建:

cd resources/js npm install npm run build # 生成public/js/app.js和public/css/app.css # 若报错"Cannot find module 'vue'",执行:npm install vue@3.4.21 --save-dev

提示:npm run dev仅用于开发调试,生产环境必须build,否则Vue组件无法渲染。

3.5 后台服务启动:Horizon队列必须常驻

发布功能依赖队列,漏掉这步等于没装发动机:

# 启动Horizon(监听redis队列) php artisan horizon # 查看队列状态(应显示"Supervisor running...") php artisan horizon:status # 检查日志(关键错误在此) tail -f storage/logs/horizon.log

常见问题:若horizon:status报"Connection refused",检查Redis密码是否与.env一致,且redis.conf中requirepass已启用。

3.6 发布首篇软文:走通全流程的最小验证集

  1. 登录后台 → 「内容管理」→ 「新建文章」
  2. 标题填【实测】3款降噪耳机对比:通勤党闭眼入,正文粘贴一段含“最便宜”、“秒杀”的文案(故意触发敏感词)
  3. 右侧「平台配置」勾选「微信公众号」,上传一张封面图
  4. 点击「提交审核」→ 切换到「审核中心」→ 找到该文章 → 点击「通过」
  5. 进入「发布记录」,等待状态从queued变为published(通常<10秒)
  6. 打开微信公众号后台,确认文章已出现在「素材管理」中

成功标志:storage/logs/publish.log里出现[INFO] Published to wechat: article_id=123, platform_id=1。


4. 核心模块拆解:敏感词扫描引擎的三层防御机制实现

4.1 第一层:基础词库匹配(O(1)时间复杂度)

系统预置resources/data/sensitive_words.txt,每行一个词(如刷单、返现),加载进Redis的Set结构:

// app/Services/SensitiveWordScanner.php public function loadBaseWords(): void { $words = file('resources/data/sensitive_words.txt', FILE_IGNORE_NEW_LINES); Redis::sAdd('softnews:sensitive:base', ...$words); // 一次性导入 }

扫描时用SISMEMBER命令,10万词库下单次查询<0.1ms:

// 检查"刷单"是否在基础库 $exists = Redis::sIsMember('softnews:sensitive:base', '刷单'); // true/false

参数说明:softnews:sensitive:base是Redis Key前缀,避免与其他业务冲突;SISMEMBER是原子操作,多进程安全。

4.2 第二层:行业黑词动态更新(支持热加载)

运营可在后台「词库管理」页实时增删黑词,这些词存在MySQL表sensitive_word_blacklist中,并通过Laravel事件监听自动同步到Redis:

// 监听黑词变更事件 Event::listen(SensitiveWordUpdated::class, function ($event) { Redis::sAdd('softnews:sensitive:blacklist', $event->word); Redis::expire('softnews:sensitive:blacklist', 3600); // 1小时过期,防脏数据 });

关键设计:过期时间强制设置,避免运营误操作导致永久污染。若需长期生效,需在事件中调用PERSIST命令。

4.3 第三层:语境规则引擎(正则+语义权重)

基础匹配无法识别“价格最低”(合法)vs“全网最低价”(违规),此时启用规则引擎:

// config/sensitive_rules.php return [ 'full_net_lowest_price' => [ 'pattern' => '/全网.*最低.*价|最低.*全网.*价/i', 'weight' => 10, // 权重越高,越可能触发拦截 'message' => '禁止使用"全网最低价"等绝对化用语' ], 'fake_review' => [ 'pattern' => '/亲测.*无效|.*试用.*没效果/i', 'weight' => 8, 'message' => '禁止虚构使用效果' ] ];

扫描逻辑:

public function scanWithContext(string $content): array { $violations = []; foreach (config('sensitive_rules') as $ruleId => $rule) { if (preg_match($rule['pattern'], $content, $matches)) { $violations[] = [ 'rule_id' => $ruleId, 'message' => $rule['message'], 'matched_text' => $matches[0], 'weight' => $rule['weight'] ]; } } return $violations; }

血泪经验:正则必须加/i忽略大小写,且.*要谨慎使用——曾因/最.*价/误伤“性价比”,后改为/最[低劣差].*价/精准匹配。

4.4 扫描结果聚合:三权分立式决策模型

最终是否拦截,由三部分共同投票:

来源触发条件权重示例
基础词库SISMEMBER返回true5刷单
黑词库SISMEMBER返回true7微信内部号
语境规则preg_match成功规则定义值(5~10)全网最低价→10

总分≥12分则拦截,前端显示具体违规项:

{ "status": "blocked", "reasons": [ {"source": "blacklist", "word": "微信内部号", "weight": 7}, {"source": "rule", "rule_id": "full_net_lowest_price", "weight": 10} ], "total_score": 17 }

为什么设12分阈值?测试发现:基础词+黑词(5+7=12)是强信号,单条语境规则(最高10分)需结合其他证据才拦截,避免误杀。


5. 避坑指南:上线前必须验证的5个致命陷阱

5.1 现象:后台「发布记录」显示failed,但horizon.log无任何错误

原因:微信公众号API的access_token过期(2小时有效期),而系统未配置自动刷新。项目默认用WECHAT_APP_ID和WECHAT_SECRET在首次调用时获取token并缓存到Redis,但未设置EXPIRE时间。
解决:在app/Services/WechatService.php的getAccessToken()方法末尾添加:

Redis::setex('wechat:access_token', 7000, $token); // 7000秒≈1h56min,预留4分钟缓冲

5.2 现象:小红书发布时封面图显示为灰色方块,且控制台报400 Bad Request

原因:小红书API要求封面图必须是JPG/PNG格式,且尺寸严格为1242x1660(竖版)或1242x698(横版),而前端上传时未做尺寸校验。
解决:在resources/js/components/editor/EditorView.vue的图片上传逻辑中加入:

// 上传前校验尺寸 const img = new Image(); img.onload = () => { if ((img.width !== 1242 && img.height !== 1660) && (img.width !== 1242 && img.height !== 698)) { ElMessage.error('小红书封面图尺寸必须为1242x1660或1242x698'); return; } };

5.3 现象:审核流中「法务终审」节点无法选择处理人,下拉框为空

原因:roles表中law_dept角色的permissions字段是JSON字符串,但Seeder中写成了'["view_article","approve_article"]'(带单引号),导致Laravel Policy无法解析。
解决:修改database/seeders/RolePermissionSeeder.php,确保JSON无单引号:

DB::table('roles')->where('code', 'law_dept')->update([ 'permissions' => json_encode(['view_article', 'approve_article']) // 正确:无单引号 ]);

5.4 现象:定时发布功能失效,文章始终卡在scheduled状态

原因:Linux服务器未配置Cron任务触发Laravel Scheduler,而项目依赖php artisan schedule:run每分钟检查一次定时任务。
解决:在服务器执行:

# 编辑crontab crontab -e # 添加这一行(假设项目在/var/www/softnews) * * * * * cd /var/www/softnews && php artisan schedule:run >> /dev/null 2>&1

5.5 现象:导出Excel报表时中文乱码,显示为某句软文

原因:maatwebsite/excel包在Laravel 9中默认用UTF-8 BOM编码,而Excel for Mac不兼容。
解决:在导出控制器中强制指定编码:

return Excel::download(new ArticleExport($articles), 'articles.xlsx', \Maatwebsite\Excel\Excel::XLSX, [ 'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8' ]);

6. 二次开发实战:如何新增一个抖音图文平台对接?

6.1 平台接入四步法:从注册到上线的完整路径

抖音图文(非短视频)API需单独申请,接入本质是「三填两写一验证」:

步骤操作位置
填1在抖音开放平台获取client_key和client_secret.env新增DOUYIN_CLIENT_KEY=xxx
填2在后台「平台管理」页添加抖音图文平台,填入platform_id=24platforms表INSERT
填3配置抖音图文专属字段(如是否开启评论、封面图比例)article_platform_configs.config_json
写1实现DouyinPublishService类,继承AbstractPlatformServiceapp/Services/Platform/DouyinPublishService.php
写2在config/platforms.php中注册该服务'douyin' => DouyinPublishService::class
验证用Postman调用POST /api/v1/publish/douyin传测试数据routes/api.php新增路由

6.2 关键代码:抖音图文发布服务的核心实现

app/Services/Platform/DouyinPublishService.php需覆盖三个抽象方法:

<?php namespace App\Services\Platform; use App\Services\AbstractPlatformService; use Illuminate\Support\Facades\Http; class DouyinPublishService extends AbstractPlatformService { // 1. 获取平台认证Token(抖音用client_key+secret换access_token) protected function getAccessToken(): string { $response = Http::asForm()->post('https://open.douyin.com/oauth/access_token/', [ 'client_key' => config('services.douyin.client_key'), 'client_secret' => config('services.douyin.client_secret'), 'grant_type' => 'client_credential' ]); return $response->json()['access_token']; } // 2. 构建发布Payload(抖音要求JSON结构严格) protected function buildPayload(array $articleData): array { return [ 'title' => $articleData['title'], 'content' => $this->formatForDouyin($articleData['content']), // 过滤微信特有标签 'cover_image_url' => $articleData['cover_url'] ?? '', 'enable_comment' => $articleData['enable_comment'] ?? true, 'share_to_feed' => true ]; } // 3. 调用抖音API发布(注意:必须带access_token Header) public function publish(array $payload): array { $token = $this->getAccessToken(); $response = Http::withToken($token) ->timeout(30) ->post('https://open.douyin.com/article/publish/', $payload); if ($response->failed()) { throw new \Exception('Douyin publish failed: ' . $response->body()); } return [ 'platform_id' => 24, 'status' => 'published', 'external_id' => $response->json()['data']['article_id'] ?? null, 'publish_time' => now()->toDateTimeString() ]; } // 辅助方法:将通用富文本转为抖音兼容格式 private function formatForDouyin(string $content): string { // 移除微信特有的「阅读原文」链接 $content = preg_replace('/<a[^>]*?href="[^"]*?">阅读原文<\/a>/i', '', $content); // 将小红书话题标签#xxx转为抖音支持的#xxx(抖音不支持空格) $content = preg_replace('/#(\w+)\s+/i', '#$1 ', $content); return $content; } }

参数说明:config('services.douyin.client_key')读取.env中的DOUYIN_CLIENT_KEY;$response->json()['data']['article_id']是抖音返回的文章唯一ID,用于后续状态查询。

6.3 前端适配:在编辑页增加抖音专属配置面板

在resources/js/components/platform/DouyinConfigPanel.vue中:

<template> <el-form-item label="是否开启评论"> <el-switch v-model="form.enable_comment" /> </el-form-item> <el-form-item label="封面图比例"> <el-radio-group v-model="form.cover_ratio"> <el-radio label="1242x1660">竖版(推荐)</el-radio> <el-radio label="1242x698">横版</el-radio> </el-radio-group> </el-form-item> </template> <script setup> import { defineProps, defineEmits } from 'vue'; const props = defineProps(['form']); const emit = defineEmits(['update:form']); </script>

然后在EditorView.vue的平台配置区动态加载:

<!-- resources/js/views/EditorView.vue --> <component :is="`platform-${platform.code}-config-panel`" v-if="platform.code === 'douyin'" :form="platformConfig" @update:form="updatePlatformConfig" />

6.4 验证与上线:三步确认法保交付

  1. 本地验证:在routes/api.php临时加测试路由,用Postman发送JSON:

    POST /api/test-douyin { "title": "测试抖音图文", "content": "<p>这是正文</p>", "cover_url": "https://xxx.jpg" }

    检查返回article_id是否为抖音格式(如245678901234567890)。

  2. 灰度发布:在后台「平台管理」中将抖音平台状态设为disabled,仅对admin@softnews.local账号可见,用其账号创建一篇测试软文,走通审核流。

  3. 生产监控:在app/Console/Commands/CheckDouyinHealth.php中写心跳检测:

    // 每5分钟调用抖音token接口,失败则发告警邮件 $response = Http::get('https://open.douyin.com/oauth/access_token/...'); if ($response->failed()) { Mail::to('ops@softnews.local')->send(new DouyinApiDownAlert()); }

从那以后我每次新增平台,都强制走一遍「本地验证→灰度发布→健康监控」三步,哪怕只是加个字段。因为软文发布不是功能上线就结束,而是当第一篇客户稿件在抖音发出时,你得确保它真能被看见、被搜索、被转化。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询