优化增加第三方【每日推送】原始数据记录日志.log
This commit is contained in:
@@ -31,6 +31,8 @@ PLAYX_RETURN_RATIO=0.1
|
||||
PLAYX_UNLOCK_RATIO=0.1
|
||||
# Daily Push 签名校验密钥(HMAC,建议从部署系统注入,避免写入代码/仓库)
|
||||
PLAYX_DAILY_PUSH_SECRET=
|
||||
# 第三方每日推送原始日志保留天数(runtime/logs/daily_push_raw,默认 30)
|
||||
PLAYX_DAILY_PUSH_RAW_LOG_DAYS=30
|
||||
# 合作方回调 JWT 验签密钥(HS256,与对端私发密钥一致;与上一项可同时配置,则两种均需通过)
|
||||
PLAYX_PARTNER_JWT_SECRET=
|
||||
# Agent authtoken(/api/v1/authToken)JWT 签名密钥;留空则使用下方 buildadmin.token.key
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace app\admin\controller\mall;
|
||||
|
||||
namespace app\admin\controller\mall;
|
||||
|
||||
|
||||
|
||||
use Throwable;
|
||||
|
||||
@@ -17,7 +18,7 @@ class Address extends Backend
|
||||
|
||||
* 收获地址管理
|
||||
|
||||
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time'];
|
||||
*/
|
||||
|
||||
class Address extends Backend
|
||||
|
||||
@@ -26,7 +27,7 @@ class Address extends Backend
|
||||
/**
|
||||
|
||||
* MallAddress模型对象
|
||||
$this->model = new \app\common\model\MallAddress();
|
||||
|
||||
* @var object|null
|
||||
|
||||
* @phpstan-var \app\common\model\MallAddress|null
|
||||
@@ -45,11 +46,6 @@ class Address extends Backend
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 1. withJoin 不可使用 alias 方法设置表别名,别名将自动使用关联模型名称(小写下划线命名规则)
|
||||
* 2. 以下的别名设置了主表别名,同时便于拼接查询参数等
|
||||
* 3. paginate 数据集可使用链式操作 each(function($item, $key) {}) 遍历处理
|
||||
*/
|
||||
protected string|array $quickSearchField = ['id'];
|
||||
|
||||
|
||||
@@ -68,7 +64,40 @@ class Address extends Backend
|
||||
|
||||
/**
|
||||
|
||||
/**
|
||||
* 若需重写查看、编辑、删除等方法,请复制 @see \app\admin\library\traits\Backend 中对应的方法至此进行重写
|
||||
*/
|
||||
* 查看
|
||||
|
||||
* @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');
|
||||
@@ -93,7 +93,7 @@ class DailyPush extends Backend
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出 Excel(CSV 流式写入,兼容 Excel 打开)
|
||||
* 导出 Excel(XLSX 流式写入)
|
||||
*/
|
||||
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'));
|
||||
}
|
||||
|
||||
|
||||
@@ -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', '')));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
70
app/common/library/MallDailyPushRawLogger.php
Normal file
70
app/common/library/MallDailyPushRawLogger.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
"firebase/php-jwt": "^7.0",
|
||||
"guzzlehttp/guzzle": "^7.10",
|
||||
"robthree/twofactorauth": "^3.0",
|
||||
"bacon/bacon-qr-code": "^3.1"
|
||||
"bacon/bacon-qr-code": "^3.1",
|
||||
"openspout/openspout": "^4.28"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-event": "For better performance. "
|
||||
|
||||
@@ -13,6 +13,8 @@ return [
|
||||
'points_to_cash_ratio' => floatval(env('PLAYX_POINTS_TO_CASH_RATIO', '0.1')),
|
||||
// Daily Push 签名校验(PlayX 调用商城时使用)
|
||||
'daily_push_secret' => strval(env('PLAYX_DAILY_PUSH_SECRET', '')),
|
||||
/** 第三方每日推送原始日志保留天数(runtime/logs/daily_push_raw) */
|
||||
'daily_push_raw_log_days' => intval(env('PLAYX_DAILY_PUSH_RAW_LOG_DAYS', '30')),
|
||||
/**
|
||||
* 合作方 JWT 验签密钥(HS256)。非空时:dailyPush 等回调需带 Authorization: Bearer
|
||||
* 仅写入部署环境变量,勿提交仓库。
|
||||
|
||||
@@ -5,6 +5,7 @@ export default {
|
||||
receiver_name: 'receiver name',
|
||||
phone: 'phone',
|
||||
region: 'region',
|
||||
region_tip: 'Separate levels with commas, e.g. Kuala Lumpur,KLCC',
|
||||
detail_address: 'detail_address',
|
||||
default_setting: 'Default address',
|
||||
'default_setting 0': '--',
|
||||
|
||||
@@ -11,15 +11,15 @@ export default {
|
||||
'quick Search Fields': 'ID',
|
||||
export_excel: 'Export Excel',
|
||||
export_title: 'Export daily push data',
|
||||
export_tip: 'Exports data using the current filters and sort order. The server writes in batches to avoid freezing on large datasets.',
|
||||
export_tip: 'Exports filtered and sorted data as Excel (.xlsx). The server writes in batches and supports exporting all matched records.',
|
||||
export_fields: 'Export fields',
|
||||
export_select_all: 'Select all',
|
||||
export_clear_all: 'Clear all',
|
||||
export_limit: 'Export limit',
|
||||
export_limit: 'Export limit (0 = all matched)',
|
||||
export_all_matched: 'All matched',
|
||||
export_matched_count: '{count} record(s) match the current filters',
|
||||
export_actual_count: 'will export {count} record(s)',
|
||||
export_large_warning: 'Large export may take a while. Maximum {max} records per export.',
|
||||
export_large_warning: 'Large export may take a while. Please keep this page open.',
|
||||
export_confirm: 'Start export',
|
||||
export_success: 'Successfully exported {count} record(s)',
|
||||
export_failed: 'Export failed, please try again later',
|
||||
|
||||
@@ -5,6 +5,7 @@ export default {
|
||||
receiver_name: '收货人',
|
||||
phone: '电话',
|
||||
region: '地区',
|
||||
region_tip: '多个层级请用英文逗号分隔,如:广东省,广州市,天河区',
|
||||
detail_address: '详细地址',
|
||||
default_setting: '默认地址',
|
||||
'default_setting 0': '--',
|
||||
|
||||
@@ -11,15 +11,15 @@ export default {
|
||||
'quick Search Fields': 'ID',
|
||||
export_excel: 'Excel导出',
|
||||
export_title: '导出每日推送数据',
|
||||
export_tip: '将按当前列表筛选条件与排序导出数据;采用服务端分批写入,避免一次性加载导致卡顿。',
|
||||
export_tip: '将按当前列表筛选条件与排序导出为 Excel(.xlsx);服务端分批写入,支持导出全部匹配数据。',
|
||||
export_fields: '导出字段',
|
||||
export_select_all: '全选',
|
||||
export_clear_all: '清空',
|
||||
export_limit: '导出条数',
|
||||
export_limit: '导出条数(0 表示全部匹配)',
|
||||
export_all_matched: '全部匹配',
|
||||
export_matched_count: '当前筛选条件下共 {count} 条',
|
||||
export_actual_count: '实际将导出 {count} 条',
|
||||
export_large_warning: '导出条数较多,可能需要等待较长时间;单次最多导出 {max} 条。',
|
||||
export_large_warning: '导出条数较多,可能需要等待较长时间,请勿关闭页面。',
|
||||
export_confirm: '开始导出',
|
||||
export_success: '已成功导出 {count} 条数据',
|
||||
export_failed: '导出失败,请稍后重试',
|
||||
|
||||
@@ -50,6 +50,17 @@ const formatRegion = (raw: string) => {
|
||||
return s.replace(/[,,\s]+/g, ',')
|
||||
}
|
||||
|
||||
const normalizeRegionInput = (raw: unknown) => {
|
||||
const s = String(raw ?? '').trim()
|
||||
if (!s) return ''
|
||||
return s
|
||||
.replace(/,/g, ',')
|
||||
.split(',')
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean)
|
||||
.join(',')
|
||||
}
|
||||
|
||||
/**
|
||||
* baTable 内包含了表格的所有数据且数据具备响应性,然后通过 provide 注入给了后代组件
|
||||
*/
|
||||
@@ -145,6 +156,15 @@ const baTable = new baTableClass(
|
||||
},
|
||||
{
|
||||
defaultItems: {},
|
||||
},
|
||||
{
|
||||
onSubmit: () => {
|
||||
const items = baTable.form.items
|
||||
if (items && items.region !== undefined && items.region !== null) {
|
||||
items.region = normalizeRegionInput(items.region)
|
||||
}
|
||||
return true
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -53,10 +53,10 @@
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.address.region')"
|
||||
type="city"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.region"
|
||||
prop="region"
|
||||
:placeholder="t('Please select field', { field: t('mall.address.region') })"
|
||||
:placeholder="t('mall.address.region_tip')"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.address.detail_address')"
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<div class="export-section">
|
||||
<div class="section-title">{{ t('mall.dailyPush.export_limit') }}</div>
|
||||
<div class="limit-row">
|
||||
<el-input-number v-model="form.exportLimit" :min="1" :max="maxLimit" :step="1000" controls-position="right" />
|
||||
<el-input-number v-model="form.exportLimit" :min="0" :max="matchedCount || undefined" :step="1000" controls-position="right" />
|
||||
<div class="limit-presets">
|
||||
<el-button
|
||||
v-for="preset in limitPresets"
|
||||
@@ -43,19 +43,19 @@
|
||||
>
|
||||
{{ preset }}
|
||||
</el-button>
|
||||
<el-button size="small" :type="form.exportLimit === matchedCount ? 'primary' : 'default'" @click="useMatchedCount">
|
||||
<el-button size="small" :type="form.exportLimit === 0 ? 'primary' : 'default'" @click="useMatchedCount">
|
||||
{{ t('mall.dailyPush.export_all_matched') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="count-info" v-loading="countLoading">
|
||||
{{ t('mall.dailyPush.export_matched_count', { count: matchedCount }) }}
|
||||
<span v-if="actualExportCount < form.exportLimit">
|
||||
<span v-if="actualExportCount > 0">
|
||||
,{{ t('mall.dailyPush.export_actual_count', { count: actualExportCount }) }}
|
||||
</span>
|
||||
</div>
|
||||
<el-alert v-if="form.exportLimit > 10000" type="warning" :closable="false" class="large-export-warning">
|
||||
{{ t('mall.dailyPush.export_large_warning', { max: maxLimit }) }}
|
||||
<el-alert v-if="actualExportCount > 10000" type="warning" :closable="false" class="large-export-warning">
|
||||
{{ t('mall.dailyPush.export_large_warning') }}
|
||||
</el-alert>
|
||||
</div>
|
||||
|
||||
@@ -84,7 +84,6 @@ const visible = defineModel<boolean>({ default: false })
|
||||
const exporting = ref(false)
|
||||
const countLoading = ref(false)
|
||||
const matchedCount = ref(0)
|
||||
const maxLimit = ref(100000)
|
||||
const limitPresets = [1000, 5000, 10000, 50000]
|
||||
|
||||
const fieldOptions = computed(() => [
|
||||
@@ -101,10 +100,15 @@ const fieldOptions = computed(() => [
|
||||
|
||||
const form = reactive({
|
||||
fields: fieldOptions.value.map((item) => item.value),
|
||||
exportLimit: 10000,
|
||||
exportLimit: 0,
|
||||
})
|
||||
|
||||
const actualExportCount = computed(() => Math.min(form.exportLimit, matchedCount.value, maxLimit.value))
|
||||
const actualExportCount = computed(() => {
|
||||
if (form.exportLimit <= 0) {
|
||||
return matchedCount.value
|
||||
}
|
||||
return Math.min(form.exportLimit, matchedCount.value)
|
||||
})
|
||||
|
||||
const selectAllFields = () => {
|
||||
form.fields = fieldOptions.value.map((item) => item.value)
|
||||
@@ -115,7 +119,7 @@ const clearFields = () => {
|
||||
}
|
||||
|
||||
const useMatchedCount = () => {
|
||||
form.exportLimit = Math.min(matchedCount.value || 1, maxLimit.value)
|
||||
form.exportLimit = 0
|
||||
}
|
||||
|
||||
const buildExportParams = () => {
|
||||
@@ -123,7 +127,7 @@ const buildExportParams = () => {
|
||||
return {
|
||||
...filter,
|
||||
fields: form.fields,
|
||||
export_limit: actualExportCount.value,
|
||||
export_limit: form.exportLimit <= 0 ? 0 : form.exportLimit,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +140,6 @@ const loadMatchedCount = async () => {
|
||||
params: baTable.table.filter || {},
|
||||
})
|
||||
matchedCount.value = res.data.count
|
||||
maxLimit.value = res.data.max_limit
|
||||
} catch {
|
||||
matchedCount.value = baTable.table.total || 0
|
||||
} finally {
|
||||
@@ -197,7 +200,7 @@ const submitExport = async () => {
|
||||
|
||||
const disposition = String(response.headers['content-disposition'] || '')
|
||||
const filenameMatch = disposition.match(/filename="?([^";]+)"?/i)
|
||||
const filename = filenameMatch?.[1] || `daily_push_${Date.now()}.csv`
|
||||
const filename = filenameMatch?.[1] || `daily_push_${Date.now()}.xlsx`
|
||||
downloadBlob(blob, filename)
|
||||
|
||||
ElNotification({
|
||||
|
||||
Reference in New Issue
Block a user