shish 2 mesiacov pred
rodič
commit
044f006af4
1 zmenil súbory, kde vykonal 94 pridanie a 28 odobranie
  1. 94 28
      common/components/PosterUtil.php

+ 94 - 28
common/components/PosterUtil.php

@@ -48,7 +48,7 @@ class PosterUtil
 
     const MID_LINE_GAP = 6;
 
-    const MID_NAME_SIZE = 32;
+    const MID_NAME_SIZE = 38;
 
     const MID_REMARK_SIZE = 26;
 
@@ -60,14 +60,18 @@ class PosterUtil
 
     const QR_PAD_BOTTOM = 32;
 
-    /** 价格下方:长按识别提示(原二维码旁「微信扫码下单」已取消) */
+    /** 价格下方:灰色圆角条 + 文案(布局参考快团团) */
     const FOOTER_BUY_HINT = '长按识别小程序购买';
 
     const FOOTER_BUY_HINT_SIZE = 26;
 
-    const FOOTER_BUY_HINT_SHADOW = 55;
+    const FOOTER_BUY_HINT_BAR_W = 430;
 
-    const FOOTER_BUY_HINT_Y = 24;
+    const FOOTER_BUY_HINT_BAR_H = 56;
+
+    const FOOTER_BUY_HINT_BAR_COLOR = 'F2F2F2';
+
+    const FOOTER_BUY_HINT_TEXT_PAD_X = 28;
 
     const FOOTER_TEXT_GAP = 12;
 
@@ -82,11 +86,11 @@ class PosterUtil
         $options['shopTitle'] = $meta['shopTitle'];
         $options['shopLogo'] = $meta['shopLogo'];
         if (is_string($itemOrRemark)) {
-            $options['itemRemark'] = trim($itemOrRemark);
+            $options['itemRemark'] = self::filterPosterUtf8Text($itemOrRemark);
         } elseif ($itemOrRemark !== null) {
-            $options['itemRemark'] = trim($itemOrRemark->itemRemark ?? '');
+            $options['itemRemark'] = self::filterPosterUtf8Text($itemOrRemark->itemRemark ?? '');
         } else {
-            $options['itemRemark'] = trim($options['itemRemark'] ?? '');
+            $options['itemRemark'] = self::filterPosterUtf8Text($options['itemRemark'] ?? '');
         }
 
         return $options;
@@ -120,7 +124,7 @@ class PosterUtil
             }
         }
         $shopName = $shop ? trim((string)($shop->shopName ?? '')) : '';
-        $title = self::buildShopDisplayTitle($sjName, $shopName);
+        $title = self::filterPosterUtf8Text(self::buildShopDisplayTitle($sjName, $shopName));
         if (mb_strlen($title, 'UTF-8') > self::SHOP_TITLE_MAX_LEN) {
             $title = mb_substr($title, 0, self::SHOP_TITLE_MAX_LEN, 'UTF-8') . '…';
         }
@@ -141,12 +145,45 @@ class PosterUtil
         return imgUtil::normalizeOssObjectPath($avatar);
     }
 
-    public static function truncateText($text, $maxLen = 14)
+    /**
+     * 海报文案 UTF-8 清洗:剔除非法字节、替换符及常见乱码字符
+     */
+    public static function filterPosterUtf8Text($text)
     {
         $text = trim((string)$text);
         if ($text === '') {
             return '';
         }
+        if (function_exists('iconv')) {
+            $fixed = @iconv('UTF-8', 'UTF-8//IGNORE', $text);
+            if ($fixed !== false) {
+                $text = $fixed;
+            }
+        }
+        if (function_exists('mb_convert_encoding')) {
+            $converted = @mb_convert_encoding($text, 'UTF-8', 'UTF-8');
+            if (is_string($converted) && $converted !== '') {
+                $text = $converted;
+            }
+        }
+        $text = preg_replace('/[\x{FFFD}\x{FFF0}-\x{FFFF}]/u', '', $text);
+        $text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $text);
+        $text = preg_replace(
+            '/[^\x{4e00}-\x{9fff}a-zA-Z0-9\s,。、!?:;…·~%()【】\[\]\/\-_+|《》「」\'\"\.#&@\x{3000}-\x{303F}\x{FF01}-\x{FF5E}]/u',
+            '',
+            $text
+        );
+        $text = trim(preg_replace('/\s+/u', ' ', $text));
+
+        return $text;
+    }
+
+    public static function truncateText($text, $maxLen = 14)
+    {
+        $text = self::filterPosterUtf8Text($text);
+        if ($text === '') {
+            return '';
+        }
         if (mb_strlen($text, 'UTF-8') > $maxLen) {
             return mb_substr($text, 0, $maxLen, 'UTF-8') . '…';
         }
@@ -159,7 +196,7 @@ class PosterUtil
      */
     public static function splitPosterTextLines($text, $maxPerLine = self::TEXT_CHARS_PER_LINE)
     {
-        $text = trim((string)$text);
+        $text = self::filterPosterUtf8Text($text);
         if ($text === '') {
             return ['', ''];
         }
@@ -202,6 +239,8 @@ class PosterUtil
      */
     public static function buildPosterNameText($name, $flowerNum = 0, $subtitle = '')
     {
+        $name = self::filterPosterUtf8Text($name);
+        $subtitle = self::filterPosterUtf8Text($subtitle);
         list($line1, $line2) = self::splitPosterTextLines($name);
         $suffix = '';
         if ($flowerNum > 0) {
@@ -236,7 +275,7 @@ class PosterUtil
      */
     public static function buildPosterRemarkText($itemRemark)
     {
-        $itemRemark = trim((string)$itemRemark);
+        $itemRemark = self::filterPosterUtf8Text($itemRemark);
         if ($itemRemark === '') {
             return '';
         }
@@ -285,18 +324,44 @@ class PosterUtil
         return self::CANVAS_PAD_Y + self::SHOP_LOGO_SIZE + self::HEADER_BOTTOM_GAP;
     }
 
-    private static function footerTextYPositions()
+    /**
+     * 底部左侧(价格+灰条)与右侧太阳码垂直居中对齐
+     *
+     * @return array{buyHintBarY:int,buyHintTextY:int,buyHintTextX:int,priceY:int,qrY:int}
+     */
+    private static function calcFooterLayout()
     {
-        $buyHintH = (int)ceil(self::FOOTER_BUY_HINT_SIZE * 1.25);
-        $buyHintY = self::FOOTER_BUY_HINT_Y;
-        $priceY = $buyHintY + $buyHintH + self::FOOTER_TEXT_GAP;
+        $qrCenterFromBottom = self::QR_PAD_BOTTOM + (int)(self::QR_SIZE / 2);
+        $hintBarH = self::FOOTER_BUY_HINT_BAR_H;
+        $priceH = (int)ceil(self::FOOTER_PRICE_SIZE * 1.3);
+        $gap = self::FOOTER_TEXT_GAP;
+        $blockH = $priceH + $gap + $hintBarH;
+        $buyHintBarY = max(16, $qrCenterFromBottom - (int)($blockH / 2));
+        $priceY = $buyHintBarY + $hintBarH + $gap;
+        $textLineH = (int)ceil(self::FOOTER_BUY_HINT_SIZE * 1.2);
+        $buyHintTextY = $buyHintBarY + (int)(($hintBarH - $textLineH) / 2);
+        $qrY = max(16, $qrCenterFromBottom - (int)(self::QR_SIZE / 2));
 
         return [
-            'buyHint' => $buyHintY,
-            'price' => $priceY,
+            'buyHintBarY' => $buyHintBarY,
+            'buyHintTextY' => $buyHintTextY,
+            'buyHintTextX' => self::CANVAS_PAD_X + self::FOOTER_BUY_HINT_TEXT_PAD_X,
+            'priceY' => $priceY,
+            'qrY' => $qrY,
         ];
     }
 
+    /** OSS 生成灰色圆角底条(无需单独上传底图) */
+    private static function buildBuyHintBarWatermarkPath()
+    {
+        $w = self::FOOTER_BUY_HINT_BAR_W;
+        $h = self::FOOTER_BUY_HINT_BAR_H;
+        $r = (int)($h / 2);
+
+        return self::BLANK_CANVAS . '?x-oss-process=image/resize,m_pad,w_' . $w . ',h_' . $h
+            . ',color_' . self::FOOTER_BUY_HINT_BAR_COLOR . '/rounded-corners,r_' . $r;
+    }
+
     /**
      * @return array{imgH:int,totalH:int,scaledH:int,posterW:int,coverY:int,nameY:int,remarkY:int}
      */
@@ -419,12 +484,12 @@ class PosterUtil
         $cover = $options['cover'] ?? '';
         $miniCode = $options['miniCode'] ?? '';
         $priceText = $options['priceText'] ?? '';
-        $name = $options['name'] ?? '';
+        $name = self::filterPosterUtf8Text($options['name'] ?? '');
         $mainId = (int)($options['mainId'] ?? 0);
         $flowerNum = (int)($options['flowerNum'] ?? 0);
-        $subtitle = trim((string)($options['subtitle'] ?? ''));
-        $itemRemark = trim((string)($options['itemRemark'] ?? ''));
-        $shopTitle = trim((string)($options['shopTitle'] ?? ''));
+        $subtitle = self::filterPosterUtf8Text($options['subtitle'] ?? '');
+        $itemRemark = self::filterPosterUtf8Text($options['itemRemark'] ?? '');
+        $shopTitle = self::filterPosterUtf8Text($options['shopTitle'] ?? '');
         $shopLogo = trim((string)($options['shopLogo'] ?? ''));
 
         $coverW = (int)($options['coverWidth'] ?? 0);
@@ -441,7 +506,7 @@ class PosterUtil
         ]);
         $displayName = self::buildPosterNameText($name, $flowerNum, $subtitle);
         $displayRemark = self::buildPosterRemarkText($itemRemark);
-        $footerY = self::footerTextYPositions();
+        $footer = self::calcFooterLayout();
 
         $coverWatermark = self::buildCoverWatermarkPath($cover, $layout['scaledH']);
         $qrWatermark = $miniCode . '?x-oss-process=image/resize,m_fill,h_' . self::QR_SIZE . ',w_' . self::QR_SIZE;
@@ -474,20 +539,21 @@ class PosterUtil
         }
 
         $url .= '/watermark,image_' . stringUtil::ossBase64($qrWatermark)
-            . ',g_se,x_' . self::QR_PAD_RIGHT . ',y_' . self::QR_PAD_BOTTOM;
+            . ',g_se,x_' . self::QR_PAD_RIGHT . ',y_' . $footer['qrY'];
 
         $logoB64 = self::buildHyxhLogoWatermark($mainId);
         if ($logoB64 !== '') {
             $url .= '/watermark,image_' . $logoB64 . ',g_se,x_210,y_48';
         }
 
+        $url .= '/watermark,image_' . stringUtil::ossBase64(self::buildBuyHintBarWatermarkPath())
+            . ',g_sw,x_' . self::CANVAS_PAD_X . ',y_' . $footer['buyHintBarY'];
+        $url .= '/watermark,text_' . stringUtil::ossBase64(self::FOOTER_BUY_HINT)
+            . ',g_sw,x_' . $footer['buyHintTextX'] . ',y_' . $footer['buyHintTextY']
+            . ',color_666666,type_' . self::FONT . ',size_' . self::FOOTER_BUY_HINT_SIZE;
         $url .= '/watermark,text_' . stringUtil::ossBase64($priceText)
-            . ',g_sw,x_' . self::CANVAS_PAD_X . ',y_' . $footerY['price']
+            . ',g_sw,x_' . self::CANVAS_PAD_X . ',y_' . $footer['priceY']
             . ',color_EE2C2C,type_' . self::FONT . ',size_' . self::FOOTER_PRICE_SIZE;
-        $url .= '/watermark,text_' . stringUtil::ossBase64(self::FOOTER_BUY_HINT)
-            . ',g_sw,x_' . self::CANVAS_PAD_X . ',y_' . $footerY['buyHint']
-            . ',color_888888,type_' . self::FONT . ',size_' . self::FOOTER_BUY_HINT_SIZE
-            . ',shadow_' . self::FOOTER_BUY_HINT_SHADOW;
 
         return $url;
     }