朋友圈表格导出

This commit is contained in:
wong
2025-11-28 16:03:56 +08:00
parent 5087190816
commit 5691d78004
6 changed files with 654 additions and 15 deletions

View File

@@ -1,4 +1,6 @@
import request from "@/api/request";
import axios from "axios";
import { useUserStore } from "@/store/module/user";
// 获取微信号详情
export function getWechatAccountDetail(id: string) {
@@ -50,3 +52,68 @@ export function transferWechatFriends(params: {
}) {
return request("/v1/wechats/transfer-friends", params, "POST");
}
// 导出朋友圈接口(直接下载文件)
export async function exportWechatMoments(params: {
wechatId: string;
keyword?: string;
type?: number;
startTime?: string;
endTime?: string;
}): Promise<void> {
const { token } = useUserStore.getState();
const baseURL =
(import.meta as any).env?.VITE_API_BASE_URL || "/api";
// 构建查询参数
const queryParams = new URLSearchParams();
queryParams.append("wechatId", params.wechatId);
if (params.keyword) {
queryParams.append("keyword", params.keyword);
}
if (params.type !== undefined) {
queryParams.append("type", params.type.toString());
}
if (params.startTime) {
queryParams.append("startTime", params.startTime);
}
if (params.endTime) {
queryParams.append("endTime", params.endTime);
}
try {
const response = await axios.get(
`${baseURL}/v1/wechats/moments/export?${queryParams.toString()}`,
{
responseType: "blob",
headers: {
Authorization: token ? `Bearer ${token}` : undefined,
},
}
);
// 创建下载链接
const blob = new Blob([response.data]);
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
// 从响应头获取文件名,如果没有则使用默认文件名
const contentDisposition = response.headers["content-disposition"];
let fileName = "朋友圈导出.xlsx";
if (contentDisposition) {
const fileNameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
if (fileNameMatch && fileNameMatch[1]) {
fileName = decodeURIComponent(fileNameMatch[1].replace(/['"]/g, ""));
}
}
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (error: any) {
throw new Error(error.response?.data?.message || error.message || "导出失败");
}
}

View File

@@ -845,6 +845,56 @@
margin-top: 20px;
}
.popup-footer {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
.export-form {
margin-top: 20px;
.form-item {
margin-bottom: 20px;
label {
display: block;
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 8px;
}
.type-selector {
display: flex;
gap: 8px;
flex-wrap: wrap;
.type-option {
padding: 8px 16px;
border: 1px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
color: #666;
cursor: pointer;
transition: all 0.2s;
background: white;
&:hover {
border-color: #1677ff;
color: #1677ff;
}
&.active {
background: #1677ff;
border-color: #1677ff;
color: white;
}
}
}
}
}
.restrictions-detail {
.restriction-detail-item {
display: flex;

View File

@@ -11,6 +11,7 @@ import {
Avatar,
Tag,
Switch,
DatePicker,
} from "antd-mobile";
import { Input, Pagination } from "antd";
import NavCommon from "@/components/NavCommon";
@@ -27,6 +28,7 @@ import {
transferWechatFriends,
getWechatAccountOverview,
getWechatMoments,
exportWechatMoments,
} from "./api";
import DeviceSelection from "@/components/DeviceSelection";
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
@@ -64,6 +66,16 @@ const WechatAccountDetail: React.FC = () => {
const [momentsError, setMomentsError] = useState<string | null>(null);
const MOMENTS_LIMIT = 10;
// 导出相关状态
const [showExportPopup, setShowExportPopup] = useState(false);
const [exportKeyword, setExportKeyword] = useState("");
const [exportType, setExportType] = useState<number | undefined>(undefined);
const [exportStartTime, setExportStartTime] = useState<Date | null>(null);
const [exportEndTime, setExportEndTime] = useState<Date | null>(null);
const [showStartTimePicker, setShowStartTimePicker] = useState(false);
const [showEndTimePicker, setShowEndTimePicker] = useState(false);
const [exportLoading, setExportLoading] = useState(false);
// 获取基础信息
const fetchAccountInfo = useCallback(async () => {
if (!id) return;
@@ -370,6 +382,50 @@ const WechatAccountDetail: React.FC = () => {
fetchMomentsList(momentsPage + 1, true);
};
// 处理朋友圈导出
const handleExportMoments = useCallback(async () => {
if (!id) {
Toast.show({ content: "微信ID不存在", position: "top" });
return;
}
setExportLoading(true);
try {
// 格式化时间
const formatDate = (date: Date | null): string | undefined => {
if (!date) return undefined;
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
};
await exportWechatMoments({
wechatId: id,
keyword: exportKeyword || undefined,
type: exportType,
startTime: formatDate(exportStartTime),
endTime: formatDate(exportEndTime),
});
Toast.show({ content: "导出成功", position: "top" });
setShowExportPopup(false);
// 重置筛选条件
setExportKeyword("");
setExportType(undefined);
setExportStartTime(null);
setExportEndTime(null);
} catch (error: any) {
console.error("导出失败:", error);
Toast.show({
content: error.message || "导出失败,请重试",
position: "top",
});
} finally {
setExportLoading(false);
}
}, [id, exportKeyword, exportType, exportStartTime, exportEndTime]);
const formatMomentDateParts = (dateString: string) => {
const date = new Date(dateString);
if (Number.isNaN(date.getTime())) {
@@ -810,7 +866,10 @@ const WechatAccountDetail: React.FC = () => {
<span className={style["action-icon-video"]}></span>
<span className={style["action-text"]}></span>
</div>
<div className={style["action-button-dark"]}>
<div
className={style["action-button-dark"]}
onClick={() => setShowExportPopup(true)}
>
<span className={style["action-icon-export"]}></span>
<span className={style["action-text-light"]}></span>
</div>
@@ -1023,6 +1082,153 @@ const WechatAccountDetail: React.FC = () => {
</div>
</Popup>
{/* 朋友圈导出弹窗 */}
<Popup
visible={showExportPopup}
onMaskClick={() => setShowExportPopup(false)}
bodyStyle={{ borderRadius: "16px 16px 0 0" }}
>
<div className={style["popup-content"]}>
<div className={style["popup-header"]}>
<h3></h3>
<Button
size="small"
fill="outline"
onClick={() => setShowExportPopup(false)}
>
</Button>
</div>
<div className={style["export-form"]}>
{/* 关键词搜索 */}
<div className={style["form-item"]}>
<label></label>
<Input
placeholder="请输入关键词"
value={exportKeyword}
onChange={e => setExportKeyword(e.target.value)}
allowClear
/>
</div>
{/* 类型筛选 */}
<div className={style["form-item"]}>
<label></label>
<div className={style["type-selector"]}>
<div
className={`${style["type-option"]} ${
exportType === undefined ? style["active"] : ""
}`}
onClick={() => setExportType(undefined)}
>
</div>
<div
className={`${style["type-option"]} ${
exportType === 4 ? style["active"] : ""
}`}
onClick={() => setExportType(4)}
>
</div>
<div
className={`${style["type-option"]} ${
exportType === 1 ? style["active"] : ""
}`}
onClick={() => setExportType(1)}
>
</div>
<div
className={`${style["type-option"]} ${
exportType === 3 ? style["active"] : ""
}`}
onClick={() => setExportType(3)}
>
</div>
</div>
</div>
{/* 开始时间 */}
<div className={style["form-item"]}>
<label></label>
<Input
readOnly
placeholder="请选择开始时间"
value={
exportStartTime
? exportStartTime.toLocaleDateString("zh-CN")
: ""
}
onClick={() => setShowStartTimePicker(true)}
/>
<DatePicker
visible={showStartTimePicker}
title="开始时间"
value={exportStartTime}
onClose={() => setShowStartTimePicker(false)}
onConfirm={val => {
setExportStartTime(val);
setShowStartTimePicker(false);
}}
/>
</div>
{/* 结束时间 */}
<div className={style["form-item"]}>
<label></label>
<Input
readOnly
placeholder="请选择结束时间"
value={
exportEndTime ? exportEndTime.toLocaleDateString("zh-CN") : ""
}
onClick={() => setShowEndTimePicker(true)}
/>
<DatePicker
visible={showEndTimePicker}
title="结束时间"
value={exportEndTime}
onClose={() => setShowEndTimePicker(false)}
onConfirm={val => {
setExportEndTime(val);
setShowEndTimePicker(false);
}}
/>
</div>
</div>
<div className={style["popup-footer"]}>
<Button
block
color="primary"
onClick={handleExportMoments}
loading={exportLoading}
disabled={exportLoading}
>
{exportLoading ? "导出中..." : "确认导出"}
</Button>
<Button
block
color="danger"
fill="outline"
onClick={() => {
setShowExportPopup(false);
setExportKeyword("");
setExportType(undefined);
setExportStartTime(null);
setExportEndTime(null);
}}
style={{ marginTop: 12 }}
>
</Button>
</div>
</div>
</Popup>
{/* 好友详情弹窗 */}
{/* Removed */}
</Layout>

View File

@@ -26,6 +26,11 @@ class ExportController extends Controller
* @param array $rows 数据行,需与 $headers 的 key 对应
* @param array $imageColumns 需要渲染为图片的列 key 列表
* @param string $sheetName 工作表名称
* @param array $options 额外选项:
* - imageWidth(图片宽度默认100)
* - imageHeight(图片高度默认100)
* - imageColumnWidth(图片列宽默认15)
* - titleRow(标题行内容,支持多行文本数组)
*
* @throws Exception
*/
@@ -34,7 +39,8 @@ class ExportController extends Controller
array $headers,
array $rows,
array $imageColumns = [],
$sheetName = 'Sheet1'
$sheetName = 'Sheet1',
array $options = []
) {
if (empty($headers)) {
throw new Exception('导出列定义不能为空');
@@ -43,22 +49,114 @@ class ExportController extends Controller
throw new Exception('导出数据不能为空');
}
// 默认选项
$imageWidth = isset($options['imageWidth']) ? (int)$options['imageWidth'] : 100;
$imageHeight = isset($options['imageHeight']) ? (int)$options['imageHeight'] : 100;
$imageColumnWidth = isset($options['imageColumnWidth']) ? (float)$options['imageColumnWidth'] : 15;
$rowHeight = isset($options['rowHeight']) ? (int)$options['rowHeight'] : ($imageHeight + 10);
$excel = new PHPExcel();
$sheet = $excel->getActiveSheet();
$sheet->setTitle($sheetName);
$columnKeys = array_keys($headers);
$totalColumns = count($columnKeys);
$lastColumnLetter = self::columnLetter($totalColumns - 1);
// 写入表头
// 定义特定列的固定宽度(如果未指定则使用默认值)
$columnWidths = isset($options['columnWidths']) ? $options['columnWidths'] : [];
// 检查是否有标题行
$titleRow = isset($options['titleRow']) ? $options['titleRow'] : null;
$dataStartRow = 1; // 数据开始行(表头行)
// 如果有标题行,先写入标题行
if ($titleRow && is_array($titleRow) && !empty($titleRow)) {
$dataStartRow = 2; // 数据从第2行开始第1行是标题第2行是表头
// 合并标题行单元格(从第一列到最后一列)
$titleRange = 'A1:' . $lastColumnLetter . '1';
$sheet->mergeCells($titleRange);
// 构建标题内容(支持多行)
$titleContent = '';
if (is_array($titleRow)) {
$titleContent = implode("\n", $titleRow);
} else {
$titleContent = (string)$titleRow;
}
// 写入标题
$sheet->setCellValue('A1', $titleContent);
// 设置标题行样式
$sheet->getStyle('A1')->applyFromArray([
'font' => ['bold' => true, 'size' => 16],
'alignment' => [
'horizontal' => \PHPExcel_Style_Alignment::HORIZONTAL_CENTER,
'vertical' => \PHPExcel_Style_Alignment::VERTICAL_CENTER,
'wrap' => true
],
'fill' => [
'type' => \PHPExcel_Style_Fill::FILL_SOLID,
'color' => ['rgb' => 'FFF8DC'] // 浅黄色背景
],
'borders' => [
'allborders' => [
'style' => \PHPExcel_Style_Border::BORDER_THIN,
'color' => ['rgb' => '000000']
]
]
]);
$sheet->getRowDimension(1)->setRowHeight(80); // 标题行高度
}
// 写入表头并设置列宽
$headerRow = $dataStartRow;
foreach ($columnKeys as $index => $key) {
$columnLetter = self::columnLetter($index);
$sheet->setCellValue($columnLetter . '1', $headers[$key]);
$sheet->getColumnDimension($columnLetter)->setAutoSize(true);
$sheet->setCellValue($columnLetter . $headerRow, $headers[$key]);
// 如果是图片列,设置固定列宽
if (in_array($key, $imageColumns, true)) {
$sheet->getColumnDimension($columnLetter)->setWidth($imageColumnWidth);
} elseif (isset($columnWidths[$key])) {
// 如果指定了该列的宽度,使用指定宽度
$sheet->getColumnDimension($columnLetter)->setWidth($columnWidths[$key]);
} else {
// 否则自动调整
$sheet->getColumnDimension($columnLetter)->setAutoSize(true);
}
}
// 设置表头样式
$headerRange = 'A' . $headerRow . ':' . $lastColumnLetter . $headerRow;
$sheet->getStyle($headerRange)->applyFromArray([
'font' => ['bold' => true, 'size' => 11],
'alignment' => [
'horizontal' => \PHPExcel_Style_Alignment::HORIZONTAL_CENTER,
'vertical' => \PHPExcel_Style_Alignment::VERTICAL_CENTER,
'wrap' => true
],
'fill' => [
'type' => \PHPExcel_Style_Fill::FILL_SOLID,
'color' => ['rgb' => 'FFF8DC']
],
'borders' => [
'allborders' => [
'style' => \PHPExcel_Style_Border::BORDER_THIN,
'color' => ['rgb' => '000000']
]
]
]);
$sheet->getRowDimension($headerRow)->setRowHeight(30); // 增加表头行高以确保文本完整显示
// 写入数据与图片
$dataRowStart = $dataStartRow + 1; // 数据从表头行下一行开始
foreach ($rows as $rowIndex => $rowData) {
$excelRow = $rowIndex + 2; // 数据从第 2 行开始
$excelRow = $dataRowStart + $rowIndex; // 数据
$maxRowHeight = $rowHeight; // 记录当前行的最大高度
foreach ($columnKeys as $colIndex => $key) {
$columnLetter = self::columnLetter($colIndex);
$cell = $columnLetter . $excelRow;
@@ -67,21 +165,79 @@ class ExportController extends Controller
if (in_array($key, $imageColumns, true) && !empty($value)) {
$imagePath = self::resolveImagePath($value);
if ($imagePath) {
$drawing = new PHPExcel_Worksheet_Drawing();
$drawing->setPath($imagePath);
$drawing->setCoordinates($cell);
$drawing->setOffsetX(5);
$drawing->setOffsetY(5);
$drawing->setHeight(60);
$drawing->setWorksheet($sheet);
$sheet->getRowDimension($excelRow)->setRowHeight(60);
// 获取图片实际尺寸并等比例缩放
$imageSize = @getimagesize($imagePath);
if ($imageSize) {
$originalWidth = $imageSize[0];
$originalHeight = $imageSize[1];
// 计算等比例缩放后的尺寸
$ratio = min($imageWidth / $originalWidth, $imageHeight / $originalHeight);
$scaledWidth = $originalWidth * $ratio;
$scaledHeight = $originalHeight * $ratio;
// 确保不超过最大尺寸
if ($scaledWidth > $imageWidth) {
$scaledWidth = $imageWidth;
$scaledHeight = $originalHeight * ($imageWidth / $originalWidth);
}
if ($scaledHeight > $imageHeight) {
$scaledHeight = $imageHeight;
$scaledWidth = $originalWidth * ($imageHeight / $originalHeight);
}
$drawing = new PHPExcel_Worksheet_Drawing();
$drawing->setPath($imagePath);
$drawing->setCoordinates($cell);
// 居中显示图片Excel列宽1单位≈7像素行高1单位≈0.75像素)
$cellWidthPx = $imageColumnWidth * 7;
$cellHeightPx = $maxRowHeight * 0.75;
$offsetX = max(2, ($cellWidthPx - $scaledWidth) / 2);
$offsetY = max(2, ($cellHeightPx - $scaledHeight) / 2);
$drawing->setOffsetX((int)$offsetX);
$drawing->setOffsetY((int)$offsetY);
$drawing->setWidth((int)$scaledWidth);
$drawing->setHeight((int)$scaledHeight);
$drawing->setWorksheet($sheet);
// 更新行高以适应图片(留出一些边距)
$neededHeight = (int)($scaledHeight / 0.75) + 10;
if ($neededHeight > $maxRowHeight) {
$maxRowHeight = $neededHeight;
}
} else {
// 如果无法获取图片尺寸,使用默认尺寸
$drawing = new PHPExcel_Worksheet_Drawing();
$drawing->setPath($imagePath);
$drawing->setCoordinates($cell);
$drawing->setOffsetX(5);
$drawing->setOffsetY(5);
$drawing->setWidth($imageWidth);
$drawing->setHeight($imageHeight);
$drawing->setWorksheet($sheet);
}
} else {
$sheet->setCellValue($cell, $value);
$sheet->setCellValue($cell, '');
}
} else {
$sheet->setCellValue($cell, $value);
// 设置文本对齐和换行
$style = $sheet->getStyle($cell);
$style->getAlignment()->setVertical(\PHPExcel_Style_Alignment::VERTICAL_CENTER);
$style->getAlignment()->setWrapText(true);
// 根据列类型设置水平对齐
if (in_array($key, ['date', 'postTime'])) {
$style->getAlignment()->setHorizontal(\PHPExcel_Style_Alignment::HORIZONTAL_CENTER);
} else {
$style->getAlignment()->setHorizontal(\PHPExcel_Style_Alignment::HORIZONTAL_LEFT);
}
}
}
// 设置行高
$sheet->getRowDimension($excelRow)->setRowHeight($maxRowHeight);
}
$safeName = preg_replace('/[^\w\-]/', '_', $fileName ?: 'export_' . date('Ymd_His'));

View File

@@ -38,6 +38,7 @@ Route::group('v1/', function () {
Route::get('getWechatInfo', 'app\cunkebao\controller\wechat\GetWechatController@getWechatInfo');
Route::get('overview', 'app\cunkebao\controller\wechat\GetWechatOverviewV1Controller@index'); // 获取微信账号概览数据
Route::get('moments', 'app\cunkebao\controller\wechat\GetWechatMomentsV1Controller@index'); // 获取微信朋友圈
Route::get('moments/export', 'app\cunkebao\controller\wechat\GetWechatMomentsV1Controller@export'); // 导出微信朋友圈
Route::get('count', 'app\cunkebao\controller\DeviceWechat@count');
Route::get('device-count', 'app\cunkebao\controller\DeviceWechat@deviceCount'); // 获取有登录微信的设备数量
Route::put('refresh', 'app\cunkebao\controller\DeviceWechat@refresh'); // 刷新设备微信状态

View File

@@ -2,6 +2,7 @@
namespace app\cunkebao\controller\wechat;
use app\common\controller\ExportController;
use app\common\model\Device as DeviceModel;
use app\common\model\DeviceUser as DeviceUserModel;
use app\common\model\DeviceWechatLogin as DeviceWechatLoginModel;
@@ -145,6 +146,164 @@ class GetWechatMomentsV1Controller extends BaseController
}
}
/**
* 导出朋友圈数据到Excel
*
* @return void
*/
public function export()
{
try {
$wechatId = $this->request->param('wechatId/s', '');
if (empty($wechatId)) {
return ResponseHelper::error('wechatId不能为空');
}
// 权限校验:只能查看当前账号可访问的微信
$accessibleWechatIds = $this->getAccessibleWechatIds();
if (!in_array($wechatId, $accessibleWechatIds, true)) {
return ResponseHelper::error('无权查看该微信的朋友圈', 403);
}
// 获取对应的微信账号ID
$accountId = Db::table('s2_wechat_account')
->where('wechatId', $wechatId)
->value('id');
if (empty($accountId)) {
return ResponseHelper::error('微信账号不存在或尚未同步', 404);
}
$query = Db::table('s2_wechat_moments')
->where('wechatAccountId', $accountId);
// 关键词搜索
if ($keyword = trim((string)$this->request->param('keyword', ''))) {
$query->whereLike('content', '%' . $keyword . '%');
}
// 类型筛选
$type = $this->request->param('type', '');
if ($type !== '' && $type !== null) {
$query->where('type', (int)$type);
}
// 时间筛选
$startTime = $this->request->param('startTime', '');
$endTime = $this->request->param('endTime', '');
if ($startTime || $endTime) {
$start = $startTime ? strtotime($startTime) : 0;
$end = $endTime ? strtotime($endTime) : time();
if ($start && $end && $end < $start) {
return ResponseHelper::error('结束时间不能早于开始时间');
}
$query->whereBetween('createTime', [$start ?: 0, $end ?: time()]);
}
// 获取所有数据(不分页)
$moments = $query->order('createTime', 'desc')->select();
if (empty($moments)) {
return ResponseHelper::error('暂无数据可导出');
}
// 定义表头
$headers = [
'date' => '日期',
'postTime' => '投放时间',
'functionCategory' => '作用分类',
'content' => '朋友圈文案',
'selfReply' => '自回评内容',
'displayForm' => '朋友圈展示形式',
'image1' => '配图1',
'image2' => '配图2',
'image3' => '配图3',
'image4' => '配图4',
'image5' => '配图5',
'image6' => '配图6',
'image7' => '配图7',
'image8' => '配图8',
'image9' => '配图9',
];
// 格式化数据
$rows = [];
foreach ($moments as $moment) {
$resUrls = $this->decodeJson($moment['resUrls'] ?? null);
$imageUrls = is_array($resUrls) ? $resUrls : [];
// 格式化日期和时间
$createTime = !empty($moment['createTime'])
? (is_numeric($moment['createTime']) ? $moment['createTime'] : strtotime($moment['createTime']))
: 0;
$date = $createTime ? date('Y年m月d日', $createTime) : '';
$postTime = $createTime ? date('H:i', $createTime) : '';
// 判断展示形式
$displayForm = '';
if (!empty($moment['content']) && !empty($imageUrls)) {
$displayForm = '文字+图片';
} elseif (!empty($moment['content'])) {
$displayForm = '文字';
} elseif (!empty($imageUrls)) {
$displayForm = '图片';
}
$row = [
'date' => $date,
'postTime' => $postTime,
'functionCategory' => '', // 暂时放空
'content' => $moment['content'] ?? '',
'selfReply' => '', // 暂时放空
'displayForm' => $displayForm,
];
// 分配图片到配图1-9列
for ($i = 1; $i <= 9; $i++) {
$imageKey = 'image' . $i;
$row[$imageKey] = isset($imageUrls[$i - 1]) ? $imageUrls[$i - 1] : '';
}
$rows[] = $row;
}
// 定义图片列配图1-9
$imageColumns = ['image1', 'image2', 'image3', 'image4', 'image5', 'image6', 'image7', 'image8', 'image9'];
// 生成文件名
$fileName = '朋友圈投放_' . date('Ymd_His');
// 调用导出方法,优化图片显示效果
ExportController::exportExcelWithImages(
$fileName,
$headers,
$rows,
$imageColumns,
'朋友圈投放',
[
'imageWidth' => 120, // 图片宽度(像素)
'imageHeight' => 120, // 图片高度(像素)
'imageColumnWidth' => 18, // 图片列宽Excel单位
'rowHeight' => 130, // 行高(像素)
'columnWidths' => [ // 特定列的固定宽度
'date' => 15, // 日期列宽
'postTime' => 12, // 投放时间列宽
'functionCategory' => 15, // 作用分类列宽
'content' => 40, // 朋友圈文案列宽(自动调整可能不够)
'selfReply' => 30, // 自回评内容列宽
'displayForm' => 18, // 朋友圈展示形式列宽
],
'titleRow' => [ // 标题行内容(第一行)
'朋友圈投放',
'我能提供什么价值? (40%) 有谁正在和我合作 (20%) 如何和我合作? (20%) 你找我合作需要付多少钱? (20%)'
]
]
);
} catch (\Exception $e) {
return ResponseHelper::error('导出失败:' . $e->getMessage(), 500);
}
}
/**
* 格式化朋友圈数据
*