shish 2 ay önce
ebeveyn
işleme
843c3f4b59
1 değiştirilmiş dosya ile 114 ekleme ve 12 silme
  1. 114 12
      common/components/PosterUtil.php

+ 114 - 12
common/components/PosterUtil.php

@@ -14,9 +14,14 @@ class PosterUtil
 
     const POSTER_W = 750;
 
-    /** 画布左右留白(各 20px) */
+    /** 画布左右、顶部留白(各 20px) */
     const CANVAS_PAD_X = 20;
 
+    const CANVAS_PAD_Y = 20;
+
+    /** 底部文案每行最多字数 */
+    const NAME_CHARS_PER_LINE = 13;
+
     /** 商品图显示宽度 = POSTER_W - 2×CANVAS_PAD_X */
     const SLOT_W = 710;
 
@@ -42,6 +47,16 @@ class PosterUtil
 
     const SCAN_HINT = '微信扫码下单';
 
+    const FOOTER_STAFF_Y = 32;
+
+    const FOOTER_TEXT_GAP = 12;
+
+    const FOOTER_PRICE_SIZE = 56;
+
+    const FOOTER_NAME_SIZE = 36;
+
+    const FOOTER_STAFF_SIZE = 26;
+
     /**
      * 海报标题过长时截断,避免压住底部文案区
      */
@@ -57,6 +72,93 @@ class PosterUtil
         return $text;
     }
 
+    /**
+     * 商品名拆成最多两行:首行 13 字;次行剩余,超过 13 字则省略
+     *
+     * @return array{0:string,1:string}
+     */
+    public static function splitPosterNameLines($name, $maxPerLine = self::NAME_CHARS_PER_LINE)
+    {
+        $name = trim((string)$name);
+        if ($name === '') {
+            return ['', ''];
+        }
+        $maxPerLine = (int)$maxPerLine;
+        if ($maxPerLine < 1) {
+            $maxPerLine = self::NAME_CHARS_PER_LINE;
+        }
+        $len = mb_strlen($name, 'UTF-8');
+        if ($len <= $maxPerLine) {
+            return [$name, ''];
+        }
+        $line1 = mb_substr($name, 0, $maxPerLine, 'UTF-8');
+        $rest = mb_substr($name, $maxPerLine, null, 'UTF-8');
+        $restLen = mb_strlen($rest, 'UTF-8');
+        if ($restLen <= $maxPerLine) {
+            return [$line1, $rest];
+        }
+        $line2 = mb_substr($rest, 0, $maxPerLine, 'UTF-8') . '…';
+
+        return [$line1, $line2];
+    }
+
+    /**
+     * 商品名水印文案(支持 \\n 两行),支数/副标题尽量接在末行
+     */
+    public static function buildPosterNameText($name, $flowerNum = 0, $subtitle = '')
+    {
+        list($line1, $line2) = self::splitPosterNameLines($name);
+        $suffix = '';
+        if ($flowerNum > 0) {
+            $suffix = ' · ' . (int)$flowerNum . '支';
+        } elseif (trim((string)$subtitle) !== '') {
+            $suffix = ' · ' . trim((string)$subtitle);
+        }
+        if ($suffix !== '') {
+            $target = $line2 !== '' ? 'line2' : 'line1';
+            if ($target === 'line1') {
+                $merged = $line1 . $suffix;
+                if (mb_strlen($merged, 'UTF-8') <= self::NAME_CHARS_PER_LINE) {
+                    $line1 = $merged;
+                } else {
+                    $line2 = self::truncateText($suffix, self::NAME_CHARS_PER_LINE);
+                }
+            } else {
+                $merged = $line2 . $suffix;
+                if (mb_strlen($merged, 'UTF-8') <= self::NAME_CHARS_PER_LINE) {
+                    $line2 = $merged;
+                } else {
+                    $line2 = self::truncateText($merged, self::NAME_CHARS_PER_LINE);
+                }
+            }
+        }
+        if ($line2 === '') {
+            return $line1;
+        }
+
+        return $line1 . "\n" . $line2;
+    }
+
+    /**
+     * 底部文案 y(g_sw,自下而上:推荐语 → 价格 → 商品名)
+     */
+    private static function footerTextYPositions($nameLineCount)
+    {
+        $staffY = self::FOOTER_STAFF_Y;
+        $staffH = (int)ceil(self::FOOTER_STAFF_SIZE * 1.3);
+        $priceY = $staffY + $staffH + self::FOOTER_TEXT_GAP;
+        $priceH = (int)ceil(self::FOOTER_PRICE_SIZE * 1.1);
+        $nameLineCount = max(1, min(2, (int)$nameLineCount));
+        $nameLineH = (int)ceil(self::FOOTER_NAME_SIZE * 1.15);
+        $nameY = $priceY + $priceH + self::FOOTER_TEXT_GAP + ($nameLineCount - 1) * $nameLineH;
+
+        return [
+            'staff' => $staffY,
+            'price' => $priceY,
+            'name' => $nameY,
+        ];
+    }
+
     /**
      * 按商品图比例计算主图区高度与画布总高(能多矮就多矮,仅保留温和下限)
      *
@@ -200,19 +302,16 @@ class PosterUtil
         $imgH = $layout['imgH'];
         $totalH = $layout['totalH'];
 
-        $displayName = self::truncateText($name, 14);
-        if ($flowerNum > 0 && $displayName !== '') {
-            $displayName .= ' · ' . $flowerNum . '支';
-        } elseif ($subtitle !== '' && $displayName !== '') {
-            $displayName .= ' · ' . self::truncateText($subtitle, 8);
-        }
+        $displayName = self::buildPosterNameText($name, $flowerNum, $subtitle);
+        $nameLineCount = ($displayName !== '' && strpos($displayName, "\n") !== false) ? 2 : 1;
+        $footerY = self::footerTextYPositions($nameLineCount);
 
         $coverWatermark = self::buildCoverWatermarkPath($cover, $layout['scaledH']);
         $qrWatermark = $miniCode . '?x-oss-process=image/resize,m_fill,h_' . self::QR_SIZE . ',w_' . self::QR_SIZE;
 
         $url = self::buildCanvasBaseUrl($prefix, $totalH);
         $url .= '/watermark,image_' . stringUtil::ossBase64($coverWatermark)
-            . ',g_nw,x_' . self::CANVAS_PAD_X . ',y_8';
+            . ',g_nw,x_' . self::CANVAS_PAD_X . ',y_' . self::CANVAS_PAD_Y;
         $url .= '/watermark,image_' . stringUtil::ossBase64($qrWatermark) . ',g_se,x_' . self::QR_PAD_RIGHT . ',y_' . self::QR_PAD_BOTTOM;
 
         $logoB64 = self::buildHyxhLogoWatermark($mainId);
@@ -220,12 +319,15 @@ class PosterUtil
             $url .= '/watermark,image_' . $logoB64 . ',g_se,x_210,y_48';
         }
 
+        $url .= '/watermark,text_' . stringUtil::ossBase64($staffText)
+            . ',g_sw,x_' . self::CANVAS_PAD_X . ',y_' . $footerY['staff']
+            . ',color_999999,type_' . self::FONT . ',size_' . self::FOOTER_STAFF_SIZE;
         $url .= '/watermark,text_' . stringUtil::ossBase64($priceText)
-            . ',g_sw,x_' . self::CANVAS_PAD_X . ',y_32,color_EE2C2C,type_' . self::FONT . ',size_56';
+            . ',g_sw,x_' . self::CANVAS_PAD_X . ',y_' . $footerY['price']
+            . ',color_EE2C2C,type_' . self::FONT . ',size_' . self::FOOTER_PRICE_SIZE;
         $url .= '/watermark,text_' . stringUtil::ossBase64($displayName)
-            . ',g_sw,x_' . self::CANVAS_PAD_X . ',y_102,color_333333,type_' . self::FONT . ',size_36';
-        $url .= '/watermark,text_' . stringUtil::ossBase64($staffText)
-            . ',g_sw,x_' . self::CANVAS_PAD_X . ',y_148,color_999999,type_' . self::FONT . ',size_26';
+            . ',g_sw,x_' . self::CANVAS_PAD_X . ',y_' . $footerY['name']
+            . ',color_333333,type_' . self::FONT . ',size_' . self::FOOTER_NAME_SIZE;
         $url .= '/watermark,text_' . stringUtil::ossBase64($scanHint)
             . ',g_south,x_' . self::qrCenterX() . ',y_' . self::scanHintYFromBottom()
             . ',color_888888,type_' . self::FONT . ',size_24';