From 8be522f110a3b300ad912353e826275d45dc6517 Mon Sep 17 00:00:00 2001 From: hant Date: Fri, 11 Jul 2025 00:08:29 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E5=AF=BC=E5=87=BA=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/admin/command/Test.php | 33 ++++----- application/admin/controller/Order.php | 99 +++++++++++++++++++++++-- application/admin/view/order/index.html | 3 + public/assets/js/backend/order.js | 27 +++++++ 4 files changed, 136 insertions(+), 26 deletions(-) diff --git a/application/admin/command/Test.php b/application/admin/command/Test.php index a9190af..4ad4148 100644 --- a/application/admin/command/Test.php +++ b/application/admin/command/Test.php @@ -2,26 +2,13 @@ namespace app\admin\command; -use app\admin\addresmart\Address; -use app\admin\controller\AutoDispatchLogic; -use app\admin\controller\orders\DispatchLogic; -use app\admin\controller\SendMailLogic; -use app\admin\model\Order; -use app\admin\model\OrderDispatch; -use app\admin\model\OrderReview; -use app\admin\model\Worker; -use app\admin\model\WorkerItem; use app\admin\controller\AmapTrait; -use think\Collection; +use app\admin\controller\AutoDispatchLogic; +use app\admin\model\Order; use think\console\Command; use think\console\Input; use think\console\Output; -use think\Db; -use think\exception\DbException; -use think\Hook; -use think\Lang; -use think\Model; -use function Symfony\Component\Clock\now; +use think\Exception; class Test extends Command { @@ -35,8 +22,18 @@ class Test extends Command protected function execute(Input $input, Output $output) { - $order = Order::where('id',221)->select()[0]; - AutoDispatchLogic::autoDispatch($order); +// $order = Order::where('id','>=',694)->select(); + try { + $order = Order::get(5126); + AutoDispatchLogic::autoDispatch($order,null,true); + }catch (Exception $e){ + $output->writeln('' . $e->getMessage() . ''); + $output->writeln('' . $e->getFile() . ':' . $e->getLine() . ''); + $output->writeln($e->getTraceAsString()); + } + + dd(1); + } diff --git a/application/admin/controller/Order.php b/application/admin/controller/Order.php index d395d67..63a35ab 100644 --- a/application/admin/controller/Order.php +++ b/application/admin/controller/Order.php @@ -19,6 +19,7 @@ use app\common\Logic\OrderLogic; use Carbon\Carbon; use Carbon\Traits\Creator; use fast\Tree; +use PhpOffice\PhpSpreadsheet\Settings; use think\Db; use think\Exception; use think\exception\DbException; @@ -28,6 +29,8 @@ use think\Hook; use think\Loader; use think\Model; use function Symfony\Component\Clock\now; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Writer\Xlsx; /** * 订单列管理 @@ -127,12 +130,11 @@ class Order extends Backend */ - public function index() + public function index($getArray = false,$page = 0,$input_limit = 0) { $this->request->filter(['strip_tags', 'trim']); $group = \model('auth_group_access')->where('uid', $this->auth->id)->find()->group_id ?? 0; $user = \model('admin')->find($this->auth->id); - $this->assignconfig('permissions', [ 'add' => $this->auth->check('order/add'), 'edit' => $this->auth->check('order/edit'), @@ -149,8 +151,7 @@ class Order extends Backend ]); - if (false === $this->request->isAjax()) { - + if (false === $this->request->isAjax() && $getArray == false) { return $this->view->fetch(); } //如果发送的来源是 Selectpage,则转发到 Selectpage @@ -216,7 +217,7 @@ class Order extends Backend }); } - $list = $build + $build ->with(['user' => function ($q) { $q->field('id,nickname'); }, 'area' => function ($q) { @@ -231,8 +232,15 @@ class Order extends Backend } ]] ) - ->order($sort, $order) - ->paginate($limit); + ->order($sort, $order); + if ($getArray){ + $list = $build->paginate([ + 'list_rows' => $input_limit, + 'page' => $page + ]); + }else{ + $list = $build->paginate($limit); + } foreach ($list as $item) { $item->aftersale_btn = false; @@ -246,7 +254,6 @@ class Order extends Backend } unset($item->source); } - $result = ['total' => $list->total(), 'rows' => $list->items()]; return json($result); } @@ -1076,4 +1083,80 @@ class Order extends Backend return json($result); } + + public function export() + { + // 默认分页配置 + $limit = 1000; // 每页导出 500 条 + $page = 1; + $allRows = []; + + do { + + // 调用 index 方法(返回 JSON 格式数据) + $result = $this->index(true,$page,$limit); // index(true) 表示返回原始数据数组而不是 view + $result = $result->getData(); + if (!isset($result['rows'])) { + break; + } + + $rows = $result['rows']; + $count = count($rows); + if ($count == 0 || count($allRows) > 3000) { + break; + } + + $allRows = array_merge($allRows, $rows); + $page++; + } while ($count == $limit); // 如果最后一页不足 limit,说明结束了 + + if (empty($allRows)) { + return json(['code' => 0, 'msg' => '无数据可导出']); + } + + // 开启磁盘缓存,防止内存爆掉 + Settings::setLocale('fr'); + + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + +// ===== 表头处理 ===== + $columns = json_decode($this->request->get('columns', ''), true); + if (!$columns && isset($allRows[0])) { + $columns = []; + foreach ($allRows[0] as $key => $value) { + $columns[] = ['field' => $key, 'title' => $key]; + } + } + $titles = array_column($columns, 'title'); + $fields = array_column($columns, 'field'); + +// 写入表头(从 A1 开始) + foreach ($titles as $col => $title) { + $sheet->setCellValueByColumnAndRow($col + 1, 1, $title); + } + +// ===== 分批写入数据行 ===== + $rowIndex = 2; + foreach ($allRows as $row) { + foreach ($fields as $colIndex => $field) { + $value = isset($row[$field]) ? $row[$field] : ''; + $sheet->setCellValueByColumnAndRow($colIndex + 1, $rowIndex, $value); + } + $rowIndex++; + } + + // 保存文件 + $filename = '导出_' . date('Ymd_His') . '.xlsx'; + $filepath = __DIR__ .'/' . $filename; + (new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet))->save($filepath); + + return json([ + 'code' => 1, + 'msg' => '导出成功', + 'url' => request()->domain() . '/' . $filename + ]); + } + + } diff --git a/application/admin/view/order/index.html b/application/admin/view/order/index.html index 2b4db2e..1af2926 100644 --- a/application/admin/view/order/index.html +++ b/application/admin/view/order/index.html @@ -24,6 +24,9 @@
{:__('Edit')} + + 导出 + diff --git a/public/assets/js/backend/order.js b/public/assets/js/backend/order.js index 5ff7b6c..def402a 100644 --- a/public/assets/js/backend/order.js +++ b/public/assets/js/backend/order.js @@ -95,6 +95,7 @@ ${data.receive_type == 1 ? '已收定金' : '已收全款'} renderDefault: true, searchFormVisible: true, search: false, + showExport: false, columns: [ [ {checkbox: true}, @@ -530,6 +531,32 @@ ${data.receive_type == 1 ? '已收定金' : '已收全款'} clickParent: true }); + $('#btn-export').on('click', function () { + var options = $("#table").bootstrapTable('getOptions'); + + // 提取列信息(不含 checkbox) + var columns = []; + $.each(options.columns[0], function (i, item) { + if (item.field && !item.checkbox && !item.visible === false) { + columns.push({ + field: item.field, + title: item.title + }); + } + }); + + var params = { + columns: JSON.stringify(columns), + filter: options.queryParams({}).filter || {}, + op: options.queryParams({}).op || {}, + sort: options.sortName, + order: options.sortOrder + }; + // console.log($.param(params)) + var url = '/admin/order/export?' + $.param(params); + window.open(url); // 发起文件下载 + }); + }, add: function () { $("#mybuttom").on("click", function () { From 354eb071868289cb626cbdf1c5edfd0135b2b545 Mon Sep 17 00:00:00 2001 From: todaywindy Date: Fri, 11 Jul 2025 15:00:04 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E5=AF=BC=E5=87=BA=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/admin/controller/Order.php | 157 ++++++++---------- application/common/cache/SimpleFileCache.php | 109 ++++++++++++ .../common/cache/ThinkphpCacheAdapter.php | 74 +++++++++ application/common/controller/Backend.php | 77 +++++++++ 4 files changed, 325 insertions(+), 92 deletions(-) create mode 100644 application/common/cache/SimpleFileCache.php create mode 100644 application/common/cache/ThinkphpCacheAdapter.php diff --git a/application/admin/controller/Order.php b/application/admin/controller/Order.php index ceed3d3..d160404 100644 --- a/application/admin/controller/Order.php +++ b/application/admin/controller/Order.php @@ -14,12 +14,16 @@ use app\admin\model\OrderLog; use app\admin\model\orders\Dispatchlog; use app\admin\model\Worker; use app\admin\model\WorkerItem; +use app\common\cache\SimpleFileCache; +use app\common\cache\ThinkphpCacheAdapter; use app\common\controller\Backend; use app\common\Logic\OrderLogic; use Carbon\Carbon; use Carbon\Traits\Creator; use fast\Tree; +use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Settings; +use think\Cache; use think\Db; use think\Exception; use think\exception\DbException; @@ -32,6 +36,7 @@ use function Symfony\Component\Clock\now; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Writer\Xlsx; + /** * 订单列管理 * @@ -130,7 +135,7 @@ class Order extends Backend */ - public function index($getArray = false,$page = 0,$input_limit = 0) + public function index($getArray = false, $page = 0, $input_limit = 0) { $this->request->filter(['strip_tags', 'trim']); $group = \model('auth_group_access')->where('uid', $this->auth->id)->find()->group_id ?? 0; @@ -178,8 +183,8 @@ class Order extends Backend ->where($where); $filter = (array)json_decode(input()['filter'] ?? '', true); - if(isset($filter['audit_time'])){ - $build->where('status',\app\admin\model\Order::STATUS_FINISHED); + if (isset($filter['audit_time'])) { + $build->where('status', \app\admin\model\Order::STATUS_FINISHED); } $admin_filter = $filter['user.nickname'] ?? false; @@ -200,13 +205,13 @@ class Order extends Backend $build->whereIn('item_id', $item_ids); } if (!is_null($is_timeout)) { - $build->where('is_overtime',$is_timeout); + $build->where('is_overtime', $is_timeout); } - if ($group == 2 ) { // 录单员 - $build->where('admin_id',$user->id); - }elseif ($group == 6){ // 派单员 + if ($group == 2) { // 录单员 + $build->where('admin_id', $user->id); + } elseif ($group == 6) { // 派单员 // 生成 SQL 语句 $ids = $user->area_ids ?? ''; if ($ids == '') { @@ -238,12 +243,12 @@ class Order extends Backend ]] ) ->order($sort, $order); - if ($getArray){ + if ($getArray) { $list = $build->paginate([ 'list_rows' => $input_limit, 'page' => $page ]); - }else{ + } else { $list = $build->paginate($limit); } @@ -252,13 +257,13 @@ class Order extends Backend if ($item->aftersale_id == 0 && $this->auth->check('aftersales/aftersale/add') && $item->status == \app\admin\model\Order::STATUS_FINISHED) { //$item->status == \app\admin\model\Order::STATUS_FINISHED && $item->aftersale_btn = true; } - if (isset($item->getRelation('source')->parent->title)){ - $item->source_total_name = '【' . $item->getRelation('source')->parent->title . '】' . ($item->getRelation('source')->title??''); - }else{ - $item->source_total_name = ($item->getRelation('source')->title??''); + if (isset($item->getRelation('source')->parent->title)) { + $item->source_total_name = '【' . $item->getRelation('source')->parent->title . '】' . ($item->getRelation('source')->title ?? ''); + } else { + $item->source_total_name = ($item->getRelation('source')->title ?? ''); } - if($item->status <60){ + if ($item->status < 60) { $item->audit_time = null; } unset($item->source); @@ -314,7 +319,7 @@ class Order extends Backend $sources = array_column($sources, null, 'id'); - if (!in_array($sources[$params['source']]['pid'],[3,8])&& empty($params['customer'])){ + if (!in_array($sources[$params['source']]['pid'], [3, 8]) && empty($params['customer'])) { $this->error('请输入客户名称'); } $params['source_shop'] = $sources[$params['source']]['title'] ?? null; @@ -335,23 +340,23 @@ class Order extends Backend $params['order_no'] = $this->generateOrderNumber(); $params['create_time'] = date('Y-m-d H:i:s'); $params['update_time'] = date('Y-m-d H:i:s'); - $params['receive_type'] = $params['receive_type']?: 1; - $params['audit_time'] = date('Y-m-d H:i:s'); + $params['receive_type'] = $params['receive_type'] ?: 1; + $params['audit_time'] = date('Y-m-d H:i:s'); $result = $this->model->allowField(true)->save($params); $auth = clone $this->auth; - $order = \app\admin\model\Order::where('id',$this->model->id)->with(['source' => [ + $order = \app\admin\model\Order::where('id', $this->model->id)->with(['source' => [ 'parent' => function ($q) { $q->field('id,title'); } ]] )->find(); // dd($order); - if (isset($order->getRelation('source')->parent->title)){ + if (isset($order->getRelation('source')->parent->title)) { $order->source_total_name = '【' . $order->getRelation('source')->parent->title . '】' . - ($order->getRelation('source')->title??''); - }else{ - $order->source_total_name = ($order->getRelation('source')->title??''); + ($order->getRelation('source')->title ?? ''); + } else { + $order->source_total_name = ($order->getRelation('source')->title ?? ''); } unset($order->source); //日志 @@ -433,11 +438,11 @@ class Order extends Backend // $params['create_time'] = date('Y-m-d H:i:s'); $params['update_time'] = date('Y-m-d H:i:s'); - $params['receive_type'] = $params['receive_type']?: 1; + $params['receive_type'] = $params['receive_type'] ?: 1; - if($order->receive_type != $params['receive_type']){ + if ($order->receive_type != $params['receive_type']) { //修改派单表 - OrderDispatch::where('order_id',$order->id)->whereBetween('status',[0,30])->update(['is_receipt' => $params['receive_type']== 1? 1:0]); + OrderDispatch::where('order_id', $order->id)->whereBetween('status', [0, 30])->update(['is_receipt' => $params['receive_type'] == 1 ? 1 : 0]); } // 更新订单信息 @@ -578,11 +583,11 @@ class Order extends Backend $dispatch_admin = AuthGroupAccess::where('group_id', 6) ->column('uid'); - $res = array_values(array_intersect($res,$dispatch_admin)); + $res = array_values(array_intersect($res, $dispatch_admin)); $message = $params['detail'] ?? ''; $abnormals = model('abnormal')->where('type', 3) - ->where('id',$params['abnormal_id'])->find(); + ->where('id', $params['abnormal_id'])->find(); $insert = []; foreach ($res as $re) { @@ -590,7 +595,7 @@ class Order extends Backend 'to_id' => $re, 'type' => 1, 'title' => '订单内容变更', - 'content' => '【订单内容变更】您有一条订单'.$order->order_no.'信息被修改,异常类型【'.$abnormals->title.'】,备注:【' . $message . '】,请注意查收。' + 'content' => '【订单内容变更】您有一条订单' . $order->order_no . '信息被修改,异常类型【' . $abnormals->title . '】,备注:【' . $message . '】,请注意查收。' ]; } $build = new Message(); @@ -637,7 +642,7 @@ class Order extends Backend 'update_time' => now()->format('Y-m-d H:m:s'), ]; - $orderDispatch = OrderDispatch::where('order_id',$order->id)->whereBetween('status','>=',0)->find(); + $orderDispatch = OrderDispatch::where('order_id', $order->id)->whereBetween('status', '>=', 0)->find(); if ($params['abnormal_id'] == 2 || $params['abnormal_id'] == 3) { @@ -739,7 +744,7 @@ class Order extends Backend $result = $order->allowField(true)->save($params); - $dispatch = OrderDispatch::where('order_id', $order->id)->where('status','>=',0)->find(); + $dispatch = OrderDispatch::where('order_id', $order->id)->where('status', '>=', 0)->find(); if (!empty($dispatch)) { $orderLogic = new OrderLogic(); $orderLogic->cancelOrderDispatch($dispatch, $this->auth, '订单被取消', false); @@ -1097,76 +1102,44 @@ class Order extends Backend public function export() { - // 默认分页配置 - $limit = 1000; // 每页导出 500 条 - $page = 1; - $allRows = []; - - do { - - // 调用 index 方法(返回 JSON 格式数据) - $result = $this->index(true,$page,$limit); // index(true) 表示返回原始数据数组而不是 view - $result = $result->getData(); - if (!isset($result['rows'])) { - break; - } - - $rows = $result['rows']; - $count = count($rows); - if ($count == 0 || count($allRows) > 3000) { - break; - } - - $allRows = array_merge($allRows, $rows); - $page++; - } while ($count == $limit); // 如果最后一页不足 limit,说明结束了 - - if (empty($allRows)) { - return json(['code' => 0, 'msg' => '无数据可导出']); - } - - // 开启磁盘缓存,防止内存爆掉 - Settings::setLocale('fr'); - - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); - -// ===== 表头处理 ===== + // 表头结构(可从参数获取或手动定义) $columns = json_decode($this->request->get('columns', ''), true); - if (!$columns && isset($allRows[0])) { +// dd($columns); + if (!$columns) { + // 用第一页数据自动生成字段 + $sample = $this->index(true, 1, 1)->getData(); + if (!isset($sample['rows'][0])) { + return json(['code' => 0, 'msg' => '无数据可导出']); + } $columns = []; - foreach ($allRows[0] as $key => $value) { + foreach ($sample['rows'][0] as $key => $value) { $columns[] = ['field' => $key, 'title' => $key]; } } - $titles = array_column($columns, 'title'); - $fields = array_column($columns, 'field'); -// 写入表头(从 A1 开始) - foreach ($titles as $col => $title) { - $sheet->setCellValueByColumnAndRow($col + 1, 1, $title); - } - -// ===== 分批写入数据行 ===== - $rowIndex = 2; - foreach ($allRows as $row) { - foreach ($fields as $colIndex => $field) { - $value = isset($row[$field]) ? $row[$field] : ''; - $sheet->setCellValueByColumnAndRow($colIndex + 1, $rowIndex, $value); + // 数据获取函数:分页获取数据(支持返回 array) + $fetchData = function ($page, $limit) { + $result = $this->index(true, $page, $limit); + $order = new \app\admin\model\Order(); + foreach ($result->getData()['rows'] as $item){ + $item->status = $order->getStatusList()[$item->status] ?? ''; + $item->dispatch_type = $item->dispatch_type == 1 ? '手动派单' : '自动派单'; + $item->is_overtime = $item->is_overtime ? '超时' : '未超时'; + $item->receive_type = $item->receive_type == 1 ? '已收定金' : '已收全款'; + $images = explode(',',$item->images ?? ''); + $url = ''; + if ($images){ + foreach ($images as $img){ + if($img == '')continue; + $url .= cdnurl($img) . ','; + } + } + $item->images = $url; } - $rowIndex++; - } + return $result->getData()['rows'] ?? []; + }; - // 保存文件 - $filename = '导出_' . date('Ymd_His') . '.xlsx'; - $filepath = __DIR__ .'/' . $filename; - (new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet))->save($filepath); - - return json([ - 'code' => 1, - 'msg' => '导出成功', - 'url' => request()->domain() . '/' . $filename - ]); + $this->exportExcelByStream($columns, $fetchData, '订单导出_' . date('Ymd_His') . '.xlsx'); } diff --git a/application/common/cache/SimpleFileCache.php b/application/common/cache/SimpleFileCache.php new file mode 100644 index 0000000..4e662a3 --- /dev/null +++ b/application/common/cache/SimpleFileCache.php @@ -0,0 +1,109 @@ +path = $path ?? CACHE_PATH; + $this->prefix = $prefix; + + if (!is_dir($this->path)) { + mkdir($this->path, 0777, true); + } + } + + protected function getFilePath(string $key): string + { + return $this->path . DIRECTORY_SEPARATOR . $this->prefix . md5($key) . '.cache'; + } + + public function get($key, $default = null): mixed + { + $file = $this->getFilePath($key); + if (!file_exists($file)) { + return $default; + } + $content = file_get_contents($file); + $data = unserialize($content); + if ($data['expire'] !== 0 && $data['expire'] < time()) { + @unlink($file); + return $default; + } + return $data['value']; + } + + public function set($key, $value, $ttl = null): bool + { + $file = $this->getFilePath($key); + $expire = $ttl instanceof \DateInterval ? (new \DateTime())->add($ttl)->getTimestamp() : ($ttl ? time() + $ttl : 0); + $data = [ + 'expire' => $expire, + 'value' => $value, + ]; + return file_put_contents($file, serialize($data)) !== false; + } + + public function delete($key): bool + { + $file = $this->getFilePath($key); + return file_exists($file) ? unlink($file) : true; + } + + public function clear(): bool + { + $files = glob($this->path . '/' . $this->prefix . '*.cache'); + foreach ($files as $file) { + unlink($file); + } + return true; + } + + public function getMultiple($keys, $default = null): iterable + { + $results = []; + foreach ($keys as $key) { + $results[$key] = $this->get($key, $default); + } + return $results; + } + + public function setMultiple($values, $ttl = null): bool + { + $success = true; + foreach ($values as $key => $value) { + if (!$this->set($key, $value, $ttl)) { + $success = false; + } + } + return $success; + } + + public function deleteMultiple($keys): bool + { + $success = true; + foreach ($keys as $key) { + if (!$this->delete($key)) { + $success = false; + } + } + return $success; + } + + public function has($key): bool + { + $file = $this->getFilePath($key); + if (!file_exists($file)) return false; + $data = unserialize(file_get_contents($file)); + if ($data['expire'] !== 0 && $data['expire'] < time()) { + @unlink($file); + return false; + } + return true; + } +} diff --git a/application/common/cache/ThinkphpCacheAdapter.php b/application/common/cache/ThinkphpCacheAdapter.php new file mode 100644 index 0000000..31fde7c --- /dev/null +++ b/application/common/cache/ThinkphpCacheAdapter.php @@ -0,0 +1,74 @@ +cache = $cache; + } + + public function get($key, $default = null): mixed + { + $value = $this->cache->get($key); + return $value === false ? $default : $value; + } + + public function set($key, $value, $ttl = null): bool + { + if ($ttl === null) { + return $this->cache->set($key, $value); + } + return $this->cache->set($key, $value, $ttl); + } + + public function delete($key): bool + { + return $this->cache->delete($key); + } + + public function clear(): bool + { + return $this->cache->clear(); + } + + public function getMultiple($keys, $default = null): iterable + { + $results = []; + foreach ($keys as $key) { + $results[$key] = $this->get($key, $default); + } + return $results; + } + + public function setMultiple($values, $ttl = null): bool + { + $success = true; + foreach ($values as $key => $value) { + if (!$this->set($key, $value, $ttl)) { + $success = false; + } + } + return $success; + } + + public function deleteMultiple($keys): bool + { + $success = true; + foreach ($keys as $key) { + if (!$this->delete($key)) { + $success = false; + } + } + return $success; + } + + public function has($key): bool + { + return $this->cache->has($key); + } +} diff --git a/application/common/controller/Backend.php b/application/common/controller/Backend.php index 9f41132..c7c514b 100755 --- a/application/common/controller/Backend.php +++ b/application/common/controller/Backend.php @@ -3,6 +3,9 @@ namespace app\common\controller; use app\admin\library\Auth; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; +use PhpOffice\PhpSpreadsheet\Writer\Xlsx; use think\Config; use think\Controller; use think\Db; @@ -762,4 +765,78 @@ class Backend extends Controller } return $res; } + + protected function exportExcelByStream(array $columns, callable $fetchData, string $filename = '导出.xlsx', int $limit = 1000) + { + $page = 1; + $rowIndex = 2; + + // 创建 Spreadsheet 和 Sheet + $spreadsheet = new Spreadsheet(); + $spreadsheet->removeSheetByIndex(0); + $sheet = new Worksheet($spreadsheet, 'Sheet1'); + $spreadsheet->addSheet($sheet); + $spreadsheet->setActiveSheetIndex(0); + $sheet = $spreadsheet->getActiveSheet(); + + $writer = new Xlsx($spreadsheet); + $writer->setPreCalculateFormulas(false); + + // 获取字段映射 + $titles = array_column($columns, 'title'); + $fields = array_column($columns, 'field'); + + // 写入表头 + $sheet->fromArray($titles, null, 'A1'); + + // 分页写入数据 + do { + $rows = $fetchData($page, $limit); + $count = count($rows); + if ($count === 0) break; + + foreach ($rows as $row) { + $dataRow = []; + foreach ($fields as $field) { + $value = $this->getNestedValue($row->toArray(), $field); + if (is_array($value) || is_object($value)) { + $value = json_encode($value, JSON_UNESCAPED_UNICODE); + } + $dataRow[] = $value; + } + $sheet->fromArray($dataRow, null, 'A' . $rowIndex); + $rowIndex++; + } + + $page++; + } while ($count === $limit); + + // 输出为下载流 + $filename = $filename ?: ('导出_' . date('Ymd_His') . '.xlsx'); + header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + header('Content-Disposition: attachment;filename="' . $filename . '"'); + header('Cache-Control: max-age=0'); + header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); + header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); + header('Pragma: public'); + + $writer->save('php://output'); + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); + exit; + } + protected function getNestedValue(array $data, string $path, $default = '') + { + $segments = explode('.', $path); + foreach ($segments as $segment) { + if (is_array($data) && array_key_exists($segment, $data)) { + $data = $data[$segment]; + } elseif (is_object($data) && isset($data->{$segment})) { + $data = $data->{$segment}; + } else { + return $default; + } + } + return $data; + } }