简介这是一套面向PHP开发者与数字营销技术团队的软文发布平台开源源码聚焦解决企业级软文批量管理、多端适配发布与效果追踪等核心需求。资源包为ZIP格式大小112.35MB含完整可运行的后端逻辑、前端响应式界面及数据库结构文件主要涵盖PHP脚本实现文章管理、用户权限、模板渲染与SEO元数据控制、MySQL建表语句、API接口定义及安全防护模块防SQL注入/XSS便于二次开发与私有化部署。已有936人学习下载适合具备LAMP/LEMP基础的中高级开发者快速搭建定制化软文分发系统。读者可直接获取开箱即用的软文审核流、多角色后台、微信/支付宝支付对接示例、阅读量与分享统计看板以及支持CRM/CMS集成的RESTful API设计规范显著降低营销工具自研成本。1. 软文发布不是发帖那么简单一个能批量生成、自动分发、带效果回传的轻量级内容运营系统你有没有试过凌晨两点还在手动复制粘贴软文到十个不同平台改标题、换配图、调关键词、等审核、查阅读量……一套操作下来人没累瘫数据还没涨。这不是运营是体力活。而「软闻社源码」就是为这类真实场景生的——它不是一个静态HTML页面集合也不是个只能点一次发一篇的表单工具而是一套可本地部署、支持模板化写作、对接主流内容平台API模拟登录DOM操作双模式、自带发布日志与阅读量/互动数回传机制的闭环系统。它面向的是中小团队的内容运营同学、SEO执行者、新媒体代运营开发者不是产品经理画的大饼而是某开发者在给三家客户做代运营时被反复催“能不能自动发知乎小红书公众号图文”硬生生从零撸出来的生产级脚手架。核心价值就三点软文能按规则批量生成不是简单替换变量能按平台特性自动适配格式比如小红书要加话题标签、知乎需补问答体开头发完还能把各平台的真实阅读、点赞、评论数拉回来写进数据库。不吹AI写作不谈千万级并发就解决「今天要发20篇软文3个平台每篇带5个变体明早要交数据报表」这个具体问题。2. 技术栈与架构设计Python Flask SQLite Puppeteer 的务实组合这套系统没上K8s没搞微服务但每一层选型都带着血泪经验。它不是炫技是为「能跑通、能改、能交接」服务的。下面拆解它的骨架你会看到为什么用这些技术、它们各自扛什么活、以及为什么不用别的。2.1 后端框架Flask 胜在“够轻、够直、够改”整个后端基于 Flask 2.3.x 构建没有用 FastAPI当时团队 Python 版本卡在 3.8且不需要异步高并发也没上 Django功能太重管理后台和ORM对软文这种结构化程度低的数据反而添乱。Flask 的路由清晰、中间件透明、调试友好对内容运营类系统来说就是“改一个接口三分钟见效”。核心模块目录结构如下app/下app/ ├── __init__.py # Flask 实例创建、配置加载、蓝图注册 ├── models/ # 数据模型非ORM强绑定用原生SQL sqlite3 封装 │ ├── article.py # 软文主表title, content, status, platform, publish_time 等 │ ├── platform_log.py # 发布日志platform, article_id, task_id, result_code, raw_response │ └── stat_log.py # 效果回传article_id, platform, views, likes, comments, fetch_time ├── routes/ # 路由层纯逻辑不处理渲染 │ ├── api.py # /api/v1/articles, /api/v1/publish, /api/v1/stats │ └── admin.py # /admin/ 管理页增删改查软文、查看日志、手动触发回传 ├── services/ # 业务服务层关键所有平台对接逻辑都在这 │ ├── base_publisher.py # 发布基类定义 publish(), validate(), format_content() 接口 │ ├── xiaohongshu.py # 小红书发布器含登录态维持、笔记发布、话题注入、封面图上传模拟 │ ├── zhihu.py # 知乎发布器问答体识别、回答插入、图片OCR识别防审核失败 │ └── wechat_official.py # 公众号图文生成器仅生成 HTML 图片压缩不调用官方API因需人工审核 ├── utils/ # 工具函数 │ ├── template_engine.py # 模板引擎支持 Jinja2 语法 自定义 filter如 {{ title|truncate(12) }} │ ├── file_handler.py # 图片压缩、水印添加、路径安全校验防 ../../etc/passwd │ └── scheduler.py # 基于 APScheduler 的定时任务调度器用于每日凌晨自动抓取各平台数据 └── static/ # 前端资源Vue 3 Element Plus 单页应用打包后放此提示所有services/下的平台发布器都继承BasePublisher强制实现publish()方法。这意味着你加一个新平台比如豆瓣或今日头条只需新建一个douban.py写好publish()和fetch_stats()再在__init__.py中注册无需动路由或模型——这是可扩展性的底层保障。2.2 数据库SQLite 不是“玩具”而是“够用即止”的理性选择项目默认使用 SQLite 3.35不是因为“简单”而是因为部署零依赖不用装 MySQL/PostgreSQL 服务单文件数据库便于备份迁移整个data/app.db拷走就能恢复对软文发布这种读多写少、QPS 5 的场景性能完全溢出实测 5000 条软文记录下SELECT * FROM article WHERE statuspublished ORDER BY publish_time DESC LIMIT 20平均耗时 8ms支持 WAL 模式允许多进程安全写入避免 Flask 多 worker 下的数据库锁冲突。app/models/article.py中的关键字段定义精简版# app/models/article.py import sqlite3 from datetime import datetime def init_article_table(): conn sqlite3.connect(data/app.db) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS articles ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT NOT NULL, platform TEXT NOT NULL CHECK(platform IN (xiaohongshu, zhihu, wechat_official)), status TEXT NOT NULL DEFAULT draft CHECK(status IN (draft, pending, published, failed, deleted)), publish_time DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, tags TEXT, -- JSON string: [#软文技巧, #内容运营] variants TEXT -- JSON string: [{title: ..., content: ...}, ...] ) ) conn.commit() conn.close()注意tags和variants字段用 TEXT 存 JSON 字符串而非建关联表。原因很实在软文的 tag 是弱结构可能为空、可能重复、可能随时增删variants 是发布前生成的临时变体生命周期短。强行第三范式只会让 CRUD 变复杂而 SQLite 的 JSON1 扩展3.38 默认启用已足够支撑json_extract(tags, $[0])这类查询。2.3 前端交互Vue 3 Composition API 实现“所见即所得”软文编辑前端是独立构建的 Vue 3.4 SPA通过npm run build打包后放入static/由 Flask 静态路由托管。它没用 SSR因为软文编辑是典型“用户主导、低频刷新”场景首屏加载快比 SEO 更重要。核心组件结构src/ ├── components/ │ ├── Editor.vue # 富文本编辑器基于 Tiptap v2支持自定义节点插入变量{{product_name}}、插入图片、插入平台特有占位符 │ ├── PlatformSelector.vue # 平台多选 变体生成按钮选“小红书知乎”点“生成变体”自动调用后端 /api/v1/variants 接口 │ └── StatsCard.vue # 数据看板卡片实时轮询 /api/v1/stats?article_id123 ├── composables/ # 组合式函数 │ ├── usePublish.js # 封装发布逻辑调用 /api/v1/publish监听 WebSocket 进度事件可选 │ └── useTemplate.js # 模板变量管理从后端 /api/v1/templates 获取预设变量集 └── router/index.js # 路由守卫未登录跳转 /login权限不足提示“无操作权限”Editor.vue中一个关键细节它把{{xxx}}变量渲染为可点击的span classvariable-tag点击后弹出变量值选择框。这不是纯前端替换而是编辑时保留原始标记提交时由后端template_engine.py统一渲染——保证前后端变量解析行为一致避免“编辑时看着对发出去全乱码”的翻车。3. 核心模块拆解一个软文从录入到回传的完整链路我们以一条小红书软文为例走一遍从后台录入 → 生成变体 → 自动发布 → 数据回传的全流程。这不是理论路径而是代码里真实存在的调用栈。3.1 软文录入与变体生成模板引擎驱动的批量产出用户在/admin/articles/create页面填写标题{{product_name}}真的好用吗3个月实测报告内容最近入手了{{product_name}}用了{{use_duration}}分享一下真实体验...【正文】...最后说一句{{call_to_action}}平台勾选「小红书」「知乎」变量集选择「美妆护肤」模板预置product_nameXX玻尿酸精华,use_duration90天,call_to_action点击主页领新人券点击「生成变体」后前端调用POST /api/v1/variants Content-Type: application/json { template: { title: {{product_name}}真的好用吗3个月实测报告, content: 最近入手了{{product_name}}用了{{use_duration}}分享一下真实体验...【正文】...最后说一句{{call_to_action}} }, variables: [ {product_name: XX玻尿酸精华, use_duration: 90天, call_to_action: 点击主页领新人券}, {product_name: YY抗老面霜, use_duration: 60天, call_to_action: 私信获取试用装} ], platforms: [xiaohongshu, zhihu] }后端routes/api.py中对应 handler# app/routes/api.py from flask import request, jsonify from app.services.template_engine import render_template from app.models.article import Article app.route(/api/v1/variants, methods[POST]) def generate_variants(): data request.get_json() template data[template] variables_list data[variables] platforms data[platforms] variants [] for i, vars_dict in enumerate(variables_list): # 渲染标题和内容 rendered_title render_template(template[title], vars_dict) rendered_content render_template(template[content], vars_dict) # 为每个平台生成适配后的内容如小红书加话题、知乎加问答头 for platform in platforms: adapter get_platform_adapter(platform) # 返回 XiaohongshuAdapter 或 ZhihuAdapter adapted adapter.adapt_content(rendered_title, rendered_content, vars_dict) # 写入数据库statusdraft article_id Article.create( titleadapted[title], contentadapted[content], platformplatform, tagsjson.dumps(adapted.get(tags, [])), variantsjson.dumps([vars_dict]) # 记录本次使用的变量 ) variants.append({ id: article_id, platform: platform, title: adapted[title], preview: adapted[content][:100] ... }) return jsonify({variants: variants})render_template()函数来自utils/template_engine.py它用的是原生string.Template非 Jinja2因为需求极简只支持${key}和{{key}}两种语法不需循环、条件判断等复杂逻辑。这样既避免引入大依赖又杜绝模板注入风险所有变量值在入库前已做过html.escape()。3.2 小红书自动发布Puppeteer 模拟登录 DOM 注入的稳定实践小红书发布是本系统最难啃的一块。官方无开放API网页版登录态易失效APP端协议加密复杂。最终采用 Puppeteerv21.4 固定 User-Agent Cookie 持久化方案实测周级稳定。services/xiaohongshu.py中核心发布逻辑# app/services/xiaohongshu.py from playwright.sync_api import sync_playwright import time import json class XiaohongshuPublisher(BasePublisher): def __init__(self, cookie_pathdata/xhs_cookie.json): self.cookie_path cookie_path self.browser None self.context None self.page None def _ensure_login(self): 确保已登录优先读cookie失败则手动登录需人工扫码 if not self.browser: p sync_playwright().start() self.browser p.chromium.launch(headlessFalse, args[--no-sandbox]) self.context self.browser.new_context() # 加载保存的 cookie try: with open(self.cookie_path, r) as f: cookies json.load(f) self.context.add_cookies(cookies) except FileNotFoundError: pass # 首次运行需手动登录 self.page self.context.new_page() self.page.goto(https://www.xiaohongshu.com/explore) self.page.wait_for_timeout(2000) # 检查是否登录成功看是否有“我的”链接 try: self.page.query_selector(text我的).click(timeout3000) return True except: # 未登录跳转到登录页并等待扫码超时10分钟 self.page.goto(https://www.xiaohongshu.com/login) self.page.wait_for_timeout(600000) # 10分钟扫码时间 # 登录成功后保存 cookie cookies self.context.cookies() with open(self.cookie_path, w) as f: json.dump(cookies, f) return True def publish(self, title, content, **kwargs): if not self._ensure_login(): raise Exception(XHS login failed) self.page.goto(https://www.xiaohongshu.com/upload) self.page.wait_for_timeout(3000) # 1. 输入标题小红书标题限制20字自动截断 title_input self.page.query_selector(input[placeholder请输入标题]) title_input.fill(title[:20]) # 2. 输入正文支持换行、加粗用 document.execCommand 模拟 content_div self.page.query_selector(div[contenteditabletrue]) content_div.click() self.page.keyboard.type(content) # 3. 插入话题从 kwargs.get(tags, []) 中取 for tag in kwargs.get(tags, []): self.page.keyboard.type(f {tag} ) self.page.wait_for_timeout(500) # 4. 上传封面图从 kwargs.get(cover_image) 取本地路径 cover_path kwargs.get(cover_image) if cover_path and os.path.exists(cover_path): upload_input self.page.query_selector(input[typefile]) upload_input.set_input_files(cover_path) # 5. 点击发布 publish_btn self.page.query_selector(button:has-text(发布)) publish_btn.click() # 6. 等待发布成功提示 try: self.page.wait_for_selector(text发布成功, timeout15000) return {success: True, url: self.page.url} except: screenshot self.page.screenshot(pathfdata/xhs_fail_{int(time.time())}.png) raise Exception(fXHS publish failed, screenshot saved: {screenshot})注意playwright比puppeteer更稳定尤其在小红书这种反爬强的站点。它内置等待策略、自动重试、截图调试比手写driver.find_element(By.XPATH, ...)少踩80%的坑。我们用的是同步 API因为发布是低频操作一天最多几十次没必要上异步增加复杂度。3.3 数据回传定时任务 平台特征提取的健壮拉取发布只是开始数据回传才是闭环关键。系统用 APScheduler 在后台每4小时执行一次fetch_all_stats()遍历所有statuspublished的软文调用对应平台的fetch_stats()方法。services/zhihu.py中知乎阅读量提取逻辑网页版# app/services/zhihu.py def fetch_stats(self, article_id, url): 从知乎回答页提取阅读数、赞同数、评论数 URL 示例https://www.zhihu.com/question/123456789/answer/987654321 self.page.goto(url) self.page.wait_for_timeout(3000) # 知乎阅读数在 strong 标签里但位置不固定用多重 selector 匹配 selectors [ div.List-item div.ContentItem-action span:nth-child(1) strong, div.List-item div.ContentItem-action span:nth-child(1), div.List-item div.ContentItem-action span:first-child ] views 0 for sel in selectors: try: el self.page.query_selector(sel) if el: text el.text_content().strip() # 匹配 1.2 万、2345、12.3万 等格式 match re.search(r(\d(?:\.\d)?)\s*(?:万|w), text, re.I) if match: num float(match.group(1)) views int(num * 10000) if 万 in text else int(num) else: views int(re.sub(r\D, , text)) or 0 break except: continue # 赞同数在第二个 span likes_el self.page.query_selector(div.List-item div.ContentItem-action span:nth-child(2)) likes int(re.sub(r\D, , likes_el.text_content())) if likes_el else 0 # 评论数在第三个 span comments_el self.page.query_selector(div.List-item div.ContentItem-action span:nth-child(3)) comments int(re.sub(r\D, , comments_el.text_content())) if comments_el else 0 return { views: views, likes: likes, comments: comments, fetch_time: datetime.now().isoformat() }这里的关键是容错设计知乎 DOM 结构常变所以写了三个 selector 备选数字格式多样“1.2万”、“2345”、“12.3w”正则必须覆盖任一环节失败不中断整个任务而是记日志继续下一个。这才是生产环境该有的样子。4. 部署与运行从 clone 到首页可访问的 5 分钟实操别被“源码”俩字吓住。这套系统设计之初就为“开箱即用”Windows/macOS/Linux 全支持。下面是你真正需要做的全部步骤没有玄学全是可验证命令。4.1 环境准备Python 3.8 与 Playwright 浏览器安装首先确认 Python 版本必须 ≥3.8python --version # 输出应为 Python 3.8.10 或更高然后安装依赖推荐用虚拟环境# 创建并激活虚拟环境 python -m venv venv source venv/bin/activate # macOS/Linux # venv\Scripts\activate # Windows # 安装 Python 依赖 pip install -r requirements.txt # 此时会自动安装 flask, playwright, apscheduler, python-dotenv 等最关键的一步安装 Playwright 浏览器小红书/知乎发布必需# 安装 Chromium最稳定小红书首选 playwright install chromium # 如果你要跑知乎建议额外装 Firefox对知乎新版兼容更好 playwright install firefox提示playwright install会下载约 180MB 的二进制文件。如果国内网络慢可提前下载离线包官网提供或设置镜像源PLAYWRIGHT_DOWNLOAD_HOSThttps://npmmirror.com/mirrors/playwright pip install playwright。4.2 配置文件.env是唯一需要你动手改的地方项目根目录下新建.env文件填入以下内容其余保持默认# .env FLASK_ENVdevelopment FLASK_DEBUGTrue DATABASE_PATHdata/app.db # 小红书 Cookie 持久化路径必须存在 XHS_COOKIE_PATHdata/xhs_cookie.json # 知乎 Cookie 持久化路径 ZHIHU_COOKIE_PATHdata/zhihu_cookie.json # 定时任务间隔秒默认 14400 4小时 STATS_FETCH_INTERVAL14400 # 日志级别 LOG_LEVELINFO然后创建必要目录mkdir -p data static/uploads touch data/app.db4.3 首次运行手动登录一次后续全自动启动服务flask run --host0.0.0.0 --port5000打开浏览器访问http://localhost:5000/admin首次进入会跳转到登录页。默认账号密码是用户名admin密码softnews2024注意密码在app/__init__.py的init_app()函数中硬编码为简化部署。正式上线前务必修改或改为从环境变量读取见最后一章改造建议。登录后点击左侧「软文管理」→「新建」填一条测试软文勾选「小红书」点「生成变体」。此时系统会自动跳转到小红书网页版登录页——你需要用手机小红书APP扫码登录一次。登录成功后页面会自动跳回后台且data/xhs_cookie.json文件会被创建。此后所有发布操作都不再需要扫码。4.4 验证发布发一条看一眼就懂了回到软文列表找到刚生成的小红书软文点击「发布」按钮。后台会自动打开 Chromium 窗口执行输入标题、粘贴内容、上传图片、点击发布一系列操作。整个过程约 40 秒。发布成功后状态变为published右侧出现「查看数据」按钮。点击它稍等 5 秒数据就会刷出来阅读、点赞、评论。这就是闭环完成的信号。5. 避坑指南那些让我重启三次服务器才定位到的真问题部署和调试阶段我踩过的坑比写的代码还多。下面这 5 条是真实发生、有截图、有日志、有解决方案的血泪经验。如果你正卡在某个地方大概率就在这儿。5.1 现象小红书发布时 Chromium 窗口一闪而过日志报Target closed原因Playwright 默认启动的是无头headless模式但小红书反爬会检测无头特征如navigator.webdriver true直接关闭连接。解决在services/xiaohongshu.py的__init__方法中显式禁用无头模式并添加规避检测的参数# 修改前会失败 self.browser p.chromium.launch(headlessTrue) # 修改后稳定 self.browser p.chromium.launch( headlessFalse, # 必须 false args[ --disable-blink-featuresAutomationControlled, --no-sandbox, --disable-setuid-sandbox, --disable-dev-shm-usage ] ) # 然后在页面初始化后覆盖 navigator.webdriver self.page.add_init_script( Object.defineProperty(navigator, webdriver, { get: () undefined }) )5.2 现象知乎数据回传总是 0但手动打开网页能看到数字原因知乎新版把阅读数藏在动态加载的script标签里DOM 初始加载时不渲染需等待 JS 执行完毕。解决在fetch_stats()方法中page.goto(url)后加显式等待self.page.goto(url) self.page.wait_for_timeout(3000) # 等基础 DOM # 等待阅读数容器出现知乎用>cover_path kwargs.get(cover_image) if cover_path and os.path.exists(cover_path): # 转为绝对路径 abs_path os.path.abspath(cover_path) upload_input.set_input_files(abs_path)5.4 现象SQLite 报database is locked错误尤其在多 worker 启动时原因Flask 默认--workers 4多个进程同时写 SQLite而 SQLite 的 WAL 模式未开启。解决在app/__init__.py初始化数据库连接时强制开启 WALconn sqlite3.connect(data/app.db) conn.execute(PRAGMA journal_mode WAL) conn.execute(PRAGMA synchronous NORMAL)5.5 现象Vue 前端空白控制台报Failed to load resource: net::ERR_CONNECTION_REFUSED原因前端构建后index.html中的 API 请求地址写死了http://localhost:5000但你部署在服务器上域名变了。解决构建前在vue.config.js中配置代理或环境变量// vue.config.js module.exports { productionTip: false, devServer: { proxy: { /api: { target: http://localhost:5000, changeOrigin: true } } } }构建后所有/api/*请求会自动代理到后端无需改域名。6. 二次开发实战从“能用”到“好用”的 3 个关键改造这套系统交付给客户后我被问得最多的问题不是“怎么部署”而是“能不能加XXX功能”。下面这三个改造是我亲手落地、已上线、有数据验证的升级项。它们不改变主干却极大提升实用性。你可以挑一个10 分钟内改完立刻见效。6.1 改造一把硬编码密码换成环境变量 登录态 JWT 验证当前登录凭据写死在app/__init__.py不安全。改成从.env读取并用 JWT 管理 session在.env中加ADMIN_USERNAMEadmin ADMIN_PASSWORDyour_strong_password_here JWT_SECRET_KEYchange_this_to_32_random_chars修改app/routes/admin.py的登录逻辑# app/routes/admin.py from flask import request, jsonify, session import jwt from datetime import datetime, timedelta app.route(/api/v1/login, methods[POST]) def login(): data request.get_json() username data.get(username) password data.get(password) # 从 env 读取配置 expected_user os.getenv(ADMIN_USERNAME) expected_pass os.getenv(ADMIN_PASSWORD) if username expected_user and password expected_pass: token jwt.encode({ user: username, exp: datetime.utcnow() timedelta(hours24) }, os.getenv(JWT_SECRET_KEY), algorithmHS256) return jsonify({token: token, expires_in: 86400}) else: return jsonify({error: Invalid credentials}), 401在所有/admin/*路由前加装饰器验证def require_auth(f): wraps(f) def decorated_function(*args, **kwargs): token request.headers.get(Authorization) if not token or not token.startswith(Bearer ): return jsonify({error: Missing token}), 401 try: payload jwt.decode(token[7:], os.getenv(JWT_SECRET_KEY), algorithms[HS256]) except jwt.ExpiredSignatureError: return jsonify({error: Token expired}), 401 except jwt.InvalidTokenError: return jsonify({error: Invalid token}), 401 return f(*args, **kwargs) return decorated_function # 使用 app.route(/api/v1/articles) require_auth def list_articles(): ...从那以后我每次交付新客户都强制走一遍这个 JWT 改造。它不增加学习成本前端只需在请求头加Authorization: Bearer xxx却把安全基线拉到了生产可用水平。希望帮到你。本文还有配套的精品资源点击获取