优化增加第三方【每日推送】原始数据记录日志.log

This commit is contained in:
2026-07-21 16:44:39 +08:00
parent 2fbc07329e
commit 4fbc4500fd
16 changed files with 371 additions and 143 deletions

View File

@@ -1,74 +1,103 @@
<?php
namespace app\admin\controller\mall;
use Throwable;
use app\common\controller\Backend;
/**
* 收获地址管理
*/
class Address extends Backend
{
/**
* MallAddress模型对象
* @var object|null
* @phpstan-var \app\common\model\MallAddress|null
*/
protected ?object $model = null;
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time'];
protected array $withJoinTable = ['playxUserAsset'];
protected string|array $quickSearchField = ['id'];
public function initialize(): void
{
parent::initialize();
$this->model = new \app\common\model\MallAddress();
}
/**
* 查看
* @throws Throwable
*/
public function index(\Webman\Http\Request $request): \support\Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
if ($request->get('select') || $request->post('select')) {
$this->_select();
return $this->success();
}
/**
* 1. withJoin 不可使用 alias 方法设置表别名,别名将自动使用关联模型名称(小写下划线命名规则)
* 2. 以下的别名设置了主表别名,同时便于拼接查询参数等
* 3. paginate 数据集可使用链式操作 each(function($item, $key) {}) 遍历处理
*/
list($where, $alias, $limit, $order) = $this->queryBuilder();
$res = $this->model
->with(['playxUserAsset' => function ($query) {
$query->field('id,username');
}])
->visible(['playxUserAsset' => ['username']])
->alias($alias)
->where($where)
->order($order)
->paginate($limit);
return $this->success('', [
'list' => $res->items(),
'total' => $res->total(),
'remark' => get_route_remark(),
]);
}
/**
* 若需重写查看、编辑、删除等方法,请复制 @see \app\admin\library\traits\Backend 中对应的方法至此进行重写
*/
}
<?php
namespace app\admin\controller\mall;
use Throwable;
use app\common\controller\Backend;
use app\common\model\MallAddress;
/**
* 收获地址管理
*/
class Address extends Backend
{
/**
* MallAddress模型对象
* @var object|null
* @phpstan-var \app\common\model\MallAddress|null
*/
protected ?object $model = null;
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time', 'region_origin'];
protected array $withJoinTable = ['playxUserAsset'];
protected string|array $quickSearchField = ['id'];
public function initialize(): void
{
parent::initialize();
$this->model = new MallAddress();
}
/**
* 查看
* @throws Throwable
*/
public function index(\Webman\Http\Request $request): \support\Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
if ($request->get('select') || $request->post('select')) {
$this->_select();
return $this->success();
}
list($where, $alias, $limit, $order) = $this->queryBuilder();
$res = $this->model
->with(['playxUserAsset' => function ($query) {
$query->field('id,username');

View File

@@ -93,7 +93,7 @@ class DailyPush extends Backend
}
/**
* 导出 ExcelCSV 流式写入,兼容 Excel 打开
* 导出 ExcelXLSX 流式写入)
*/
public function export(Request $request): Response
{
@@ -104,12 +104,12 @@ class DailyPush extends Backend
$fieldsRaw = $request->post('fields', $request->get('fields', []));
$fields = is_array($fieldsRaw) ? $fieldsRaw : explode(',', strval($fieldsRaw));
$limitRaw = strval($request->post('export_limit', $request->get('export_limit', '1000')));
$limitRaw = strval($request->post('export_limit', $request->get('export_limit', '0')));
if (!is_numeric($limitRaw)) {
return $this->error(__('Parameter error'));
}
$limit = intval($limitRaw);
if ($limit < 1) {
if ($limit < 0) {
return $this->error(__('Parameter error'));
}

View File

@@ -15,6 +15,7 @@ use app\common\model\MallSession;
use app\common\model\MallOrder;
use app\common\model\MallUserAsset;
use app\common\library\MallPlayxRatios;
use app\common\library\MallDailyPushRawLogger;
use app\common\model\MallAddress;
use support\think\Db;
use Webman\Http\Request;
@@ -188,13 +189,15 @@ class Playx extends Api
return $response;
}
$rawBody = $request->rawBody();
$body = $request->post();
if (empty($body)) {
$raw = $request->rawBody();
if ($raw) {
$body = json_decode($raw, true) ?? [];
}
if (empty($body) && $rawBody !== '') {
$body = json_decode($rawBody, true) ?? [];
}
if ($rawBody === '') {
$rawBody = json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
MallDailyPushRawLogger::log($request, $rawBody);
$secret = config('playx.daily_push_secret', '');
if ($secret !== '') {
@@ -1101,7 +1104,7 @@ SQL;
$phone = trim(strval($request->post('phone', '')));
$receiverName = trim(strval($request->post('receiver_name', '')));
$region = $request->post('region', '');
$region = MallAddress::normalizeRegion($request->post('region', ''));
$detailAddress = trim(strval($request->post('detail_address', '')));
$defaultSetting = strval($request->post('default_setting', '0')) === '1' ? 1 : 0;
@@ -1171,7 +1174,7 @@ SQL;
$updates['receiver_name'] = trim(strval($request->post('receiver_name', '')));
}
if ($request->post('region', null) !== null) {
$updates['region'] = $request->post('region', '');
$updates['region'] = MallAddress::normalizeRegion($request->post('region', ''));
}
if ($request->post('detail_address', null) !== null) {
$updates['detail_address'] = trim(strval($request->post('detail_address', '')));

View File

@@ -5,15 +5,19 @@ declare(strict_types=1);
namespace app\common\library;
use app\common\model\MallDailyPush;
use OpenSpout\Common\Entity\Row;
use OpenSpout\Writer\XLSX\Options;
use OpenSpout\Writer\XLSX\Writer;
use support\Response;
use Throwable;
/**
* 每日推送数据导出(流式 CSV兼容 Excel 打开
* 每日推送数据导出(OpenSpout 流式 XLSX
*/
class MallDailyPushExport
{
public const MAX_EXPORT_LIMIT = 100000;
/** 0 表示不限制条数,导出全部匹配数据 */
public const UNLIMITED_EXPORT = 0;
public const CHUNK_SIZE = 2000;
@@ -64,28 +68,34 @@ class MallDailyPushExport
throw new \InvalidArgumentException(__('Parameter error'));
}
$limit = max(1, min(self::MAX_EXPORT_LIMIT, $limit));
if ($limit < 0) {
throw new \InvalidArgumentException(__('Parameter error'));
}
@set_time_limit(0);
$exportDir = runtime_path('export');
if (!is_dir($exportDir) && !mkdir($exportDir, 0755, true) && !is_dir($exportDir)) {
throw new \RuntimeException('Failed to create export directory');
}
$filepath = $exportDir . DIRECTORY_SEPARATOR . 'daily_push_' . date('YmdHis') . '_' . bin2hex(random_bytes(4)) . '.csv';
$filepath = $exportDir . DIRECTORY_SEPARATOR . 'daily_push_' . date('YmdHis') . '_' . bin2hex(random_bytes(4)) . '.xlsx';
$labels = $this->resolveFieldLabels($lang);
$handle = fopen($filepath, 'wb');
if ($handle === false) {
throw new \RuntimeException('Failed to create export file');
}
$options = new Options();
$options->setTempFolder($exportDir);
$writer = new Writer($options);
try {
fwrite($handle, "\xEF\xBB\xBF");
fputcsv($handle, $this->buildHeaderRow($fields, $labels));
$writer->openToFile($filepath);
$writer->addRow(Row::fromValues($this->buildHeaderRow($fields, $labels)));
$exported = 0;
$page = 1;
while ($exported < $limit) {
$batchSize = min(self::CHUNK_SIZE, $limit - $exported);
$unlimited = $limit === self::UNLIMITED_EXPORT;
while ($unlimited || $exported < $limit) {
$batchSize = $unlimited ? self::CHUNK_SIZE : min(self::CHUNK_SIZE, $limit - $exported);
$rows = $model
->field($fields)
->alias($alias)
@@ -99,9 +109,9 @@ class MallDailyPushExport
}
foreach ($rows as $row) {
fputcsv($handle, $this->formatRow($row->toArray(), $fields));
$writer->addRow(Row::fromValues($this->formatRow($row->toArray(), $fields)));
$exported++;
if ($exported >= $limit) {
if (!$unlimited && $exported >= $limit) {
break;
}
}
@@ -111,22 +121,27 @@ class MallDailyPushExport
}
$page++;
}
$writer->close();
} catch (Throwable $e) {
fclose($handle);
try {
$writer->close();
} catch (Throwable) {
// ignore
}
if (is_file($filepath)) {
@unlink($filepath);
}
throw $e;
}
fclose($handle);
register_shutdown_function(static function () use ($filepath): void {
if (is_file($filepath)) {
@unlink($filepath);
}
});
$filename = 'daily_push_' . date('YmdHis') . '.csv';
$filename = 'daily_push_' . date('YmdHis') . '.xlsx';
return (new Response())->file($filepath, $filename);
}
@@ -157,7 +172,7 @@ class MallDailyPushExport
public function getMaxExportLimit(): int
{
return self::MAX_EXPORT_LIMIT;
return self::UNLIMITED_EXPORT;
}
/**

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace app\common\library;
use Webman\Http\Request;
/**
* 第三方每日推送原始数据日志(按天写入,默认保留 30 天)
*/
class MallDailyPushRawLogger
{
public const DEFAULT_RETENTION_DAYS = 30;
public static function log(Request $request, string $rawBody): void
{
$rawBody = trim($rawBody);
if ($rawBody === '') {
return;
}
$dir = runtime_path('logs/daily_push_raw');
if (!is_dir($dir) && !mkdir($dir, 0755, true) && !is_dir($dir)) {
return;
}
$entry = [
'time' => date('Y-m-d H:i:s'),
'ip' => $request->getRealIp(),
'method' => $request->method(),
'path' => $request->path(),
'headers' => [
'X-Request-Id' => strval($request->header('X-Request-Id', '')),
'X-Timestamp' => strval($request->header('X-Timestamp', '')),
'Content-Type' => strval($request->header('Content-Type', '')),
],
'raw' => $rawBody,
];
$logFile = $dir . DIRECTORY_SEPARATOR . 'daily_push_' . date('Y-m-d') . '.log';
file_put_contents(
$logFile,
json_encode($entry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL,
FILE_APPEND | LOCK_EX
);
self::cleanup($dir);
}
private static function cleanup(string $dir): void
{
$retentionDays = intval(config('playx.daily_push_raw_log_days', self::DEFAULT_RETENTION_DAYS));
if ($retentionDays <= 0) {
return;
}
$expireBefore = time() - ($retentionDays * 86400);
$files = glob($dir . DIRECTORY_SEPARATOR . 'daily_push_*.log');
if ($files === false) {
return;
}
foreach ($files as $file) {
if (is_file($file) && filemtime($file) < $expireBefore) {
@unlink($file);
}
}
}
}

View File

@@ -3,6 +3,7 @@
namespace app\common\model;
use app\common\model\traits\TimestampInteger;
use support\think\Db;
use support\think\Model;
/**
@@ -23,37 +24,117 @@ class MallAddress extends Model
'region_text',
];
public function getregionAttr($value): array
/**
* 表单展示:文本地区返回规范化字符串;历史地区 ID 转为名称(英文逗号拼接)
*/
public function getregionAttr($value): string
{
if ($value === '' || $value === null) return [];
if (!is_array($value)) {
return explode(',', $value);
if ($value === '' || $value === null) {
return '';
}
return $value;
if (is_array($value)) {
return self::normalizeRegion($value);
}
$region = trim(strval($value));
if ($region === '') {
return '';
}
if (self::isAreaIdList($region)) {
return self::resolveAreaNames($region);
}
return self::normalizeRegion($region);
}
/**
* 入库:统一为英文逗号分隔(与 API / 原 city 组件存储格式一致)
*/
public function setregionAttr($value): string
{
return is_array($value) ? implode(',', $value) : $value;
return self::normalizeRegion($value);
}
public function getregionTextAttr($value, $row): string
{
if ($row['region'] === '' || $row['region'] === null) return '';
$region = $row['region'];
$ids = $region;
if (!is_array($ids)) {
$ids = explode(',', (string) $ids);
}
$ids = array_values(array_filter(array_map('trim', $ids), static function ($s) {
return $s !== '';
}));
if (empty($ids)) {
$region = $row['region'] ?? '';
if ($region === '' || $region === null) {
return '';
}
$cityNames = \support\think\Db::name('area')->whereIn('id', $ids)->column('name');
return $cityNames ? implode(',', $cityNames) : '';
if (is_array($region)) {
return self::normalizeRegion($region);
}
$region = trim(strval($region));
if ($region === '') {
return '';
}
if (self::isAreaIdList($region)) {
return self::resolveAreaNames($region);
}
return self::normalizeRegion($region);
}
/**
* 将地区规范为英文逗号分隔字符串(数组或「省,市,区」文本)
*/
public static function normalizeRegion(mixed $value): string
{
if (is_array($value)) {
$parts = [];
foreach ($value as $item) {
$part = trim(strval($item));
if ($part !== '') {
$parts[] = $part;
}
}
return implode(',', $parts);
}
$region = trim(strval($value));
if ($region === '') {
return '';
}
$region = str_replace('', ',', $region);
$parts = explode(',', $region);
$normalized = [];
foreach ($parts as $part) {
$part = trim($part);
if ($part !== '') {
$normalized[] = $part;
}
}
return implode(',', $normalized);
}
public static function isAreaIdList(string $region): bool
{
$parts = array_values(array_filter(array_map('trim', explode(',', $region)), static function ($part) {
return $part !== '';
}));
if ($parts === []) {
return false;
}
foreach ($parts as $part) {
if (!ctype_digit($part)) {
return false;
}
}
return true;
}
public static function resolveAreaNames(string $region): string
{
$ids = array_values(array_filter(array_map('trim', explode(',', $region)), static function ($part) {
return $part !== '';
}));
if ($ids === []) {
return '';
}
$cityNames = Db::name('area')->whereIn('id', $ids)->column('name');
if (!$cityNames) {
return self::normalizeRegion($region);
}
return implode(',', $cityNames);
}
public function playxUserAsset(): \think\model\relation\BelongsTo
@@ -83,4 +164,4 @@ class MallAddress extends Model
'receiver_address' => $receiverAddress,
];
}
}
}