Просмотр исходного кода

Merge branch 'feature/EK-5175' of guanli/ngds into master

ryan001 1 месяц назад
Родитель
Сommit
95a6f8292a

+ 1 - 0
backendApi/config/main.php

@@ -18,6 +18,7 @@ return [
     'components' => [
         'request' => [
             'class' => \common\components\Request::class,
+            'cookieValidationKey' => '98bS8sqf3iRmSy24ZGDug2e36pLmj2wN',
             'parsers' => [
                 'application/json' => 'yii\web\JsonParser',
                 'text/json' => 'yii\web\JsonParser',

+ 1 - 0
backendApi/config/menu.php

@@ -36,6 +36,7 @@ return [
             ['name'=>'订单PDF导出', 'class'=>'', 'icon'=>'', 'controller'=>'shop', 'action'=>'order-list-export-pdf', 'routePath'=>'shop/order-list-export-pdf', 'show'=>0,],
             ['name'=>'订单发货', 'class'=>'', 'icon'=>'', 'controller'=>'shop', 'action'=>'order-delivery', 'routePath'=>'shop/order-delivery', 'show'=>0,],
             ['name'=>'订单期数调整', 'class'=>'', 'icon'=>'', 'controller'=>'shop', 'action'=>'order-period-adjust', 'routePath'=>'shop/order-period-adjust', 'show'=>1, 'wiki' => 'shopOrderPeriodAdjust'],
+            ['name'=>'订单期数批量调整', 'class'=>'', 'icon'=>'', 'controller'=>'shop', 'action'=>'order-period-adjust-range', 'routePath'=>'shop/order-period-adjust-range', 'show'=>1, 'wiki' => 'shopOrderPeriodAdjustRange'],
         ]
     ],
     'user'=>[

+ 2 - 0
backendApi/config/params.php

@@ -25,6 +25,7 @@ return [
         'v1/calc/record-list',
         'v1/calc/auto-calc',
         'v1/shop/order-period-adjust-batch',
+        'v1/shop/order-period-adjust-range',
         'v1/shop/order-invoice-remark',
         'v1/shop/order-invoice-method',
         'v1/shop/order-invoice-export',
@@ -97,6 +98,7 @@ return [
         'calc/record-list',
         'calc/auto-calc',
         'shop/order-period-adjust-batch',
+        'shop/order-period-adjust-range',
         'shop/order-invoice-remark',
         'shop/order-invoice-method',
         'shop/order-invoice-export',

+ 1 - 0
backendApi/config/urlManagerRules.php

@@ -80,6 +80,7 @@ return [
             'GET ba-order-list-export' => 'ba-order-list-export',
             'GET ba-order-list-export-pdf/<orderSn>' => 'ba-order-list-export-pdf',
             'GET,POST order-period-adjust' => 'order-period-adjust',
+            'GET,POST order-period-adjust-range' => 'order-period-adjust-range',
             'GET order-detail' => 'order-detail',
             'GET period-inspect' => 'period-inspect',
             'GET,POST order-period-adjust-batch' => 'order-period-adjust-batch',

+ 78 - 0
backendApi/modules/v1/controllers/ShopController.php

@@ -42,6 +42,7 @@ use common\models\forms\OrderDeleteForm;
 use common\models\forms\OrderForm;
 use common\models\forms\OrderInvoiceRemarkForm;
 use common\models\forms\OrderPeriodAdjustForm;
+use common\models\forms\OrderPeriodAdjustRangeForm;
 use common\models\forms\ShopGoodsForm;
 use common\models\forms\UploadForm;
 use common\models\Order;
@@ -54,6 +55,7 @@ use common\models\UserInfo;
 use common\helpers\Tool;
 use Yii;
 use yii\web\HttpException;
+use yii\web\Response;
 use yii\web\UploadedFile;
 use yii\base\Exception;
 
@@ -1100,6 +1102,82 @@ class ShopController extends BaseController {
      * @return mixed
      * @throws \yii\web\HttpException
      */
+    /**
+     * 按时间范围批量调整订单期数
+     * @return mixed
+     * @throws Exception
+     * @throws \yii\db\Exception
+     * @throws \yii\web\HttpException
+     */
+    public function actionOrderPeriodAdjustRange()
+    {
+        $isViewPage = (int)\Yii::$app->request->get('view', 0) === 1;
+
+        if (\Yii::$app->request->isPost) {
+            $formModel = new OrderPeriodAdjustRangeForm();
+            $formModel->scenario = 'orderPeriodAdjustRange';
+            if ($formModel->load(\Yii::$app->request->post(), '') && $result = $formModel->periodAdjustRange()) {
+                if ($isViewPage) {
+                    \Yii::$app->response->format = Response::FORMAT_HTML;
+                    return $this->renderPartial('order-period-adjust-range', [
+                        'defaults' => [],
+                        'formModel' => $formModel,
+                        'result' => $result,
+                        'errors' => [],
+                        'submitted' => true,
+                    ]);
+                }
+                return static::notice($result);
+            }
+            if ($isViewPage) {
+                \Yii::$app->response->format = Response::FORMAT_HTML;
+                return $this->renderPartial('order-period-adjust-range', [
+                    'defaults' => [],
+                    'formModel' => $formModel,
+                    'result' => null,
+                    'errors' => $formModel->getErrors(),
+                    'submitted' => true,
+                ]);
+            }
+            return static::notice(Form::formatErrorsForApi($formModel->getErrors()), 400);
+        }
+
+        if ($isViewPage) {
+            \Yii::$app->response->format = Response::FORMAT_HTML;
+            $defaults = [
+                'start_time' => date('Y-m-01 00:00:00'),
+                'end_time' => date('Y-m-d H:i:s'),
+                'period_num' => (string)((new Period())->getNowPeriodNum()-1),
+                'operate_user' => '',
+            ];
+
+            $formModel = new OrderPeriodAdjustRangeForm();
+            $formModel->start_time = $defaults['start_time'];
+            $formModel->end_time = $defaults['end_time'];
+            $formModel->period_num = $defaults['period_num'];
+            $formModel->operate_user = $defaults['operate_user'];
+
+            return $this->renderPartial('order-period-adjust-range', [
+                'defaults' => $defaults,
+                'formModel' => $formModel,
+                'result' => null,
+                'errors' => [],
+                'submitted' => false,
+            ]);
+        }
+
+        return static::notice([
+            'requiredParams' => [
+                'start_time',
+                'end_time',
+                'period_num',
+                'operate_user',
+            ],
+            'description' => 'Batch adjust orders by created_at range and target period.',
+            'viewUrl' => '/v1/shop/order-period-adjust-range?view=1',
+        ]);
+    }
+
     public function actionOrderDetail()
     {
         $orderSn = \Yii::$app->request->get('orderSn');

+ 383 - 0
backendApi/modules/v1/views/shop/order-period-adjust-range.php

@@ -0,0 +1,383 @@
+<?php
+
+use yii\helpers\Html;
+use yii\helpers\Url;
+
+/** @var array $defaults */
+/** @var \common\models\forms\OrderPeriodAdjustRangeForm $formModel */
+/** @var array|null $result */
+/** @var array $errors */
+/** @var bool $submitted */
+
+$title = 'Order Period Adjust Range';
+
+$e = static function ($value): string {
+    return Html::encode((string)$value);
+};
+
+$renderList = static function (array $items) use ($e): string {
+    if (!$items) {
+        return '<div class="muted">None</div>';
+    }
+
+    $html = '<ul class="list">';
+    foreach ($items as $item) {
+        $html .= '<li>' . $e($item) . '</li>';
+    }
+    $html .= '</ul>';
+    return $html;
+};
+
+$renderErrorMap = static function (array $errorMap) use ($e): string {
+    if (!$errorMap) {
+        return '<div class="muted">None</div>';
+    }
+
+    $html = '<div class="error-map">';
+    foreach ($errorMap as $orderSn => $messages) {
+        $html .= '<div class="error-item">';
+        $html .= '<div><strong>' . $e($orderSn) . '</strong></div>';
+        foreach ((array)$messages as $field => $messageList) {
+            $html .= '<div class="muted">' . $e($field) . ': ' . $e(is_array($messageList) ? implode('; ', $messageList) : $messageList) . '</div>';
+        }
+        $html .= '</div>';
+    }
+    $html .= '</div>';
+    return $html;
+};
+
+$renderReasonMap = static function (array $reasonMap) use ($e): string {
+    if (!$reasonMap) {
+        return '<div class="muted">None</div>';
+    }
+
+    $html = '<div class="reason-map">';
+    foreach ($reasonMap as $orderSn => $info) {
+        $html .= '<div class="reason-item">';
+        $html .= '<div><strong>' . $e($orderSn) . '</strong></div>';
+        $html .= '<div class="muted">原因: ' . $e($info['reason'] ?? '') . '</div>';
+        if (isset($info['current_period'])) {
+            $html .= '<div class="muted">当前期数: ' . $e($info['current_period']) . '</div>';
+        }
+        if (isset($info['target_period'])) {
+            $html .= '<div class="muted">目标期数: ' . $e($info['target_period']) . '</div>';
+        }
+        $html .= '</div>';
+    }
+    $html .= '</div>';
+    return $html;
+};
+
+?>
+<!doctype html>
+<html lang="zh-CN">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <title><?= $e($title) ?></title>
+    <style>
+        :root {
+            --bg: #f3f5f7;
+            --card: #ffffff;
+            --text: #1f2937;
+            --muted: #6b7280;
+            --accent: #2563eb;
+            --accent-soft: #dbeafe;
+            --danger: #b91c1c;
+            --success: #15803d;
+            --border: #e5e7eb;
+        }
+        body {
+            margin: 0;
+            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
+            background: linear-gradient(180deg, #eef2ff 0%, var(--bg) 40%, #f8fafc 100%);
+            color: var(--text);
+        }
+        .wrap {
+            max-width: 1120px;
+            margin: 0 auto;
+            padding: 28px 18px 48px;
+        }
+        .hero {
+            margin-bottom: 18px;
+            padding: 20px 22px;
+            background: rgba(255,255,255,.86);
+            border: 1px solid rgba(148,163,184,.2);
+            border-radius: 18px;
+            box-shadow: 0 10px 30px rgba(15,23,42,.08);
+        }
+        .hero h1 {
+            margin: 0 0 8px;
+            font-size: 26px;
+        }
+        .hero p {
+            margin: 6px 0;
+            color: var(--muted);
+            line-height: 1.6;
+        }
+        .grid {
+            display: grid;
+            grid-template-columns: 1.2fr .8fr;
+            gap: 18px;
+        }
+        .card {
+            background: var(--card);
+            border: 1px solid var(--border);
+            border-radius: 18px;
+            box-shadow: 0 10px 30px rgba(15,23,42,.06);
+            overflow: hidden;
+        }
+        .card h2 {
+            margin: 0;
+            padding: 16px 18px;
+            border-bottom: 1px solid var(--border);
+            font-size: 18px;
+        }
+        .card-body {
+            padding: 18px;
+        }
+        .field {
+            margin-bottom: 14px;
+        }
+        .field label {
+            display: block;
+            margin-bottom: 6px;
+            font-weight: 600;
+            font-size: 14px;
+        }
+        .field input {
+            width: 100%;
+            box-sizing: border-box;
+            padding: 11px 12px;
+            border: 1px solid var(--border);
+            border-radius: 12px;
+            font-size: 14px;
+            outline: none;
+        }
+        .field input:focus {
+            border-color: var(--accent);
+            box-shadow: 0 0 0 3px var(--accent-soft);
+        }
+        .btn {
+            display: inline-flex;
+            align-items: center;
+            justify-content: center;
+            padding: 11px 16px;
+            border: none;
+            border-radius: 12px;
+            background: var(--accent);
+            color: #fff;
+            font-weight: 700;
+            cursor: pointer;
+        }
+        .btn:hover {
+            opacity: .95;
+        }
+        .summary {
+            display: grid;
+            grid-template-columns: repeat(4, minmax(0, 1fr));
+            gap: 10px;
+            margin-bottom: 14px;
+        }
+        .stat {
+            border: 1px solid var(--border);
+            border-radius: 14px;
+            padding: 12px 14px;
+            background: #fafafa;
+        }
+        .stat .label {
+            color: var(--muted);
+            font-size: 12px;
+            margin-bottom: 6px;
+        }
+        .stat .value {
+            font-size: 18px;
+            font-weight: 700;
+        }
+        .muted {
+            color: var(--muted);
+        }
+        .section {
+            margin-bottom: 16px;
+            padding-bottom: 16px;
+            border-bottom: 1px dashed var(--border);
+        }
+        .section:last-child {
+            margin-bottom: 0;
+            padding-bottom: 0;
+            border-bottom: 0;
+        }
+        .badge {
+            display: inline-block;
+            margin-left: 8px;
+            padding: 2px 8px;
+            border-radius: 999px;
+            background: var(--accent-soft);
+            color: var(--accent);
+            font-size: 12px;
+            font-weight: 700;
+        }
+        .success {
+            color: var(--success);
+        }
+        .danger {
+            color: var(--danger);
+        }
+        .list {
+            margin: 8px 0 0;
+            padding-left: 18px;
+        }
+        .error-map .error-item {
+            margin-top: 10px;
+            padding: 10px 12px;
+            border: 1px solid #fee2e2;
+            background: #fffafa;
+            border-radius: 12px;
+        }
+        .reason-map .reason-item {
+            margin-top: 10px;
+            padding: 10px 12px;
+            border: 1px solid #dbeafe;
+            background: #f8fbff;
+            border-radius: 12px;
+        }
+        pre {
+            margin: 0;
+            padding: 14px;
+            overflow: auto;
+            background: #0f172a;
+            color: #e2e8f0;
+            border-radius: 14px;
+            font-size: 12px;
+            line-height: 1.6;
+        }
+        @media (max-width: 900px) {
+            .grid, .summary {
+                grid-template-columns: 1fr;
+            }
+        }
+    </style>
+</head>
+<body>
+<div class="wrap">
+    <div class="hero">
+        <h1><?= $e($title) ?></h1>
+        <p>Use this page to batch adjust orders by <code>start_time</code>, <code>end_time</code>, <code>period_num</code> and <code>operate_user</code>.</p>
+        <p>Open via <code>/v1/shop/order-period-adjust-range?view=1</code>. The result panel shows successful, failed and skipped order numbers after execution.</p>
+    </div>
+
+    <div class="grid">
+        <div class="card">
+            <h2>Execution Form</h2>
+            <div class="card-body">
+                <?= Html::beginForm(Url::current(['view' => 1]), 'post') ?>
+                    <div class="field">
+                        <label for="start_time">start_time</label>
+                        <input id="start_time" name="start_time" value="<?= $e($formModel->start_time ?? ($defaults['start_time'] ?? '')) ?>">
+                    </div>
+                    <div class="field">
+                        <label for="end_time">end_time</label>
+                        <input id="end_time" name="end_time" value="<?= $e($formModel->end_time ?? ($defaults['end_time'] ?? '')) ?>">
+                    </div>
+                    <div class="field">
+                        <label for="period_num">period_num</label>
+                        <input id="period_num" name="period_num" value="<?= $e($formModel->period_num ?? ($defaults['period_num'] ?? '')) ?>">
+                    </div>
+                    <div class="field">
+                        <label for="operate_user">operate_user</label>
+                        <input id="operate_user" name="operate_user" value="<?= $e($formModel->operate_user ?? ($defaults['operate_user'] ?? '')) ?>">
+                    </div>
+                    <button class="btn" type="submit">Execute</button>
+                <?= Html::endForm() ?>
+            </div>
+        </div>
+
+        <div class="card">
+            <h2>Configuration</h2>
+            <div class="card-body">
+                <div class="section">
+                    <div class="muted">Default start_time</div>
+                    <div><?= $e($defaults['start_time'] ?? '') ?></div>
+                </div>
+                <div class="section">
+                    <div class="muted">Default end_time</div>
+                    <div><?= $e($defaults['end_time'] ?? '') ?></div>
+                </div>
+                <div class="section">
+                    <div class="muted">Default period_num</div>
+                    <div><?= $e($defaults['period_num'] ?? '') ?></div>
+                </div>
+                <div class="section">
+                    <div class="muted">Default operate_user</div>
+                    <div><?= $e($defaults['operate_user'] ?? '') ?></div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <?php if ($submitted): ?>
+        <div class="card" style="margin-top: 18px;">
+            <h2>Execution Result</h2>
+            <div class="card-body">
+                <?php if ($result): ?>
+                    <div class="summary">
+                        <div class="stat">
+                            <div class="label">Total</div>
+                            <div class="value"><?= $e($result['total_count'] ?? 0) ?></div>
+                        </div>
+                        <div class="stat">
+                            <div class="label success">Success</div>
+                            <div class="value success"><?= $e($result['success_count'] ?? 0) ?></div>
+                        </div>
+                        <div class="stat">
+                            <div class="label danger">Failed</div>
+                            <div class="value danger"><?= $e($result['failed_count'] ?? 0) ?></div>
+                        </div>
+                        <div class="stat">
+                            <div class="label">Skipped</div>
+                            <div class="value"><?= $e($result['skipped_count'] ?? 0) ?></div>
+                        </div>
+                    </div>
+
+                    <div class="section">
+                        <div><strong>Success order numbers</strong><span class="badge"><?= $e($result['success_count'] ?? 0) ?></span></div>
+                        <?= $renderList($result['success_order_sn_list'] ?? []) ?>
+                    </div>
+
+                    <div class="section">
+                        <div><strong>Failed order numbers</strong><span class="badge"><?= $e($result['failed_count'] ?? 0) ?></span></div>
+                        <?= $renderList($result['failed_order_sn_list'] ?? []) ?>
+                        <div style="margin-top: 12px;">
+                            <strong>Failure details</strong>
+                            <?= $renderErrorMap($result['failed_detail_map'] ?? []) ?>
+                        </div>
+                    </div>
+
+                    <div class="section">
+                        <div><strong>Not modified order numbers</strong><span class="badge"><?= $e($result['skipped_count'] ?? 0) ?></span></div>
+                        <?= $renderList($result['skipped_order_sn_list'] ?? []) ?>
+                        <div style="margin-top: 12px;">
+                            <strong>Not modified reasons</strong>
+                            <?= $renderReasonMap($result['skipped_detail_map'] ?? []) ?>
+                        </div>
+                    </div>
+
+                    <div class="section">
+                        <div><strong>Raw payload</strong></div>
+                        <pre><?= $e(json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)) ?></pre>
+                    </div>
+                <?php else: ?>
+                    <div class="danger"><strong>Execution failed.</strong></div>
+                    <?php if ($errors): ?>
+                        <div style="margin-top: 12px;">
+                            <strong>Validation errors</strong>
+                            <pre><?= $e(json_encode($errors, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)) ?></pre>
+                        </div>
+                    <?php endif; ?>
+                <?php endif; ?>
+            </div>
+        </div>
+    <?php endif; ?>
+</div>
+</body>
+</html>

+ 390 - 0
backendApi/web/tools/order-period-adjust-range.php

@@ -0,0 +1,390 @@
+<?php
+
+declare(strict_types=1);
+
+date_default_timezone_set('Africa/Lagos');
+set_time_limit(0);
+
+require __DIR__ . '/../../../vendor/autoload.php';
+require __DIR__ . '/../../../vendor/yiisoft/yii2/Yii.php';
+require __DIR__ . '/../../../common/config/bootstrap.php';
+require __DIR__ . '/../../config/bootstrap.php';
+
+$config = yii\helpers\ArrayHelper::merge(
+    require __DIR__ . '/../../../common/config/main.php',
+    require __DIR__ . '/../config/main.php'
+);
+
+$app = new \yii\web\Application($config);
+
+$defaults = [
+    'start_time' => date('Y-m-01 00:00:00'),
+    'end_time' => date('Y-m-d H:i:s'),
+    'period_num' => (string)((new \common\models\Period())->getNowPeriodNum()),
+    'operate_user' => 'admin_dba',
+];
+
+$result = null;
+$errors = [];
+$submitted = false;
+
+if (\Yii::$app->request->isPost) {
+    $submitted = true;
+    $formModel = new \common\models\forms\OrderPeriodAdjustRangeForm();
+    $formModel->scenario = 'orderPeriodAdjustRange';
+    if ($formModel->load(\Yii::$app->request->post(), '') && ($result = $formModel->periodAdjustRange())) {
+        // success
+    } else {
+        $errors = $formModel->getErrors();
+        if (!$errors && $formModel->orders === []) {
+            $errors = ['form' => ['No result returned from execution.']];
+        }
+    }
+} else {
+    $formModel = new \common\models\forms\OrderPeriodAdjustRangeForm();
+    $formModel->start_time = $defaults['start_time'];
+    $formModel->end_time = $defaults['end_time'];
+    $formModel->period_num = $defaults['period_num'];
+    $formModel->operate_user = $defaults['operate_user'];
+}
+
+function e($value): string
+{
+    return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
+}
+
+function renderList(array $items): string
+{
+    if (!$items) {
+        return '<div class="muted">None</div>';
+    }
+
+    $html = '<ul class="list">';
+    foreach ($items as $item) {
+        $html .= '<li>' . e($item) . '</li>';
+    }
+    $html .= '</ul>';
+    return $html;
+}
+
+function renderErrorMap(array $errorMap): string
+{
+    if (!$errorMap) {
+        return '<div class="muted">None</div>';
+    }
+
+    $html = '<div class="error-map">';
+    foreach ($errorMap as $orderSn => $messages) {
+        $html .= '<div class="error-item">';
+        $html .= '<div><strong>' . e($orderSn) . '</strong></div>';
+        foreach ((array)$messages as $field => $messageList) {
+            $html .= '<div class="muted">' . e($field) . ': ' . e(is_array($messageList) ? implode('; ', $messageList) : $messageList) . '</div>';
+        }
+        $html .= '</div>';
+    }
+    $html .= '</div>';
+    return $html;
+}
+
+?>
+<!doctype html>
+<html lang="zh-CN">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <title>Order Period Adjust Range Tool</title>
+    <style>
+        :root {
+            --bg: #f3f5f7;
+            --card: #ffffff;
+            --text: #1f2937;
+            --muted: #6b7280;
+            --accent: #2563eb;
+            --accent-soft: #dbeafe;
+            --danger: #b91c1c;
+            --success: #15803d;
+            --border: #e5e7eb;
+        }
+        body {
+            margin: 0;
+            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
+            background: linear-gradient(180deg, #eef2ff 0%, var(--bg) 40%, #f8fafc 100%);
+            color: var(--text);
+        }
+        .wrap {
+            max-width: 1120px;
+            margin: 0 auto;
+            padding: 28px 18px 48px;
+        }
+        .hero {
+            margin-bottom: 18px;
+            padding: 20px 22px;
+            background: rgba(255,255,255,.86);
+            border: 1px solid rgba(148,163,184,.2);
+            border-radius: 18px;
+            box-shadow: 0 10px 30px rgba(15,23,42,.08);
+        }
+        .hero h1 {
+            margin: 0 0 8px;
+            font-size: 26px;
+        }
+        .hero p {
+            margin: 6px 0;
+            color: var(--muted);
+            line-height: 1.6;
+        }
+        .grid {
+            display: grid;
+            grid-template-columns: 1.2fr .8fr;
+            gap: 18px;
+        }
+        .card {
+            background: var(--card);
+            border: 1px solid var(--border);
+            border-radius: 18px;
+            box-shadow: 0 10px 30px rgba(15,23,42,.06);
+            overflow: hidden;
+        }
+        .card h2 {
+            margin: 0;
+            padding: 16px 18px;
+            border-bottom: 1px solid var(--border);
+            font-size: 18px;
+        }
+        .card-body {
+            padding: 18px;
+        }
+        .field {
+            margin-bottom: 14px;
+        }
+        .field label {
+            display: block;
+            margin-bottom: 6px;
+            font-weight: 600;
+            font-size: 14px;
+        }
+        .field input {
+            width: 100%;
+            box-sizing: border-box;
+            padding: 11px 12px;
+            border: 1px solid var(--border);
+            border-radius: 12px;
+            font-size: 14px;
+            outline: none;
+        }
+        .field input:focus {
+            border-color: var(--accent);
+            box-shadow: 0 0 0 3px var(--accent-soft);
+        }
+        .btn {
+            display: inline-flex;
+            align-items: center;
+            justify-content: center;
+            padding: 11px 16px;
+            border: none;
+            border-radius: 12px;
+            background: var(--accent);
+            color: #fff;
+            font-weight: 700;
+            cursor: pointer;
+        }
+        .btn:hover {
+            opacity: .95;
+        }
+        .summary {
+            display: grid;
+            grid-template-columns: repeat(4, minmax(0, 1fr));
+            gap: 10px;
+            margin-bottom: 14px;
+        }
+        .stat {
+            border: 1px solid var(--border);
+            border-radius: 14px;
+            padding: 12px 14px;
+            background: #fafafa;
+        }
+        .stat .label {
+            color: var(--muted);
+            font-size: 12px;
+            margin-bottom: 6px;
+        }
+        .stat .value {
+            font-size: 18px;
+            font-weight: 700;
+        }
+        .muted {
+            color: var(--muted);
+        }
+        .section {
+            margin-bottom: 16px;
+            padding-bottom: 16px;
+            border-bottom: 1px dashed var(--border);
+        }
+        .section:last-child {
+            margin-bottom: 0;
+            padding-bottom: 0;
+            border-bottom: 0;
+        }
+        .badge {
+            display: inline-block;
+            margin-left: 8px;
+            padding: 2px 8px;
+            border-radius: 999px;
+            background: var(--accent-soft);
+            color: var(--accent);
+            font-size: 12px;
+            font-weight: 700;
+        }
+        .success {
+            color: var(--success);
+        }
+        .danger {
+            color: var(--danger);
+        }
+        .list {
+            margin: 8px 0 0;
+            padding-left: 18px;
+        }
+        .error-map .error-item {
+            margin-top: 10px;
+            padding: 10px 12px;
+            border: 1px solid #fee2e2;
+            background: #fffafa;
+            border-radius: 12px;
+        }
+        pre {
+            margin: 0;
+            padding: 14px;
+            overflow: auto;
+            background: #0f172a;
+            color: #e2e8f0;
+            border-radius: 14px;
+            font-size: 12px;
+            line-height: 1.6;
+        }
+        @media (max-width: 900px) {
+            .grid, .summary {
+                grid-template-columns: 1fr;
+            }
+        }
+    </style>
+</head>
+<body>
+<div class="wrap">
+    <div class="hero">
+        <h1>Order Period Adjust Range Tool</h1>
+        <p>Use this page to batch adjust orders by <code>start_time</code>, <code>end_time</code>, <code>period_num</code> and <code>operate_user</code>.</p>
+        <p>Time zone: Africa/Lagos. The result panel shows successful, failed and skipped order numbers after execution.</p>
+    </div>
+
+    <div class="grid">
+        <div class="card">
+            <h2>Execution Form</h2>
+            <div class="card-body">
+                <form method="post">
+                    <div class="field">
+                        <label for="start_time">start_time</label>
+                        <input id="start_time" name="start_time" value="<?= e($formModel->start_time ?? $defaults['start_time']) ?>">
+                    </div>
+                    <div class="field">
+                        <label for="end_time">end_time</label>
+                        <input id="end_time" name="end_time" value="<?= e($formModel->end_time ?? $defaults['end_time']) ?>">
+                    </div>
+                    <div class="field">
+                        <label for="period_num">period_num</label>
+                        <input id="period_num" name="period_num" value="<?= e($formModel->period_num ?? $defaults['period_num']) ?>">
+                    </div>
+                    <div class="field">
+                        <label for="operate_user">operate_user</label>
+                        <input id="operate_user" name="operate_user" value="<?= e($formModel->operate_user ?? $defaults['operate_user']) ?>">
+                    </div>
+                    <button class="btn" type="submit">Execute</button>
+                </form>
+            </div>
+        </div>
+
+        <div class="card">
+            <h2>Configuration</h2>
+            <div class="card-body">
+                <div class="section">
+                    <div class="muted">Default start_time</div>
+                    <div><?= e($defaults['start_time']) ?></div>
+                </div>
+                <div class="section">
+                    <div class="muted">Default end_time</div>
+                    <div><?= e($defaults['end_time']) ?></div>
+                </div>
+                <div class="section">
+                    <div class="muted">Default period_num</div>
+                    <div><?= e($defaults['period_num']) ?></div>
+                </div>
+                <div class="section">
+                    <div class="muted">Default operate_user</div>
+                    <div><?= e($defaults['operate_user']) ?></div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <?php if ($submitted): ?>
+        <div class="card" style="margin-top: 18px;">
+            <h2>Execution Result</h2>
+            <div class="card-body">
+                <?php if ($result): ?>
+                    <div class="summary">
+                        <div class="stat">
+                            <div class="label">Total</div>
+                            <div class="value"><?= e($result['total_count'] ?? 0) ?></div>
+                        </div>
+                        <div class="stat">
+                            <div class="label success">Success</div>
+                            <div class="value success"><?= e($result['success_count'] ?? 0) ?></div>
+                        </div>
+                        <div class="stat">
+                            <div class="label danger">Failed</div>
+                            <div class="value danger"><?= e($result['failed_count'] ?? 0) ?></div>
+                        </div>
+                        <div class="stat">
+                            <div class="label">Skipped</div>
+                            <div class="value"><?= e($result['skipped_count'] ?? 0) ?></div>
+                        </div>
+                    </div>
+
+                    <div class="section">
+                        <div><strong>Success order numbers</strong><span class="badge"><?= e($result['success_count'] ?? 0) ?></span></div>
+                        <?= renderList($result['success_order_sn_list'] ?? []) ?>
+                    </div>
+
+                    <div class="section">
+                        <div><strong>Failed order numbers</strong><span class="badge"><?= e($result['failed_count'] ?? 0) ?></span></div>
+                        <?= renderList($result['failed_order_sn_list'] ?? []) ?>
+                        <div style="margin-top: 12px;">
+                            <strong>Failure details</strong>
+                            <?= renderErrorMap($result['failed_detail_map'] ?? []) ?>
+                        </div>
+                    </div>
+
+                    <div class="section">
+                        <div><strong>Skipped order numbers</strong><span class="badge"><?= e($result['skipped_count'] ?? 0) ?></span></div>
+                        <?= renderList($result['skipped_order_sn_list'] ?? []) ?>
+                    </div>
+
+                    <div class="section">
+                        <div><strong>Raw payload</strong></div>
+                        <pre><?= e(json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)) ?></pre>
+                    </div>
+                <?php else: ?>
+                    <div class="danger"><strong>Execution failed.</strong></div>
+                    <?php if ($errors): ?>
+                        <div style="margin-top: 12px;">
+                            <strong>Validation errors</strong>
+                            <pre><?= e(json_encode($errors, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)) ?></pre>
+                        </div>
+                    <?php endif; ?>
+                <?php endif; ?>
+            </div>
+        </div>
+    <?php endif; ?>
+</div>
+</body>
+</html>

+ 6 - 3
common/libs/logging/operate/AdminOperate.php

@@ -43,14 +43,17 @@ class AdminOperate extends AbstractOperate {
 
         $period = Period::instance();
         $periodNum = $period->getNowPeriodNum();
+        $userInfo = method_exists(Yii::$app->user, 'getUserInfo') ? Yii::$app->user->getUserInfo() : null;
+        $adminId = $params['adminId'] ?? Yii::$app->user->id;
+        $adminName = $params['adminName'] ?? (is_array($userInfo) && isset($userInfo['adminName']) ? $userInfo['adminName'] : $adminId);
         $this->columns[] = [
             'user_agent' => Yii::$app->request->getUserAgent(),
             'created_at' => intval(Date::nowTime()),
             'period_num' => intval($periodNum),
             'save_before_content' => $this->saveBeforeContent,
             'save_after_content' => $this->saveAfterContent,
-            'admin_id' => Yii::$app->user->id,
-            'admin_name' => isset(\Yii::$app->user->getUserInfo()['adminName']) ?\Yii::$app->user->getUserInfo()['adminName'] : ($params['adminName'] ?? \Yii::$app->user->id),
+            'admin_id' => $adminId,
+            'admin_name' => $adminName,
             'ip' => Yii::$app->request->getUserIP(),
             'request_route' => Yii::$app->requestedRoute,
             'opt_type' => $params['optType'],
@@ -62,4 +65,4 @@ class AdminOperate extends AbstractOperate {
         ];
         return $this;
     }
-}
+}

+ 17 - 31
common/models/forms/OrderPeriodAdjustForm.php

@@ -1,10 +1,10 @@
 <?php
+
 namespace common\models\forms;
 
 use backendApi\modules\v1\models\Admin;
-use common\helpers\Date;
 use common\components\Model;
-use common\helpers\LoggerTool;
+use common\helpers\Date;
 use common\libs\logging\operate\AdminOperate;
 use common\models\DecOrder;
 use common\models\FlowExchangePoints;
@@ -21,25 +21,24 @@ use common\models\UserInfo;
 use Yii;
 
 /**
- * OrderPeriodAdjustForm form
+ * Order period adjust form.
  */
 class OrderPeriodAdjustForm extends Model
 {
     public $orderSn;
     public $modernPeriod;
+    public $operateUser;
 
     public $order;
 
-    public function init() {
+    public function init()
+    {
         parent::init();
         $this->adminOperateLogger = new AdminOperate([
             'fetchClass' => OrderPeriodAdjust::class,
         ]);
     }
 
-    /**
-     * @inheritdoc
-     */
     public function rules()
     {
         return [
@@ -56,33 +55,25 @@ class OrderPeriodAdjustForm extends Model
         ];
     }
 
-    /**
-     * 指定校验场景
-     * @return array
-     */
     public function scenarios()
     {
-        $parentScenarios =  parent::scenarios();
+        $parentScenarios = parent::scenarios();
         $customScenarios = [
             'orderPeriodAdjust' => ['orderSn', 'modernPeriod'],
         ];
         return array_merge($parentScenarios, $customScenarios);
     }
 
-    /**
-     * 校验之前
-     * @return bool
-     */
     public function beforeValidate()
     {
         $parentValidate = parent::beforeValidate();
-        // 校验订单
+
         $this->order = Order::findOne(['SN' => $this->orderSn, 'IS_DELETE' => 0]);
         if (!$this->order) {
             $this->addError('orderSn', Yii::t('ctx', 'orderNotExist'));
             return false;
         }
-        // 目前期数:已封期、未挂网可调整
+
         $period = Period::getInfoByPeriodNum($this->modernPeriod);
         if (!$period) {
             $this->addError('modernPeriod', Yii::t('ctx', 'invalidPcNo'));
@@ -96,33 +87,27 @@ class OrderPeriodAdjustForm extends Model
         return $parentValidate;
     }
 
-    /**
-     * @return bool|null
-     */
     public function periodAdjust()
     {
         if (!$this->validate()) {
             return null;
         }
 
+        $operator = $this->operateUser ?: Admin::getAdminNameById(\Yii::$app->user->id);
         $this->adminOperateLogger->saveBeforeContent = Order::findOneAsArray('SN=:SN', [':SN' => $this->orderSn]);
 
         $db = \Yii::$app->db;
         $transaction = $db->beginTransaction();
         try {
-            // 调整订单期数
             Order::updateAll(['PERIOD_NUM' => $this->modernPeriod], 'SN = :SN', [':SN' => $this->orderSn]);
 
             if ($this->order->ORDER_TYPE === 'ZC') {
-                // 调整报单期数
                 DecOrder::updateAll(['PERIOD_NUM' => $this->modernPeriod], 'ORDER_SN = :ORDER_SN', ['ORDER_SN' => $this->orderSn]);
-                // 调整会员的期数
                 User::updateAll(['PERIOD_AT' => $this->modernPeriod], 'ID = :USER_ID', ['USER_ID' => $this->order->USER_ID]);
                 UserInfo::updateAll(['HIGHEST_EMP_LV_PERIOD' => $this->modernPeriod], 'USER_ID = :USER_ID', ['USER_ID' => $this->order->USER_ID]);
             }
 
-            // 调整流水的期数
-            switch ($this->order->PAY_TYPE){
+            switch ($this->order->PAY_TYPE) {
                 case 'cash':
                     FlowWallet::updateAll(['PERIOD_NUM' => $this->modernPeriod], 'ORDER_SN = :ORDER_SN', ['ORDER_SN' => $this->orderSn]);
                     break;
@@ -145,20 +130,19 @@ class OrderPeriodAdjustForm extends Model
                     break;
             }
 
-            // 写入调整记录
             $orderModel = new OrderPeriodAdjust();
             $orderModel->ORDER_SN = $this->orderSn;
-            //$orderModel->DEC_SN = $this->order->DEC_SN ?? '';
             $orderModel->ORIGIN_PERIOD = $this->order->PERIOD_NUM;
             $orderModel->MODERN_PERIOD = $this->modernPeriod;
-            $orderModel->ADMIN_ID = Admin::getAdminNameById(\Yii::$app->user->id);
+            $orderModel->ADMIN_ID = $operator;
             $orderModel->CREATED_AT = Date::nowTime();
             if (!$orderModel->save()) {
                 $this->addErrors($orderModel->getErrors());
                 return false;
             }
+
             $transaction->commit();
-        } catch(\Exception $e) {
+        } catch (\Exception $e) {
             $transaction->rollBack();
             $this->addError('periodAdjust', $e->getMessage());
             return null;
@@ -166,7 +150,9 @@ class OrderPeriodAdjustForm extends Model
 
         $this->adminOperateLogger->saveAfterContent = Order::findOneAsArray('SN=:SN', [':SN' => $this->orderSn]);
         $this->adminOperateLogger->clean()->save([
-            'optType' => '调整订单期数',
+            'optType' => 'Adjust order period',
+            'adminId' => $operator,
+            'adminName' => $operator,
         ]);
 
         return true;

+ 187 - 0
common/models/forms/OrderPeriodAdjustRangeForm.php

@@ -0,0 +1,187 @@
+<?php
+
+namespace common\models\forms;
+
+use common\components\Model;
+use common\models\Order;
+use common\models\Period;
+use Yii;
+
+/**
+ * Batch order period adjust form.
+ */
+class OrderPeriodAdjustRangeForm extends Model
+{
+    public $start_time;
+    public $end_time;
+    public $period_num;
+    public $operate_user;
+
+    public $start_timestamp;
+    public $end_timestamp;
+    public $orders = [];
+
+    public function rules()
+    {
+        return [
+            [['start_time', 'end_time', 'period_num', 'operate_user'], 'trim'],
+            [['start_time', 'end_time', 'period_num', 'operate_user'], 'required'],
+        ];
+    }
+
+    public function attributeLabels()
+    {
+        return [
+            'start_time' => 'start_time',
+            'end_time' => 'end_time',
+            'period_num' => 'period_num',
+            'operate_user' => 'operate_user',
+        ];
+    }
+
+    public function scenarios()
+    {
+        $parentScenarios = parent::scenarios();
+        $customScenarios = [
+            'orderPeriodAdjustRange' => ['start_time', 'end_time', 'period_num', 'operate_user'],
+        ];
+        return array_merge($parentScenarios, $customScenarios);
+    }
+
+    public function beforeValidate()
+    {
+        $parentValidate = parent::beforeValidate();
+
+        $this->start_timestamp = strtotime($this->start_time);
+        $this->end_timestamp = strtotime($this->end_time);
+        if ($this->start_timestamp === false) {
+            $this->addError('start_time', 'start_time format is invalid');
+            return false;
+        }
+        if ($this->end_timestamp === false) {
+            $this->addError('end_time', 'end_time format is invalid');
+            return false;
+        }
+        if ($this->start_timestamp > $this->end_timestamp) {
+            $this->addError('end_time', 'end_time must be greater than or equal to start_time');
+            return false;
+        }
+
+        $period = Period::getInfoByPeriodNum($this->period_num);
+        if (!$period) {
+            $this->addError('period_num', Yii::t('ctx', 'invalidPcNo'));
+            return false;
+        }
+        if ((int)$period['IS_SENT'] === 1) {
+            $this->addError('period_num', Yii::t('ctx', 'orderHasBeenConnected'));
+            return false;
+        }
+
+        return $parentValidate;
+    }
+
+    /**
+     * Adjust orders by created_at range.
+     * @return array|null
+     */
+    public function periodAdjustRange()
+    {
+        if (!$this->validate()) {
+            return null;
+        }
+
+        $this->orders = Order::find()
+            ->select(['SN', 'PERIOD_NUM'])
+            ->where(['IS_DELETE' => 0])
+            ->andWhere(['>=', 'CREATED_AT', $this->start_timestamp])
+            ->andWhere(['<=', 'CREATED_AT', $this->end_timestamp])
+            ->orderBy(['CREATED_AT' => SORT_ASC, 'ID' => SORT_ASC])
+            ->asArray()
+            ->all();
+
+        $successOrderSnList = [];
+        $failedOrderSnList = [];
+        $skippedOrderSnList = [];
+        $skippedDetailMap = [];
+        $failedDetailMap = [];
+
+        foreach ($this->orders as $orderRow) {
+            $orderSn = $orderRow['SN'];
+            $skipReason = $this->getSkipReason($orderRow);
+            if ($skipReason !== null) {
+                $skippedOrderSnList[] = $orderSn;
+                $skippedDetailMap[$orderSn] = [
+                    'reason' => $skipReason,
+                    'current_period' => $orderRow['PERIOD_NUM'] ?? '',
+                    'target_period' => $this->period_num,
+                ];
+                continue;
+            }
+
+            $adjustForm = new OrderPeriodAdjustForm();
+            $adjustForm->scenario = 'orderPeriodAdjust';
+            $adjustForm->orderSn = $orderSn;
+            $adjustForm->modernPeriod = $this->period_num;
+            $adjustForm->operateUser = $this->operate_user;
+
+            $result = $adjustForm->periodAdjust();
+            if ($result === true) {
+                $successOrderSnList[] = $orderSn;
+            } else {
+                $failedOrderSnList[] = $orderSn;
+                $failedDetailMap[$orderSn] = $adjustForm->getErrors();
+            }
+        }
+
+        return [
+            'start_time' => $this->start_time,
+            'end_time' => $this->end_time,
+            'period_num' => $this->period_num,
+            'operate_user' => $this->operate_user,
+            'total_count' => count($this->orders),
+            'success_count' => count($successOrderSnList),
+            'failed_count' => count($failedOrderSnList),
+            'skipped_count' => count($skippedOrderSnList),
+            'success_order_sn_list' => $successOrderSnList,
+            'failed_order_sn_list' => $failedOrderSnList,
+            'skipped_order_sn_list' => $skippedOrderSnList,
+            'skipped_detail_map' => $skippedDetailMap,
+            'failed_detail_map' => $failedDetailMap,
+        ];
+    }
+
+    private function getSkipReason(array $orderRow): ?string
+    {
+        $currentPeriodNum = (string)($orderRow['PERIOD_NUM'] ?? '');
+        $targetPeriodNum = (string)$this->period_num;
+
+        if ($currentPeriodNum !== '' && $currentPeriodNum === $targetPeriodNum) {
+            return '订单期数与目标期数相同';
+        }
+
+        if ($currentPeriodNum === '') {
+            return '订单期数为空,无法判断是否可调整';
+        }
+
+        $currentPeriod = Period::getInfoByPeriodNum($currentPeriodNum);
+        if (!$currentPeriod) {
+            return Yii::t('ctx', 'invalidPcNo');
+        }
+
+        if ((int)$currentPeriod['IS_SENT'] === 1) {
+            return '已挂网,不可调整';
+        }
+
+        if ($currentPeriodNum === $targetPeriodNum) {
+            return '订单期数与目标期数相同';
+        }
+
+        $availablePeriodList = Period::getPeriodList(+(int)$currentPeriodNum - 1);
+        $availablePeriodNums = array_column($availablePeriodList, 'PERIOD_NUM');
+        if (!in_array($targetPeriodNum, $availablePeriodNums, false)) {
+            return '不在可调整期数范围内';
+        }
+
+        return null;
+    }
+}