参考文章:PHP实现独立Discuz站外发帖(直连操作数据库)_discuz 发帖api-CSDN博客
简单改造了一下,适配我自己的需求
有一个站点存在多个采集站,我想通过主站拿标题,采集站拿内容
使用到的sql如下
CREATE TABLE `pre_forum_post_sync` (
`id` int(0) NOT NULL AUTO_INCREMENT,
`foreign_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`foreign2_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`foreign3_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`tid` int(0) NOT NULL,
`pid` int(0) NOT NULL,
`update_time` int(0) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `foreign_id`(`foreign_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
CREATE TABLE `pre_wechat_push_log` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`tid` INT NOT NULL COMMENT '主题ID',
`pid` INT NOT NULL COMMENT '帖子ID',
`push_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '推送时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
php api代码
同步主站标题api
topic_only.php
<?php
date_default_timezone_set('Asia/Shanghai');
header('Content-Type: application/json');
// 定义允许的 accesskey 和 accesssecret
define('ACCESS_KEY', 'my-access-key');
define('ACCESS_SECRET', 'my-access-secret');
// 获取并统一转换请求头为小写
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
// 检查 accesskey 和 accesssecret 是否存在且匹配
if (!isset($headers['accesskey']) || !isset($headers['accesssecret'])) {
http_response_code(403);
echo json_encode(['status' => 'error', 'message' => 'Missing AccessKey or AccessSecret']);
exit;
}
if ($headers['accesskey'] !== ACCESS_KEY || $headers['accesssecret'] !== ACCESS_SECRET) {
http_response_code(403);
echo json_encode(['status' => 'error', 'message' => 'Invalid AccessKey or AccessSecret']);
exit;
}
// 接收并解析 JSON 输入
$input = file_get_contents('php://input');
$data = json_decode($input, true);
// 检查必要字段
if (!isset($data['fid'], $data['title'], $data['content'], $data['foreign_id'])) {
echo json_encode(['status' => 'error', 'message' => 'Missing required parameters']);
exit;
}
$fid = $data['fid'];
$title = $data['title'];
$content = $data['content'];
$foreign_id = $data['foreign_id'];
$post_time = isset($data['post_time']) ? strtotime($data['post_time']) : time(); // 新增行
try {
$test = new insert_content('1', 'admin', $post_time); // 使用传入时间
$result = $test->sync_post($fid, $title, $content, $foreign_id);
echo json_encode(['status' => 'success', 'data' => $result]);
} catch (Exception $e) {
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}
class insert_content {
private $_prefix = 'pre'; // Discuz 表前缀
private $_con;
private $_tid;
private $_fid;
private $_pid;
private $_authorid;
private $_author;
private $_time;
private $_title;
private $_content;
public function __construct($authorid, $author, $time = null) {
$this->_authorid = $authorid;
$this->_author = $author;
$this->_time = $time ?? time();
$this->_con = mysqli_connect('mysql', 'root', 'pass', 'ultrax', 3306);
if (!$this->_con) {
throw new Exception("Database connection failed");
}
}
/**
* 同步发帖方法
*
* @param $fid
* @param $title
* @param $content
* @param $foreign_id
* @return array
* @throws Exception
*/
public function sync_post($fid, $title, $content, $foreign_id) {
$this->_fid = $fid;
$this->_title = $title;
$this->_content = $content;
// 先检查是否已同步
$sql = "SELECT tid, pid FROM {$this->_prefix}_forum_post_sync WHERE foreign_id='{$foreign_id}'";
$res = mysqli_query($this->_con, $sql);
if ($row = mysqli_fetch_assoc($res)) {
// 已存在,仅更新标题
$this->_tid = $row['tid'];
$this->_pid = $row['pid'];
// 仅更新主题表中的标题
$sql = "UPDATE {$this->_prefix}_forum_thread SET subject='{$this->_title}', lastpost='{$this->_time}' WHERE tid='{$this->_tid}'";
if (!mysqli_query($this->_con, $sql)) {
throw new Exception("Update thread failed: " . mysqli_error($this->_con));
}
// 可选:更新同步记录时间
$sql = "UPDATE {$this->_prefix}_forum_post_sync SET update_time='{$this->_time}' WHERE foreign_id='{$foreign_id}'";
if (!mysqli_query($this->_con, $sql)) {
throw new Exception("Update sync record failed: " . mysqli_error($this->_con));
}
return [
'action' => 'updated',
'tid' => $this->_tid,
'pid' => $this->_pid
];
} else {
// 不存在,新建帖子和主题
return $this->insert_new_post($foreign_id);
}
}
private function insert_new_post($foreign_id) {
// 第一步:插入主题表
$sql = "INSERT INTO {$this->_prefix}_forum_thread SET
fid='{$this->_fid}',
authorid='{$this->_authorid}',
author='{$this->_author}',
subject='{$this->_title}',
dateline='{$this->_time}', lastpost='{$this->_time}',
lastposter='{$this->_author}'";
if (!mysqli_query($this->_con, $sql)) {
throw new Exception("Insert thread failed: " . mysqli_error($this->_con));
}
$this->_tid = mysqli_insert_id($this->_con);
// 第二步:插入分表协调表
if (!mysqli_query($this->_con, "INSERT INTO {$this->_prefix}_forum_post_tableid VALUES ()")) {
throw new Exception("Insert post tableid failed: " . mysqli_error($this->_con));
}
$this->_pid = mysqli_insert_id($this->_con);
// 第三步:获取 position 并插入帖子表
$res = mysqli_query($this->_con, "SELECT MAX(position) AS max_pos FROM {$this->_prefix}_forum_post WHERE tid='{$this->_tid}'");
$row = mysqli_fetch_assoc($res);
$position = $row['max_pos'] ? $row['max_pos'] + 1 : 1;
$sql = "INSERT INTO {$this->_prefix}_forum_post SET
pid='{$this->_pid}',
fid='{$this->_fid}',
tid='{$this->_tid}',
author='{$this->_author}',
authorid='{$this->_authorid}',
subject='{$this->_title}',
dateline='{$this->_time}',
message='{$this->_content}',
premsg='',
position={$position}";
if (!mysqli_query($this->_con, $sql)) {
throw new Exception("Insert post failed: " . mysqli_error($this->_con));
}
// 第四步:更新版块统计
$sql = "UPDATE {$this->_prefix}_forum_forum SET posts=posts+1, threads=threads+1 WHERE fid='{$this->_fid}'";
if (!mysqli_query($this->_con, $sql)) {
throw new Exception("Update forum stats failed: " . mysqli_error($this->_con));
}
// 第五步:更新用户统计
$sql = "UPDATE {$this->_prefix}_common_member_count SET posts=posts+1, threads=threads+1 WHERE uid='{$this->_authorid}'";
if (!mysqli_query($this->_con, $sql)) {
throw new Exception("Update user stats failed: " . mysqli_error($this->_con));
}
// 插入同步记录
$sql = "INSERT INTO {$this->_prefix}_forum_post_sync SET
foreign_id='{$foreign_id}',
tid='{$this->_tid}',
pid='{$this->_pid}',
update_time='{$this->_time}'";
if (!mysqli_query($this->_con, $sql)) {
throw new Exception("Insert sync record failed: " . mysqli_error($this->_con));
}
return [
'action' => 'created',
'tid' => $this->_tid,
'pid' => $this->_pid
];
}
public function __destruct() {
if ($this->_con instanceof mysqli) {
$this->_con->close();
}
}
}
使用方法
curl --location 'http://d.example.com/topic_only.php' \
--header 'AccessKey: my-access-key' \
--header 'AccessSecret: my-access-secret' \
--header 'Content-Type: application/json' \
--data '{
"fid": "2",
"title": "测试标题0816",
"content": "这是一个测试内容。",
"foreign_id": "9966",
"post_time": "2024-08-16 10:00:00"
}
'
跟参考的代码相比,这里做了一个简单的鉴权,AccessKey AccessSecret与php代码里保持一致才可调用。本来像参考oss写个签名方法,还是先不搞这么复杂了。
由于采集站没有源站帖子id,则写一个根据标题更新内容的api
update_by_title.php
<?php
date_default_timezone_set('Asia/Shanghai');
header('Content-Type: application/json');
// 定义允许的 accesskey 和 accesssecret
define('ACCESS_KEY', 'my-access-key');
define('ACCESS_SECRET', 'my-access-secret');
// 获取并统一转换请求头为小写
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
// 检查 accesskey 和 accesssecret 是否存在且匹配
if (!isset($headers['accesskey']) || !isset($headers['accesssecret'])) {
http_response_code(403);
echo json_encode(['status' => 'error', 'message' => 'Missing AccessKey or AccessSecret']);
exit;
}
if ($headers['accesskey'] !== ACCESS_KEY || $headers['accesssecret'] !== ACCESS_SECRET) {
http_response_code(403);
echo json_encode(['status' => 'error', 'message' => 'Invalid AccessKey or AccessSecret']);
exit;
}
// 接收并解析 JSON 输入
$input = file_get_contents('php://input');
$data = json_decode($input, true);
// 检查必要字段
if (!isset($data['fid'], $data['title'], $data['content']) ||
(!isset($data['foreign2_id']) && !isset($data['foreign3_id']))) {
echo json_encode(['status' => 'error', 'message' => 'Missing required parameters: fid, title, content, and either foreign2_id or foreign3_id']);
exit;
}
$fid = $data['fid'];
$title = $data['title'];
$content = $data['content'];
$foreign2_id = $data['foreign2_id'] ?? null;
$foreign3_id = $data['foreign3_id'] ?? null;
try {
// 初始化数据库连接
$con = mysqli_connect('mysql', 'root', 'pass', 'ultrax', 3306);
if (!$con) {
throw new Exception("Database connection failed");
}
// 查找 title 对应的主题 ID(tid)
$sql = "SELECT tid FROM pre_forum_thread WHERE subject = '{$title}' AND fid = '{$fid}' LIMIT 1";
$res = mysqli_query($con, $sql);
if (!$res || mysqli_num_rows($res) === 0) {
throw new Exception("No thread found with the given title and fid");
}
$row = mysqli_fetch_assoc($res);
$tid = $row['tid'];
// 获取主帖 pid(通常 position = 1)
$sql = "SELECT pid FROM pre_forum_post WHERE tid = '{$tid}' AND position = 1 ORDER BY dateline DESC LIMIT 1";
$res = mysqli_query($con, $sql);
if (!$res || mysqli_num_rows($res) === 0) {
throw new Exception("Main post not found for the thread");
}
$row = mysqli_fetch_assoc($res);
$pid = $row['pid'];
// 更新帖子内容
$sql = "UPDATE pre_forum_post SET message = '{$content}' WHERE pid = '{$pid}'";
if (!mysqli_query($con, $sql)) {
throw new Exception("Update post content failed: " . mysqli_error($con));
}
// 更新同步记录表中的 foreign2_id
if (isset($data['foreign2_id'])) {
// 更新 foreign2_id 字段
$sql = "UPDATE pre_forum_post_sync
SET foreign2_id = '{$foreign2_id}'
WHERE tid = '{$tid}' AND pid = '{$pid}'";
} elseif (isset($data['foreign3_id'])) {
// 新增 foreign3_id 字段更新逻辑
$sql = "UPDATE pre_forum_post_sync
SET foreign3_id = '{$foreign3_id}'
WHERE tid = '{$tid}' AND pid = '{$pid}'";
}
if (!mysqli_query($con, $sql)) {
throw new Exception("Update sync record failed: " . mysqli_error($con));
}
$updateFields = [];
if (isset($data['foreign2_id'])) {
$updateFields[] = 'foreign2_id';
}
if (isset($data['foreign3_id'])) {
$updateFields[] = 'foreign3_id';
}
echo json_encode([
'status' => 'success',
'data' => [
'tid' => $tid,
'pid' => $pid,
'updated_fields' => $updateFields,
'message' => 'Post updated successfully'
]
]);
} catch (Exception $e) {
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}
使用方法
curl --location 'https://d.example.com/update_by_title.php' \
--header 'AccessKey: my-access-key' \
--header 'AccessSecret: my-access-secret' \
--header 'Content-Type: application/json' \
--data '{
"fid": "2",
"title": "测试标题",
"content": "这是一个测试内容8888872",
"foreign3_id": "123456"
}
'
这里 "foreign3_id": "123456"也可以改成 "foreign2_id": "123456",当有多个采集站时扩充数据库字段修改代码即可。
推送新帖到企微群api
wechat_pusher.php
<?php
date_default_timezone_set('Asia/Shanghai');
header('Content-Type: application/json');
// 获取并统一转换请求头为小写
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
// 检查 accesskey 和 accesssecret 是否存在且匹配
if (!isset($headers['accesskey']) || !isset($headers['accesssecret'])) {
http_response_code(403);
die(json_encode(['status' => 'error', 'message' => 'Missing AccessKey or AccessSecret']));
}
define('ACCESS_KEY', 'my-access-key');
define('ACCESS_SECRET', 'my-access-secret');
if ($headers['accesskey'] !== ACCESS_KEY || $headers['accesssecret'] !== ACCESS_SECRET) {
http_response_code(403);
die(json_encode(['status' => 'error', 'message' => 'Invalid AccessKey or AccessSecret']));
}
// 微信机器人 Webhook 地址(替换为你的)
$webhook_url = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=youruuid';
// 数据库连接配置
$con = mysqli_connect('mysql', 'root', 'pass', 'ultrax', 3306);
if (!$con) {
die(json_encode(['status' => 'error', 'message' => 'Database connection failed']));
}
// 获取当前时间和30分钟前的时间
$now = date('Y-m-d H:i:s');
$thirty_minutes_ago = date('Y-m-d H:i:s', strtotime('-30 minutes'));
// 查询最近30分钟发布的帖子(只取主帖 position = 1)
$sql = "
SELECT
p.tid,
p.pid,
t.subject AS title,
p.message AS content
FROM
pre_forum_post p
JOIN
pre_forum_thread t ON p.tid = t.tid
WHERE
p.dateline >= UNIX_TIMESTAMP('$thirty_minutes_ago')
AND p.position = 1
AND NOT EXISTS (
SELECT 1 FROM pre_wechat_push_log l
WHERE l.tid = p.tid AND l.pid = p.pid
)
";
$res = mysqli_query($con, $sql);
if (!$res) {
die(json_encode(['status' => 'error', 'message' => 'Query failed: ' . mysqli_error($con)]));
}
while ($row = mysqli_fetch_assoc($res)) {
$title = trim($row['title']);
$content = trim($row['content']);
// 屏蔽以“这是主题内容:”开头的帖子
if (mb_strpos($content, '这是主题内容:') === 0) {
continue; // 跳过该帖子
}
// 替换由反引号包裹的 [img] 标签,并转换为 [图片1](url) 的 Markdown 链接格式
$imgCount = 0;
$content = preg_replace_callback(
'/`\[img\](.+?)\[\/img\]`/is', // 匹配反引号内的 [img]标签[/img]
function ($matches) use (&$imgCount) {
$imgCount++;
$url = htmlspecialchars($matches[1]);
return "[图片{$imgCount}]({$url})";
},
$content
);
// 构造微信消息体(Markdown 格式)
$message = "### 新帖子通知\n\n**标题:** {$title}\n\n**内容预览:**\n\n" . mb_substr($content, 0, 200);
$postData = json_encode([
'msgtype' => 'markdown',
'markdown' => [
'content' => $message
]
]);
// 发送请求
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $webhook_url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
// 记录推送日志
$insert_sql = "
INSERT INTO pre_wechat_push_log (tid, pid)
VALUES ({$row['tid']}, {$row['pid']})
";
if (!mysqli_query($con, $insert_sql)) {
error_log("Failed to log push record for tid={$row['tid']} pid={$row['pid']}: " . mysqli_error($con));
}
}
echo json_encode(['status' => 'success', 'message' => 'Push job completed']);
?>
调用
curl --location --request POST 'https://d.example.com/wechat_pusher.php' \
--header 'AccessKey: my-access-key' \
--header 'AccessSecret: my-access-secret' \
--header 'Content-Type: application/json'
这里用post get都可以。
把这3个php文件防弹discuz安装目录下,后续就可以通过调用API实现帖子同步和新帖通知操作了。