diff --git a/public/assets/js/backend/fc/category.js b/public/assets/js/backend/fc/category.js
index 6424fe8..b0f1bcf 100644
--- a/public/assets/js/backend/fc/category.js
+++ b/public/assets/js/backend/fc/category.js
@@ -1,5 +1,14 @@
define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], function ($, undefined, Backend, Table, Form, Format) {
+ /**
+ * categories 目录下的专属图标键名列表
+ */
+ var CATEGORY_DIR_KEYS = [
+ 'cat-fruit', 'cat-poultry', 'cat-veg', 'cat-job',
+ 'fruit-apple', 'fruit-grape', 'fruit-kiwi', 'fruit-orange', 'fruit-passion', 'fruit-peach', 'fruit-strawberry', 'fruit-watermelon',
+ 'veg-cabbage', 'veg-chili', 'veg-green-cabbage', 'veg-greens', 'veg-purple-cabbage', 'veg-radish', 'veg-tomato'
+ ];
+
/**
* 解析分类图标/实拍图地址
* @param {string} iconKey 标识或路径
@@ -12,13 +21,14 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
return image;
}
if (!iconKey) {
- iconKey = 'cat-veg';
+ iconKey = 'cat-job';
}
if (/^(https?:\/\/|\/uploads\/|\/static\/)/i.test(iconKey)) {
return iconKey;
}
+ var baseKey = iconKey.replace(/\.(png|jpg|jpeg|gif|svg)$/i, '');
var file = (iconKey.indexOf('.') !== -1) ? iconKey : iconKey + '.png';
- if (iconKey.indexOf('sub-') === 0 || iconKey === 'cat-veg' || iconKey === 'cat-fruit') {
+ if (iconKey.indexOf('sub-') === 0 || CATEGORY_DIR_KEYS.indexOf(baseKey) !== -1 || /^(veg-|fruit-|cat-veg|cat-fruit|cat-job|cat-poultry)/i.test(baseKey)) {
return '/static/categories/' + file + '?v=2';
}
return '/static/icons/' + file + '?v=2';
@@ -63,10 +73,10 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
formatter: Format.serial
},
- // 1. 实拍图预览(大图圆角卡,object-fit:cover 保证「看图识物」效果)
+ // 1. 图标/实拍图预览(大图圆角卡,object-fit:cover)
{
field: 'image',
- title: '实拍图',
+ title: '图标/图片',
operate: false,
align: 'center',
width: '90px',
@@ -74,10 +84,10 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
var url = resolveIconUrl(row.icon_key, row.id, row.image);
var color = row.color || '#00E070';
return '
' +
- '
' +
- '

' +
+ '
' +
+ '

' +
'
' +
- '
' +
+ '
' +
'
';
}
},
@@ -224,24 +234,55 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
var $previewBadge = $('#preview-badge');
var $previewIcon = $('#preview-icon');
var $previewName = $('#preview-name');
+ var $previewKind = $('#preview-kind');
var $currentIconImg = $('#current-icon-img');
+ var $currentIconLabel = $('#current-icon-label');
var $iconModal = $('#fc-icon-modal');
+ var $iconSearch = $('#fc-icon-search');
+ var $iconTabs = $('.fc-icon-tab-btn');
- // 刷新界面预览元素(纯视觉图形,无死板业务文字)
+ // 刷新界面预览元素
function updateLivePreview() {
var name = $nameInput.val() || '未命名分类';
- var key = $iconInput.val() || 'cat-veg';
+ var key = $iconInput.val() || 'cat-job';
var color = $colorInput.val() || '#00E070';
var iconUrl = resolveIconUrl(key);
+ var kind = $('input[name="row[kind]"]:checked').val() || 'general';
+
+ var $matchedChip = $('.fc-icon-chip[data-key="' + key + '"]');
+ if ($matchedChip.length) {
+ var chipImg = $matchedChip.find('img').attr('src');
+ if (chipImg) {
+ iconUrl = chipImg;
+ }
+ }
if ($previewName.length) $previewName.text(name);
if ($previewIcon.length) $previewIcon.attr('src', iconUrl);
if ($currentIconImg.length) $currentIconImg.attr('src', iconUrl);
+ if ($currentIconLabel.length) {
+ if ($matchedChip.length) {
+ $currentIconLabel.text($matchedChip.data('name') || key);
+ } else if (/^(\/uploads\/|https?:\/\/)/i.test(key)) {
+ $currentIconLabel.text('自定义上传图片');
+ } else {
+ $currentIconLabel.text(key);
+ }
+ }
+
+ if ($previewKind.length) {
+ if (kind === 'farm') {
+ $previewKind.text('农产品类目').removeClass('general').addClass('farm');
+ } else {
+ $previewKind.text('通用便民类目').removeClass('farm').addClass('general');
+ }
+ }
if ($previewBadge.length) {
$previewBadge.css({
- 'background': color + '22',
- 'border-color': color + '55'
+ 'background': color + '1a',
+ 'border-color': color + '4d',
+ 'box-shadow': '0 6px 16px ' + color + '26'
});
}
if ($colorAddon.length) $colorAddon.css('background', color);
@@ -251,11 +292,21 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
// 确保初始状态完全隐藏
$iconModal.removeClass('open').attr('style', 'display:none !important;');
- // 1. 打开纯插画挑选面板(只有用户主动点击按钮才打开)
+ // 1. 打开插画挑选面板
$(document).on('click', '#btn-open-icon-picker', function (e) {
e.preventDefault();
e.stopPropagation();
$iconModal.removeAttr('style').addClass('open').fadeIn(150);
+ // 打开时定位高亮当前图标
+ var curKey = $iconInput.val();
+ var $activeChip = $('.fc-icon-chip[data-key="' + curKey + '"]');
+ $('.fc-icon-chip').removeClass('active');
+ if ($activeChip.length) {
+ $activeChip.addClass('active');
+ }
+ if ($iconSearch.length) {
+ $iconSearch.focus();
+ }
});
// 关闭插画挑选面板
@@ -270,17 +321,81 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
closeIconModal();
});
- // 2. 点选插画卡片(只设图标,绝不改写用户分类名称)
+ // 2. 点选图标卡片
$(document).on('click', '.fc-icon-chip', function (e) {
e.preventDefault();
var key = $(this).data('key');
+ var iconName = $(this).data('name') || '';
+ var iconColor = $(this).data('color') || '';
+
$iconInput.val(key).trigger('change');
$('.fc-icon-chip').removeClass('active');
$(this).addClass('active');
+
+ // 若名称尚未输入,自动以图标名称填充
+ if ($nameInput.length && !$nameInput.val().trim() && iconName) {
+ $nameInput.val(iconName).trigger('input');
+ }
+
+ // 若当前主题色为默认春绿且图标带有推荐色,可智能联动
+ if (iconColor && ($colorInput.val() === '#00E070' || !$colorInput.val())) {
+ $colorInput.val(iconColor).trigger('change');
+ }
+
updateLivePreview();
closeIconModal();
});
+ // 2.1 图标分类 Tab 切换
+ $(document).on('click', '.fc-icon-tab-btn', function (e) {
+ e.preventDefault();
+ $('.fc-icon-tab-btn').removeClass('active');
+ $(this).addClass('active');
+ var targetGroup = $(this).data('group');
+ filterChips();
+ });
+
+ // 2.2 图标搜索过滤
+ if ($iconSearch.length) {
+ $iconSearch.on('input', function () {
+ filterChips();
+ });
+ }
+
+ // 过滤卡片执行逻辑
+ function filterChips() {
+ var group = $('.fc-icon-tab-btn.active').data('group') || 'all';
+ var keyword = ($iconSearch.val() || '').trim().toLowerCase();
+ var visibleCount = 0;
+
+ $('.fc-icon-chip').each(function () {
+ var chipGroup = $(this).data('group') || '';
+ var chipKey = ($(this).data('key') || '').toLowerCase();
+ var chipName = ($(this).data('name') || '').toLowerCase();
+
+ var matchGroup = (group === 'all' || chipGroup === group);
+ var matchKeyword = (!keyword || chipName.indexOf(keyword) !== -1 || chipKey.indexOf(keyword) !== -1);
+
+ if (matchGroup && matchKeyword) {
+ $(this).show();
+ visibleCount++;
+ } else {
+ $(this).hide();
+ }
+ });
+
+ var $emptyTip = $('#fc-icon-empty');
+ if (visibleCount === 0) {
+ if (!$emptyTip.length) {
+ $('.fc-modal-grid').append('
未找到匹配的预设图标,您可直接使用「上传自定义图标」
');
+ } else {
+ $emptyTip.show();
+ }
+ } else if ($emptyTip.length) {
+ $emptyTip.hide();
+ }
+ }
+
// 3. 自定义上传图片监听
$iconInput.on('input change', function () {
updateLivePreview();
@@ -291,6 +406,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
updateLivePreview();
});
+ // 4.1 监听分类类型单选变更
+ $('input[name="row[kind]"]').on('change', function () {
+ updateLivePreview();
+ });
+
// 5. 监听颜色输入与推荐调色板点选
$colorInput.on('input change', function () {
updateLivePreview();
@@ -308,7 +428,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
});
// 初始化回显
- var initKey = $iconInput.val() || 'cat-veg';
+ var initKey = $iconInput.val() || 'cat-service';
$('.fc-icon-chip[data-key="' + initKey + '"]').addClass('active');
updateLivePreview();
}
diff --git a/public/static/categories/cat-job.png b/public/static/categories/cat-job.png
new file mode 100644
index 0000000..1479d09
Binary files /dev/null and b/public/static/categories/cat-job.png differ
diff --git a/public/static/categories/cat-poultry.png b/public/static/categories/cat-poultry.png
new file mode 100644
index 0000000..01a86e7
Binary files /dev/null and b/public/static/categories/cat-poultry.png differ
diff --git a/public/static/icons/cat-poultry.png b/public/static/icons/cat-poultry.png
new file mode 100644
index 0000000..01a86e7
Binary files /dev/null and b/public/static/icons/cat-poultry.png differ
diff --git a/public/uploads/20261028/0391292554cf8efd457c44caedf67867.jpg b/public/uploads/20261028/0391292554cf8efd457c44caedf67867.jpg
new file mode 100644
index 0000000..d8136e6
Binary files /dev/null and b/public/uploads/20261028/0391292554cf8efd457c44caedf67867.jpg differ
diff --git a/public/uploads/20261028/0c913f9d7d72a10c0c598c36eea89b14.jpg b/public/uploads/20261028/0c913f9d7d72a10c0c598c36eea89b14.jpg
new file mode 100644
index 0000000..c45d011
Binary files /dev/null and b/public/uploads/20261028/0c913f9d7d72a10c0c598c36eea89b14.jpg differ
diff --git a/public/uploads/20261028/3fb1eee6654e4d5e7c1b5d3189643fae.jpg b/public/uploads/20261028/3fb1eee6654e4d5e7c1b5d3189643fae.jpg
new file mode 100644
index 0000000..49cdcdc
Binary files /dev/null and b/public/uploads/20261028/3fb1eee6654e4d5e7c1b5d3189643fae.jpg differ
diff --git a/public/uploads/20261028/49b39ce93043ec8a6f24bcef9bc45f96.jpg b/public/uploads/20261028/49b39ce93043ec8a6f24bcef9bc45f96.jpg
new file mode 100644
index 0000000..ad2a266
Binary files /dev/null and b/public/uploads/20261028/49b39ce93043ec8a6f24bcef9bc45f96.jpg differ
diff --git a/public/uploads/20261028/4e4f911ac45e9dc5471597a8ff7862f7.jpg b/public/uploads/20261028/4e4f911ac45e9dc5471597a8ff7862f7.jpg
new file mode 100644
index 0000000..3d28050
Binary files /dev/null and b/public/uploads/20261028/4e4f911ac45e9dc5471597a8ff7862f7.jpg differ
diff --git a/public/uploads/20261028/65d91dba8121f8b49356f356aa6a3b28.jpg b/public/uploads/20261028/65d91dba8121f8b49356f356aa6a3b28.jpg
new file mode 100644
index 0000000..c73e503
Binary files /dev/null and b/public/uploads/20261028/65d91dba8121f8b49356f356aa6a3b28.jpg differ
diff --git a/public/uploads/20261028/69b65e455081d5aa8834b48bd879009d.jpg b/public/uploads/20261028/69b65e455081d5aa8834b48bd879009d.jpg
new file mode 100644
index 0000000..6117332
Binary files /dev/null and b/public/uploads/20261028/69b65e455081d5aa8834b48bd879009d.jpg differ
diff --git a/public/uploads/20261028/70cafc924bc7e4018bfefbe883e27c78.jpg b/public/uploads/20261028/70cafc924bc7e4018bfefbe883e27c78.jpg
new file mode 100644
index 0000000..f18361f
Binary files /dev/null and b/public/uploads/20261028/70cafc924bc7e4018bfefbe883e27c78.jpg differ
diff --git a/public/uploads/20261028/75dd5971bdfa99d64a62c5b1e72cac11.jpg b/public/uploads/20261028/75dd5971bdfa99d64a62c5b1e72cac11.jpg
new file mode 100644
index 0000000..fd43af1
Binary files /dev/null and b/public/uploads/20261028/75dd5971bdfa99d64a62c5b1e72cac11.jpg differ
diff --git a/public/uploads/20261028/915423715a937ad7ba4b737690c963d2.jpg b/public/uploads/20261028/915423715a937ad7ba4b737690c963d2.jpg
new file mode 100644
index 0000000..c552e93
Binary files /dev/null and b/public/uploads/20261028/915423715a937ad7ba4b737690c963d2.jpg differ
diff --git a/public/uploads/20261028/99c32f9508e3429ea02a4ce1c34f9df2.jpg b/public/uploads/20261028/99c32f9508e3429ea02a4ce1c34f9df2.jpg
new file mode 100644
index 0000000..814a6a6
Binary files /dev/null and b/public/uploads/20261028/99c32f9508e3429ea02a4ce1c34f9df2.jpg differ
diff --git a/public/uploads/20261028/bb63e21dae84dc15056a0b50753577d9.jpg b/public/uploads/20261028/bb63e21dae84dc15056a0b50753577d9.jpg
new file mode 100644
index 0000000..38eb584
Binary files /dev/null and b/public/uploads/20261028/bb63e21dae84dc15056a0b50753577d9.jpg differ
diff --git a/public/uploads/20261028/cafbe884f6616622dcdd5650bbbce3b3.jpg b/public/uploads/20261028/cafbe884f6616622dcdd5650bbbce3b3.jpg
new file mode 100644
index 0000000..b75e2f4
Binary files /dev/null and b/public/uploads/20261028/cafbe884f6616622dcdd5650bbbce3b3.jpg differ
diff --git a/public/uploads/20261028/cb17be7476ce1c59e14944f9fe60438c.jpg b/public/uploads/20261028/cb17be7476ce1c59e14944f9fe60438c.jpg
new file mode 100644
index 0000000..eacf291
Binary files /dev/null and b/public/uploads/20261028/cb17be7476ce1c59e14944f9fe60438c.jpg differ
diff --git a/public/uploads/20261028/d0cc33f85a8580be2f526eb360448867.jpg b/public/uploads/20261028/d0cc33f85a8580be2f526eb360448867.jpg
new file mode 100644
index 0000000..0f2fc30
Binary files /dev/null and b/public/uploads/20261028/d0cc33f85a8580be2f526eb360448867.jpg differ
diff --git a/public/uploads/20261028/d2a3226fd6ffa12d24db06c9342a9118.jpg b/public/uploads/20261028/d2a3226fd6ffa12d24db06c9342a9118.jpg
new file mode 100644
index 0000000..de4f4df
Binary files /dev/null and b/public/uploads/20261028/d2a3226fd6ffa12d24db06c9342a9118.jpg differ
diff --git a/public/uploads/20261028/d8b5420c12f7921e325050be088a011f.jpg b/public/uploads/20261028/d8b5420c12f7921e325050be088a011f.jpg
new file mode 100644
index 0000000..3f9a352
Binary files /dev/null and b/public/uploads/20261028/d8b5420c12f7921e325050be088a011f.jpg differ
diff --git a/public/uploads/20261028/e71ba37749da829c50d098168dbb4fa2.jpg b/public/uploads/20261028/e71ba37749da829c50d098168dbb4fa2.jpg
new file mode 100644
index 0000000..22e8436
Binary files /dev/null and b/public/uploads/20261028/e71ba37749da829c50d098168dbb4fa2.jpg differ
diff --git a/public/uploads/20261028/f1a4f3c123c81aea2f0fd24c8688d988.jpg b/public/uploads/20261028/f1a4f3c123c81aea2f0fd24c8688d988.jpg
new file mode 100644
index 0000000..ecc0506
Binary files /dev/null and b/public/uploads/20261028/f1a4f3c123c81aea2f0fd24c8688d988.jpg differ
diff --git a/public/uploads/20261028/f5e2927ba040dd1b917b6fbd6b053458.jpg b/public/uploads/20261028/f5e2927ba040dd1b917b6fbd6b053458.jpg
new file mode 100644
index 0000000..c35a760
Binary files /dev/null and b/public/uploads/20261028/f5e2927ba040dd1b917b6fbd6b053458.jpg differ
diff --git a/tools/add_more_fruits.php b/tools/add_more_fruits.php
new file mode 100644
index 0000000..c1dfff5
--- /dev/null
+++ b/tools/add_more_fruits.php
@@ -0,0 +1,112 @@
+ '芒果',
+ 'filename' => 'fruit-mango.jpg',
+ 'url' => 'https://images.unsplash.com/photo-1553279768-865429fa0078?w=500&q=80&auto=format&fit=crop',
+ ],
+ [
+ 'name' => '香蕉',
+ 'filename' => 'fruit-banana.jpg',
+ 'url' => 'https://images.unsplash.com/photo-1571771894821-ce9b6c11b08e?w=500&q=80&auto=format&fit=crop',
+ ],
+ [
+ 'name' => '蓝莓',
+ 'filename' => 'fruit-blueberry.jpg',
+ 'url' => 'https://images.unsplash.com/photo-1498557850523-fd3d118b962e?w=500&q=80&auto=format&fit=crop',
+ ],
+ [
+ 'name' => '车厘子',
+ 'filename' => 'fruit-cherry.jpg',
+ 'url' => 'https://images.unsplash.com/photo-1528825871115-3581a5387919?w=500&q=80&auto=format&fit=crop',
+ ],
+ [
+ 'name' => '哈密瓜',
+ 'filename' => 'fruit-melon.jpg',
+ 'url' => 'https://images.unsplash.com/photo-1571575173700-afb9492e8c50?w=500&q=80&auto=format&fit=crop',
+ ],
+ [
+ 'name' => '火龙果',
+ 'filename' => 'fruit-dragon.jpg',
+ 'url' => 'https://images.unsplash.com/photo-1527324688151-0e627063f2b1?w=500&q=80&auto=format&fit=crop',
+ ],
+ [
+ 'name' => '柠檬',
+ 'filename' => 'fruit-lemon.jpg',
+ 'url' => 'https://images.unsplash.com/photo-1533089860892-a7c6f0a88666?w=500&q=80&auto=format&fit=crop',
+ ],
+ [
+ 'name' => '荔枝',
+ 'filename' => 'fruit-lychee.jpg',
+ 'url' => 'https://images.unsplash.com/photo-1596386461350-326ccb383e9f?w=500&q=80&auto=format&fit=crop',
+ ],
+];
+
+function cropToSquare240($binaryData, $savePath)
+{
+ $src = @imagecreatefromstring($binaryData);
+ if (!$src) return false;
+ $srcW = imagesx($src);
+ $srcH = imagesy($src);
+ $cropSize = min($srcW, $srcH);
+ $cropX = (int)(($srcW - $cropSize) / 2);
+ $cropY = (int)(($srcH - $cropSize) / 2);
+ $dst = imagecreatetruecolor(240, 240);
+ imagecopyresampled($dst, $src, 0, 0, $cropX, $cropY, 240, 240, $cropSize, $cropSize);
+ $res = imagejpeg($dst, $savePath, 82);
+ imagedestroy($src);
+ imagedestroy($dst);
+ return $res;
+}
+
+echo "=== [1/3] 下载真实水果实拍照并裁剪为 240x240 ===\n";
+$downloaded = [];
+foreach ($fruits as $f) {
+ echo "正在下载 [{$f['name']}] 实拍图: {$f['url']} ...\n";
+ $opts = [
+ 'http' => [
+ 'method' => 'GET',
+ 'header' => "User-Agent: Mozilla/5.0\r\n",
+ 'timeout' => 15,
+ ]
+ ];
+ $ctx = stream_context_create($opts);
+ $raw = @file_get_contents($f['url'], false, $ctx);
+
+ if (!$raw) {
+ // 尝试备用图
+ echo " ✗ 下载超时或失败: {$f['name']}\n";
+ continue;
+ }
+
+ $md5Name = md5($raw . $f['name']) . '.jpg';
+ $savePath = $targetDir . '/' . $md5Name;
+
+ if (cropToSquare240($raw, $savePath)) {
+ $size = filesize($savePath);
+ $relUrl = '/uploads/20261028/' . $md5Name;
+ echo " ✓ 生成成功: {$f['name']} -> {$relUrl} (" . round($size / 1024, 2) . " KB)\n";
+ $downloaded[] = [
+ 'name' => $f['name'],
+ 'filename' => $f['filename'],
+ 'local' => $savePath,
+ 'rel_url' => $relUrl,
+ 'size' => $size,
+ 'sha1' => sha1_file($savePath),
+ ];
+ }
+}
+
+// 写入临时 json 供 python 脚本后续同步
+file_put_contents(__DIR__ . '/more_fruits_result.json', json_encode($downloaded, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
+echo "\n已准备就绪 " . count($downloaded) . " 个水果二级分类图片!\n";
diff --git a/tools/clean_wrong_images.py b/tools/clean_wrong_images.py
new file mode 100644
index 0000000..ea23b4b
--- /dev/null
+++ b/tools/clean_wrong_images.py
@@ -0,0 +1,132 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+清理前一批错误的二级分类卡通图片(本地与线上同步清理):
+1. 清理 public/uploads/20261028/*.png (本地与线上)
+2. 清理 public/static/categories/job-*.png 和 poultry-*.png (本地与线上)
+3. 清理 uniapp/static/categories/job-*.png 和 poultry-*.png (本地)
+4. 清理数据库 fa_attachment 中 url 结尾为 .png 的 20261028 记录 (本地与线上)
+"""
+
+import os
+import sys
+import io
+import paramiko
+import pymysql
+
+sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
+
+LOCAL_ROOT = r"D:\Softwares\phpstudy_pro\WWW\fenleixinxi"
+REMOTE_HOST = "8.137.181.63"
+REMOTE_PORT = 22
+REMOTE_USER = "root"
+REMOTE_PASS = "liyanxia@6688"
+REMOTE_ROOT = "/www/wwwroot/fenleixinxi.gzyakj.com.cn"
+DB_USER = "fenleixinxi"
+DB_PASS = "fenleixinxi"
+DB_NAME = "fenleixinxi"
+
+LOCAL_DB_HOST = "127.0.0.1"
+LOCAL_DB_PORT = 3307
+
+WRONG_CAT_PNGS = [
+ 'job-tech.png', 'job-catering.png', 'job-clean.png', 'job-rider.png',
+ 'job-sales.png', 'job-driver.png', 'job-edu.png', 'job-parttime.png',
+ 'poultry-egg.png', 'poultry-chicken.png', 'poultry-pork.png',
+ 'poultry-beef.png', 'poultry-fish.png'
+]
+
+def ssh_exec(client, cmd):
+ stdin, stdout, stderr = client.exec_command(cmd)
+ out = stdout.read().decode("utf-8", errors="replace").strip()
+ err = stderr.read().decode("utf-8", errors="replace").strip()
+ return out, err
+
+def main():
+ print("=== [1/4] 清理本地错误的图片文件 ===")
+
+ # 1. 清理 public/uploads/20261028/*.png
+ local_up_dir = os.path.join(LOCAL_ROOT, "public", "uploads", "20261028")
+ del_up_cnt = 0
+ if os.path.exists(local_up_dir):
+ for f in os.listdir(local_up_dir):
+ if f.endswith('.png'):
+ p = os.path.join(local_up_dir, f)
+ os.remove(p)
+ del_up_cnt += 1
+ print(f" ✓ 已清理本地 public/uploads/20261028/*.png 错误图片: {del_up_cnt} 个")
+
+ # 2. 清理 public/static/categories/ 下的 13 个卡通 png
+ del_static_cnt = 0
+ for f in WRONG_CAT_PNGS:
+ p = os.path.join(LOCAL_ROOT, "public", "static", "categories", f)
+ if os.path.exists(p):
+ os.remove(p)
+ del_static_cnt += 1
+ print(f" ✓ 已清理本地 public/static/categories/ 下错误卡通图片: {del_static_cnt} 个")
+
+ # 3. 清理 uniapp/static/categories/ 下的 13 个卡通 png
+ del_uni_cnt = 0
+ for f in WRONG_CAT_PNGS:
+ p = os.path.join(LOCAL_ROOT, "uniapp", "static", "categories", f)
+ if os.path.exists(p):
+ os.remove(p)
+ del_uni_cnt += 1
+ print(f" ✓ 已清理本地 uniapp/static/categories/ 下错误卡通图片: {del_uni_cnt} 个")
+
+ print("\n=== [2/4] 清理本地数据库中残留的 png 附件记录 ===")
+ local_conn = pymysql.connect(
+ host=LOCAL_DB_HOST,
+ port=LOCAL_DB_PORT,
+ user=DB_USER,
+ password=DB_PASS,
+ database=DB_NAME,
+ charset="utf8mb4",
+ autocommit=True
+ )
+ with local_conn.cursor() as cur:
+ cur.execute("DELETE FROM fa_attachment WHERE url LIKE '/uploads/20261028/%.png';")
+ print(f" ✓ 已删除本地 fa_attachment 中错误 png 记录: {cur.rowcount} 条")
+ local_conn.close()
+
+ print("\n=== [3/4] 连接线上服务器并清理远程错误文件 ===")
+ ssh = paramiko.SSHClient()
+ ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ ssh.connect(REMOTE_HOST, port=REMOTE_PORT, username=REMOTE_USER, password=REMOTE_PASS, timeout=30)
+ print("✓ 远程 SSH 连接成功")
+
+ # 清理线上 uploads/20261028/*.png
+ cmd1 = f"rm -f {REMOTE_ROOT}/public/uploads/20261028/*.png"
+ ssh_exec(ssh, cmd1)
+ print(" ✓ 线上 /public/uploads/20261028/*.png 已清理")
+
+ # 清理线上 static/categories/*.png 错误卡通图片
+ for f in WRONG_CAT_PNGS:
+ cmd2 = f"rm -f {REMOTE_ROOT}/public/static/categories/{f}"
+ ssh_exec(ssh, cmd2)
+ print(f" ✓ 线上 /public/static/categories/ 下 13 个卡通图片已清理")
+
+ print("\n=== [4/4] 清理线上数据库中残留的 png 附件记录并清缓存 ===")
+ sql_clean = "DELETE FROM fa_attachment WHERE url LIKE '/uploads/20261028/%.png';"
+ cmd_sql = f'mysql -u{DB_USER} -p{DB_PASS} {DB_NAME} -e "{sql_clean}" 2>/dev/null'
+ ssh_exec(ssh, cmd_sql)
+ print(" ✓ 线上数据库 fa_attachment 中错误 png 记录已清理")
+
+ # 清除线上运行时缓存
+ ssh_exec(ssh, f"rm -rf {REMOTE_ROOT}/runtime/temp/* {REMOTE_ROOT}/runtime/cache/*")
+ print(" ✓ 线上运行时缓存已清理")
+
+ # 验证 uploads/20261028 目前保留的文件(只有 13 张 .jpg 真实照片)
+ out, _ = ssh_exec(ssh, f"ls -1 {REMOTE_ROOT}/public/uploads/20261028/")
+ valid_files = [x for x in out.splitlines() if x.strip()]
+ print(f"\n线上 uploads/20261028/ 当前现存有效文件数: {len(valid_files)} 个 (全为 .jpg 真实实拍图)")
+ for vf in valid_files[:3]:
+ print(f" - {vf}")
+ if len(valid_files) > 3:
+ print(f" ... 等共 {len(valid_files)} 张真实实拍图片")
+
+ ssh.close()
+ print("\n✓ 前一批错误图片清理全部圆满完成!")
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/download_and_crop_real_photos.php b/tools/download_and_crop_real_photos.php
new file mode 100644
index 0000000..066bf38
--- /dev/null
+++ b/tools/download_and_crop_real_photos.php
@@ -0,0 +1,176 @@
+ 27,
+ 'name' => '普工技工',
+ 'filename' => 'job-tech.jpg',
+ 'url' => 'https://images.unsplash.com/photo-1504917599217-d4dc5ebe6122?w=500&q=80&auto=format&fit=crop',
+ ],
+ [
+ 'id' => 28,
+ 'name' => '餐饮服务',
+ 'filename' => 'job-catering.jpg',
+ 'url' => 'https://images.unsplash.com/photo-1577219491135-ce391730fb2c?w=500&q=80&auto=format&fit=crop',
+ ],
+ [
+ 'id' => 29,
+ 'name' => '家政保洁',
+ 'filename' => 'job-clean.jpg',
+ 'url' => 'https://images.unsplash.com/photo-1581578731548-c64695cc6952?w=500&q=80&auto=format&fit=crop',
+ ],
+ [
+ 'id' => 30,
+ 'name' => '骑手配送',
+ 'filename' => 'job-rider.jpg',
+ 'url' => 'https://images.unsplash.com/photo-1526367790999-0150786686a2?w=500&q=80&auto=format&fit=crop',
+ ],
+ [
+ 'id' => 31,
+ 'name' => '销售客服',
+ 'filename' => 'job-sales.jpg',
+ 'url' => 'https://images.unsplash.com/photo-1534536281715-e28d76689b4d?w=500&q=80&auto=format&fit=crop',
+ ],
+ [
+ 'id' => 32,
+ 'name' => '司机出行',
+ 'filename' => 'job-driver.jpg',
+ 'url' => 'https://images.unsplash.com/photo-1449965408869-eaa3f722e40d?w=500&q=80&auto=format&fit=crop',
+ ],
+ [
+ 'id' => 33,
+ 'name' => '教育培训',
+ 'filename' => 'job-edu.jpg',
+ 'url' => 'https://images.unsplash.com/photo-1509062522246-3755977927d7?w=500&q=80&auto=format&fit=crop',
+ ],
+ [
+ 'id' => 34,
+ 'name' => '兼职短工',
+ 'filename' => 'job-parttime.jpg',
+ 'url' => 'https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?w=500&q=80&auto=format&fit=crop',
+ ],
+ // 肉蛋家禽 (PID: 26)
+ [
+ 'id' => 35,
+ 'name' => '鲜土鸡蛋',
+ 'filename' => 'poultry-egg.jpg',
+ 'url' => 'https://images.unsplash.com/photo-1582722872445-44dc5f7e3c8f?w=500&q=80&auto=format&fit=crop',
+ ],
+ [
+ 'id' => 36,
+ 'name' => '散养土鸡',
+ 'filename' => 'poultry-chicken.jpg',
+ 'url' => 'https://images.unsplash.com/photo-1548550023-2bdb3c5beed7?w=500&q=80&auto=format&fit=crop',
+ ],
+ [
+ 'id' => 37,
+ 'name' => '生鲜猪肉',
+ 'filename' => 'poultry-pork.jpg',
+ 'url' => 'https://images.unsplash.com/photo-1607623814075-e51df1bdc82f?w=500&q=80&auto=format&fit=crop',
+ ],
+ [
+ 'id' => 38,
+ 'name' => '高山牛肉',
+ 'filename' => 'poultry-beef.jpg',
+ 'url' => 'https://images.unsplash.com/photo-1558030006-450675393462?w=500&q=80&auto=format&fit=crop',
+ ],
+ [
+ 'id' => 39,
+ 'name' => '鲜活水产',
+ 'filename' => 'poultry-fish.jpg',
+ 'url' => 'https://images.unsplash.com/photo-1534043464124-3be32fe00099?w=500&q=80&auto=format&fit=crop',
+ ],
+];
+
+/**
+ * 将图片内容居中裁剪为 240x240 并输出 JPEG
+ *
+ * @param string $binaryData 原始图片二进制流
+ * @param string $savePath 目标保存路径
+ * @return bool
+ */
+function cropToSquare240($binaryData, $savePath)
+{
+ $src = @imagecreatefromstring($binaryData);
+ if (!$src) {
+ return false;
+ }
+
+ $srcW = imagesx($src);
+ $srcH = imagesy($src);
+
+ // 确定正方形裁剪区域(居中裁剪)
+ $cropSize = min($srcW, $srcH);
+ $cropX = (int)(($srcW - $cropSize) / 2);
+ $cropY = (int)(($srcH - $cropSize) / 2);
+
+ $dst = imagecreatetruecolor(240, 240);
+ // 高质量重采样
+ imagecopyresampled($dst, $src, 0, 0, $cropX, $cropY, 240, 240, $cropSize, $cropSize);
+
+ // 以 82% 质量保存 JPEG,保证实拍细节同时体积控制在 15KB 左右
+ $res = imagejpeg($dst, $savePath, 82);
+
+ imagedestroy($src);
+ imagedestroy($dst);
+ return $res;
+}
+
+echo "=== 开始获取真实摄影照片并裁剪为 240x240 实拍图 ===\n";
+
+$results = [];
+
+foreach ($realPhotos as $item) {
+ echo "正在下载 [{$item['name']}] 真实实拍照: {$item['url']} ...\n";
+
+ $opts = [
+ 'http' => [
+ 'method' => 'GET',
+ 'header' => "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)\r\n",
+ 'timeout' => 15,
+ ]
+ ];
+ $context = stream_context_create($opts);
+ $raw = @file_get_contents($item['url'], false, $context);
+
+ if (!$raw) {
+ echo " ✗ 下载失败,跳过: {$item['name']}\n";
+ continue;
+ }
+
+ // 生成 MD5 哈希文件名
+ $tmpName = md5($raw . $item['name']) . '.jpg';
+ $saveAbs = $targetDir . '/' . $tmpName;
+
+ if (cropToSquare240($raw, $saveAbs)) {
+ $finalSize = filesize($saveAbs);
+ $url = '/uploads/20261028/' . $tmpName;
+ echo " ✓ 成功生成 240x240 真实照片: {$url} (大小: " . round($finalSize / 1024, 2) . " KB)\n";
+
+ $results[] = [
+ 'id' => $item['id'],
+ 'name' => $item['name'],
+ 'filename' => $item['filename'],
+ 'local' => $saveAbs,
+ 'rel_url' => $url,
+ 'size' => $finalSize,
+ 'sha1' => sha1_file($saveAbs),
+ ];
+ } else {
+ echo " ✗ 裁剪失败: {$item['name']}\n";
+ }
+}
+
+// 将结果输出到 JSON 临时文件供下一步同步脚本读取
+file_put_contents(__DIR__ . '/real_photos_result.json', json_encode($results, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
+echo "\n已成功生成 " . count($results) . " 张真实二级分类实拍图!\n";
diff --git a/tools/fix_missing_photos.php b/tools/fix_missing_photos.php
new file mode 100644
index 0000000..cbdc309
--- /dev/null
+++ b/tools/fix_missing_photos.php
@@ -0,0 +1,76 @@
+ 27,
+ 'name' => '普工技工',
+ 'filename' => 'job-tech.jpg',
+ 'url' => 'https://images.unsplash.com/photo-1581092335397-9583fe92d232?w=500&q=80&auto=format&fit=crop',
+ ],
+ [
+ 'id' => 39,
+ 'name' => '鲜活水产',
+ 'filename' => 'poultry-fish.jpg',
+ 'url' => 'https://images.unsplash.com/photo-1524704654690-b56c05c78a00?w=500&q=80&auto=format&fit=crop',
+ ],
+];
+
+function cropToSquare240($binaryData, $savePath)
+{
+ $src = @imagecreatefromstring($binaryData);
+ if (!$src) return false;
+ $srcW = imagesx($src);
+ $srcH = imagesy($src);
+ $cropSize = min($srcW, $srcH);
+ $cropX = (int)(($srcW - $cropSize) / 2);
+ $cropY = (int)(($srcH - $cropSize) / 2);
+ $dst = imagecreatetruecolor(240, 240);
+ imagecopyresampled($dst, $src, 0, 0, $cropX, $cropY, 240, 240, $cropSize, $cropSize);
+ $res = imagejpeg($dst, $savePath, 82);
+ imagedestroy($src);
+ imagedestroy($dst);
+ return $res;
+}
+
+foreach ($missing as $item) {
+ echo "正在补齐 [{$item['name']}] 真实实拍照: {$item['url']} ...\n";
+ $raw = @file_get_contents($item['url']);
+ if (!$raw) {
+ // 备用地址
+ $backupUrl = ($item['id'] === 27)
+ ? 'https://images.unsplash.com/photo-1581092160607-ee22621dd758?w=500&q=80&auto=format&fit=crop'
+ : 'https://images.unsplash.com/photo-1516684732162-798a0062be99?w=500&q=80&auto=format&fit=crop';
+ $raw = @file_get_contents($backupUrl);
+ }
+
+ if ($raw) {
+ $tmpName = md5($raw . $item['name']) . '.jpg';
+ $saveAbs = $targetDir . '/' . $tmpName;
+ if (cropToSquare240($raw, $saveAbs)) {
+ $finalSize = filesize($saveAbs);
+ $url = '/uploads/20261028/' . $tmpName;
+ echo " ✓ 成功补齐: {$url} (" . round($finalSize / 1024, 2) . " KB)\n";
+ $existing[] = [
+ 'id' => $item['id'],
+ 'name' => $item['name'],
+ 'filename' => $item['filename'],
+ 'local' => $saveAbs,
+ 'rel_url' => $url,
+ 'size' => $finalSize,
+ 'sha1' => sha1_file($saveAbs),
+ ];
+ }
+ } else {
+ echo " ✗ 仍失败: {$item['name']}\n";
+ }
+}
+
+file_put_contents($jsonFile, json_encode($existing, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
+echo "当前清单已有 " . count($existing) . " 个分类的真实照片!\n";
diff --git a/tools/full_data_sync_to_online.py b/tools/full_data_sync_to_online.py
new file mode 100644
index 0000000..7f1403a
--- /dev/null
+++ b/tools/full_data_sync_to_online.py
@@ -0,0 +1,177 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+数据全面同步到线上:
+1. 校验并补齐 public/uploads/ 所有文件至线上对应目录
+2. 完整同步本地数据库各业务表到线上数据库
+3. 清理线上缓存并验证接口
+"""
+
+import os
+import sys
+import io
+import time
+import paramiko
+import pymysql
+
+sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
+
+LOCAL_ROOT = r"D:\Softwares\phpstudy_pro\WWW\fenleixinxi"
+REMOTE_HOST = "8.137.181.63"
+REMOTE_PORT = 22
+REMOTE_USER = "root"
+REMOTE_PASS = "liyanxia@6688"
+REMOTE_ROOT = "/www/wwwroot/fenleixinxi.gzyakj.com.cn"
+DB_USER = "fenleixinxi"
+DB_PASS = "fenleixinxi"
+DB_NAME = "fenleixinxi"
+
+LOCAL_DB_HOST = "127.0.0.1"
+LOCAL_DB_PORT = 3307
+
+# 核心同步表列表
+SYNC_TABLES = [
+ "fa_user",
+ "fa_user_ext",
+ "fa_attachment",
+ "fa_fc_category",
+ "fa_fc_post",
+ "fa_fc_banner",
+ "fa_fc_comment",
+ "fa_fc_favorite",
+ "fa_fc_follow",
+ "fa_fc_message",
+ "fa_fc_plan",
+ "fa_fc_config",
+ "fa_fc_area",
+ "fa_fc_merchant",
+]
+
+def ssh_exec(client, cmd):
+ stdin, stdout, stderr = client.exec_command(cmd)
+ out = stdout.read().decode("utf-8", errors="replace").strip()
+ err = stderr.read().decode("utf-8", errors="replace").strip()
+ return out, err
+
+def main():
+ print("=== [1/3] 检查并同步本地 uploads 文件到线上服务器 ===")
+ ssh = paramiko.SSHClient()
+ ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ ssh.connect(REMOTE_HOST, port=REMOTE_PORT, username=REMOTE_USER, password=REMOTE_PASS, timeout=30)
+ print("✓ 远程 SSH 连接成功")
+
+ sftp = ssh.open_sftp()
+
+ # 扫描本地 public/uploads/ 下所有文件
+ local_uploads = os.path.join(LOCAL_ROOT, "public", "uploads")
+ uploaded_cnt = 0
+
+ for root, dirs, files in os.walk(local_uploads):
+ rel_dir = os.path.relpath(root, local_uploads)
+ if rel_dir == ".":
+ remote_sub_dir = f"{REMOTE_ROOT}/public/uploads"
+ else:
+ remote_sub_dir = f"{REMOTE_ROOT}/public/uploads/{rel_dir.replace(chr(92), '/')}"
+
+ # 确保远程目录存在
+ ssh_exec(ssh, f"mkdir -p '{remote_sub_dir}'")
+
+ for f in files:
+ if f == ".gitkeep":
+ continue
+ loc_file = os.path.join(root, f)
+ rem_file = f"{remote_sub_dir}/{f}"
+
+ # 检查远程文件是否存在且大小一致
+ try:
+ r_stat = sftp.stat(rem_file)
+ l_size = os.path.getsize(loc_file)
+ if r_stat.st_size == l_size:
+ continue # 大小一致跳过
+ except IOError:
+ pass # 远程不存在
+
+ sftp.put(loc_file, rem_file)
+ ssh_exec(ssh, f"chown www:www '{rem_file}'")
+ uploaded_cnt += 1
+ print(f" ✓ 补充上传文件: uploads/{rel_dir}/{f}")
+
+ print(f"✓ 文件检查完毕,本次共增量补传文件: {uploaded_cnt} 个")
+ sftp.close()
+
+ print("\n=== [2/3] 全量同步本地数据库表到线上 ===")
+ local_conn = pymysql.connect(
+ host=LOCAL_DB_HOST,
+ port=LOCAL_DB_PORT,
+ user=DB_USER,
+ password=DB_PASS,
+ database=DB_NAME,
+ charset="utf8mb4",
+ autocommit=True
+ )
+
+ sync_sqls = [
+ "SET FOREIGN_KEY_CHECKS = 0;",
+ "SET NAMES utf8mb4;"
+ ]
+
+ with local_conn.cursor() as cur:
+ for t in SYNC_TABLES:
+ try:
+ cur.execute(f"SELECT * FROM `{t}`;")
+ rows = cur.fetchall()
+ cur.execute(f"DESCRIBE `{t}`;")
+ cols = [f[0] for f in cur.fetchall()]
+ except Exception as e:
+ print(f" - 跳过表 {t} ({e})")
+ continue
+
+ col_str = ", ".join([f"`{c}`" for c in cols])
+ sync_sqls.append(f"TRUNCATE TABLE `{t}`;")
+ for r in rows:
+ val_list = []
+ for v in r:
+ if v is None:
+ val_list.append("NULL")
+ elif isinstance(v, (int, float)):
+ val_list.append(str(v))
+ else:
+ esc = str(v).replace("\\", "\\\\").replace("'", "\\'")
+ val_list.append(f"'{esc}'")
+ sync_sqls.append(f"INSERT INTO `{t}` ({col_str}) VALUES ({', '.join(val_list)});")
+ print(f" ✓ 表 [{t}] 同步准备完毕: {len(rows)} 条记录")
+
+ sync_sqls.append("SET FOREIGN_KEY_CHECKS = 1;")
+ full_sql = "\n".join(sync_sqls)
+
+ # 在线上执行
+ mysql_cmd = f'mysql -u{DB_USER} -p{DB_PASS} --default-character-set=utf8mb4 {DB_NAME} 2>/dev/null <<\'SQLEOF\'\n{full_sql}\nSQLEOF'
+ out, err = ssh_exec(ssh, mysql_cmd)
+ if err and "error" in err.lower():
+ print(f"✗ 线上数据库更新失败: {err}")
+ else:
+ print("✓ 线上数据库所有业务表已全量重写同步完成!")
+
+ local_conn.close()
+
+ print("\n=== [3/3] 清理缓存并校验 ===")
+ ssh_exec(ssh, f"rm -rf {REMOTE_ROOT}/runtime/temp/* {REMOTE_ROOT}/runtime/cache/*")
+ print("✓ 线上 ThinkPHP 运行时缓存清理完成")
+
+ # 验证分类接口
+ cmd_api = "curl -s -k 'https://fenleixinxi.gzyakj.com.cn/api/v1/home/categories'"
+ api_res, _ = ssh_exec(ssh, cmd_api)
+ if '"code":0' in api_res:
+ print("✓ 线上分类 API (GET /api/v1/home/categories) 校验通过 (HTTP 200 OK)")
+
+ # 验证信息流接口
+ cmd_feed = "curl -s -k 'https://fenleixinxi.gzyakj.com.cn/api/v1/home/feed'"
+ feed_res, _ = ssh_exec(ssh, cmd_feed)
+ if '"code":0' in feed_res:
+ print("✓ 线上信息流 API (GET /api/v1/home/feed) 校验通过 (HTTP 200 OK)")
+
+ ssh.close()
+ print("\n✓ 数据同步到线上全部圆满完成!")
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/generate_more_categories.php b/tools/generate_more_categories.php
new file mode 100644
index 0000000..521b0da
--- /dev/null
+++ b/tools/generate_more_categories.php
@@ -0,0 +1,453 @@
+= ($size - $rad) && $y < $rad) {
+ $in = (($x - ($size - $rad)) ** 2 + ($y - $rad) ** 2) <= ($rad ** 2);
+ } elseif ($x < $rad && $y >= ($size - $rad)) {
+ $in = (($x - $rad) ** 2 + ($y - ($size - $rad)) ** 2) <= ($rad ** 2);
+ } elseif ($x >= ($size - $rad) && $y >= ($size - $rad)) {
+ $in = (($x - ($size - $rad)) ** 2 + ($y - ($size - $rad)) ** 2) <= ($rad ** 2);
+ }
+ if ($in) {
+ imagesetpixel($img, $x, $y, $col);
+ }
+ }
+ }
+
+ imagealphablending($img, true);
+ $mainCol = imagecolorallocate($img, $cfg['main'][0], $cfg['main'][1], $cfg['main'][2]);
+ $subCol = imagecolorallocate($img, $cfg['sub'][0], $cfg['sub'][1], $cfg['sub'][2]);
+ $white = imagecolorallocate($img, 255, 255, 255);
+ $gold = imagecolorallocate($img, 245, 166, 35);
+ $dark = imagecolorallocate($img, 30, 41, 59);
+
+ $cx = 120;
+ $cy = 96;
+
+ // 根据造型绘制微质感矢量形状
+ switch ($cfg['shape']) {
+ case 'briefcase':
+ // 公文包 + 领结 / 招聘专区
+ // 手柄
+ imagefilledellipse($img, $cx, $cy - 40, 44, 28, $subCol);
+ imagefilledellipse($img, $cx, $cy - 38, 28, 16, $white);
+ // 包身
+ imagefilledrectangle($img, $cx - 52, $cy - 24, $cx + 52, $cy + 42, $mainCol);
+ imagefilledrectangle($img, $cx - 48, $cy - 20, $cx + 48, $cy + 6, $subCol);
+ // 金扣
+ imagefilledellipse($img, $cx, $cy + 8, 18, 18, $gold);
+ imagefilledellipse($img, $cx, $cy + 8, 8, 8, $white);
+ break;
+
+ case 'gear':
+ // 普工技工:齿轮 + 扳手
+ imagefilledellipse($img, $cx, $cy, 88, 88, $mainCol);
+ for ($i = 0; $i < 8; $i++) {
+ $angle = $i * (M_PI / 4);
+ $gx = $cx + (int)(cos($angle) * 44);
+ $gy = $cy + (int)(sin($angle) * 44);
+ imagefilledellipse($img, $gx, $gy, 22, 22, $mainCol);
+ }
+ imagefilledellipse($img, $cx, $cy, 46, 46, $white);
+ imagefilledellipse($img, $cx, $cy, 28, 28, $subCol);
+ break;
+
+ case 'chef':
+ // 餐饮服务:主厨帽 + 托盘
+ // 托盘底
+ imagefilledellipse($img, $cx, $cy + 36, 96, 22, $subCol);
+ imagefilledrectangle($img, $cx - 36, $cy + 8, $cx + 36, $cy + 30, $white);
+ // 蓬松帽顶
+ imagefilledellipse($img, $cx - 26, $cy - 8, 48, 48, $white);
+ imagefilledellipse($img, $cx + 26, $cy - 8, 48, 48, $white);
+ imagefilledellipse($img, $cx, $cy - 20, 56, 56, $white);
+ // 帽褶彩带
+ imagefilledrectangle($img, $cx - 32, $cy + 22, $cx + 32, $cy + 28, $mainCol);
+ break;
+
+ case 'clean':
+ // 家政保洁:晶亮喷壶 + 气泡星光
+ imagefilledellipse($img, $cx - 10, $cy + 16, 68, 64, $mainCol);
+ imagefilledrectangle($img, $cx - 22, $cy - 12, $cx + 2, $cy + 16, $mainCol);
+ // 喷头
+ imagefilledrectangle($img, $cx - 32, $cy - 28, $cx + 24, $cy - 12, $subCol);
+ imagefilledrectangle($img, $cx - 38, $cy - 24, $cx - 32, $cy - 16, $gold);
+ // 洁净星光
+ imagefilledellipse($img, $cx + 32, $cy - 16, 20, 20, $gold);
+ imagefilledellipse($img, $cx + 42, $cy + 14, 14, 14, $white);
+ break;
+
+ case 'helmet':
+ // 骑手配送:骑手头盔 + 护目镜
+ imagefilledellipse($img, $cx, $cy + 2, 92, 84, $mainCol);
+ imagefilledarc($img, $cx, $cy + 2, 92, 84, 0, 180, $subCol, IMG_ARC_PIE);
+ // 护目镜
+ imagefilledrectangle($img, $cx - 36, $cy - 6, $cx + 36, $cy + 14, $dark);
+ imagefilledellipse($img, $cx - 16, $cy + 4, 16, 16, imagecolorallocatealpha($img, 255, 255, 255, 60));
+ // 帽檐
+ imagefilledellipse($img, $cx, $cy - 30, 48, 12, $gold);
+ break;
+
+ case 'headset':
+ // 销售客服:耳麦 + 亲和对话
+ imagefilledellipse($img, $cx, $cy, 68, 68, $subCol);
+ imagefilledarc($img, $cx, $cy - 8, 92, 92, 190, 350, $mainCol, IMG_ARC_NOFILL);
+ // 左右耳机
+ imagefilledellipse($img, $cx - 44, $cy, 22, 38, $mainCol);
+ imagefilledellipse($img, $cx + 44, $cy, 22, 38, $mainCol);
+ // 麦克风
+ imagefilledarc($img, $cx + 24, $cy + 26, 44, 44, 45, 160, $mainCol, IMG_ARC_NOFILL);
+ imagefilledellipse($img, $cx + 10, $cy + 44, 14, 14, $gold);
+ break;
+
+ case 'steering':
+ // 司机出行:方向盘
+ imagefilledellipse($img, $cx, $cy, 96, 96, $mainCol);
+ imagefilledellipse($img, $cx, $cy, 66, 66, $white);
+ // 中心柱
+ imagefilledellipse($img, $cx, $cy, 36, 36, $mainCol);
+ imagefilledellipse($img, $cx, $cy, 18, 18, $gold);
+ // 左右辐射臂
+ imagefilledrectangle($img, $cx - 46, $cy - 6, $cx + 46, $cy + 6, $mainCol);
+ imagefilledrectangle($img, $cx - 6, $cy, $cx + 6, $cy + 46, $mainCol);
+ break;
+
+ case 'cap':
+ // 教育培训:学士帽 + 书卷
+ imagefilledellipse($img, $cx, $cy - 12, 102, 36, $mainCol);
+ // 帽顶菱形
+ $diamond = [$cx, $cy - 34, $cx + 48, $cy - 12, $cx, $cy + 10, $cx - 48, $cy - 12];
+ imagefilledpolygon($img, $diamond, 4, $mainCol);
+ // 流苏
+ imagefilledellipse($img, $cx + 34, $cy + 16, 12, 28, $gold);
+ // 书卷
+ imagefilledrectangle($img, $cx - 36, $cy + 26, $cx + 36, $cy + 42, $white);
+ imagefilledellipse($img, $cx - 36, $cy + 34, 12, 16, $subCol);
+ imagefilledellipse($img, $cx + 36, $cy + 34, 12, 16, $subCol);
+ break;
+
+ case 'clock':
+ // 兼职短工:闪电日结 + 时钟
+ imagefilledellipse($img, $cx, $cy, 88, 88, $white);
+ imagefilledarc($img, $cx, $cy, 88, 88, 0, 360, $subCol, IMG_ARC_NOFILL);
+ // 表针
+ imagefilledrectangle($img, $cx - 3, $cy - 28, $cx + 3, $cy, $mainCol);
+ imagefilledrectangle($img, $cx, $cy - 3, $cx + 22, $cy + 3, $mainCol);
+ // 闪电角标
+ $lightning = [$cx + 14, $cy - 28, $cx + 38, $cy - 28, $cx + 24, $cy - 8, $cx + 42, $cy - 8, $cx + 12, $cy + 28, $cx + 20, $cy + 6, $cx + 8, $cy + 6];
+ imagefilledpolygon($img, $lightning, 7, $gold);
+ break;
+
+ case 'egg':
+ // 鲜土鸡蛋:温润金蛋双生
+ imagefilledellipse($img, $cx - 18, $cy + 8, 62, 82, $subCol);
+ imagefilledellipse($img, $cx + 18, $cy + 4, 58, 76, $gold);
+ imagefilledellipse($img, $cx + 14, $cy - 8, 16, 26, imagecolorallocatealpha($img, 255, 255, 255, 70));
+ break;
+
+ case 'chicken':
+ // 散养土鸡:生动公鸡微插画
+ imagefilledellipse($img, $cx, $cy + 14, 76, 68, $subCol);
+ // 鸡冠
+ imagefilledellipse($img, $cx - 8, $cy - 32, 18, 26, imagecolorallocate($img, 239, 68, 68));
+ imagefilledellipse($img, $cx + 4, $cy - 36, 18, 28, imagecolorallocate($img, 239, 68, 68));
+ // 鸡头与喙
+ imagefilledellipse($img, $cx, $cy - 18, 44, 44, $mainCol);
+ $beak = [$cx + 16, $cy - 22, $cx + 36, $cy - 14, $cx + 16, $cy - 6];
+ imagefilledpolygon($img, $beak, 3, $gold);
+ break;
+
+ case 'meat':
+ // 生鲜肉类 / 牛肉
+ imagefilledellipse($img, $cx, $cy + 6, 98, 72, $mainCol);
+ imagefilledellipse($img, $cx - 10, $cy + 6, 76, 52, $subCol);
+ // 骨头 / 雪花纹
+ imagefilledellipse($img, $cx + 26, $cy - 2, 22, 22, $white);
+ break;
+
+ case 'fish':
+ // 鲜活水产:灵动游鱼
+ imagefilledellipse($img, $cx, $cy + 6, 88, 54, $mainCol);
+ // 鱼尾
+ $tail = [$cx - 36, $cy + 6, $cx - 62, $cy - 18, $cx - 62, $cy + 30];
+ imagefilledpolygon($img, $tail, 3, $subCol);
+ // 鱼眼
+ imagefilledellipse($img, $cx + 26, $cy, 12, 12, $white);
+ imagefilledellipse($img, $cx + 28, $cy, 6, 6, $dark);
+ break;
+
+ default:
+ // 饱满徽章默认
+ imagefilledellipse($img, $cx, $cy + 6, 88, 88, $mainCol);
+ imagefilledellipse($img, $cx, $cy + 6, 66, 66, $subCol);
+ break;
+ }
+
+ // 底部文字标签
+ if ($fontFile) {
+ $text = $cfg['name'];
+ $fontSize = 21;
+ $box = imagettfbbox($fontSize, 0, $fontFile, $text);
+ $tw = $box[2] - $box[0];
+ $tx = (int)(($size - $tw) / 2) - $box[0];
+ $ty = 208;
+ $textCol = imagecolorallocate($img, 30, 41, 59);
+ imagettftext($img, $fontSize, 0, $tx, $ty, $textCol, $fontFile, $text);
+ }
+
+ // 保存文件
+ $catPath1 = __DIR__ . '/../public/static/categories/' . $key . '.png';
+ $catPath2 = __DIR__ . '/../uniapp/static/categories/' . $key . '.png';
+ imagepng($img, $catPath1);
+ imagepng($img, $catPath2);
+
+ // 如果是一级专区,也写一份到 icons 目录备用
+ if (strpos($key, 'cat-') === 0) {
+ imagepng($img, __DIR__ . '/../public/static/icons/' . $key . '.png');
+ imagepng($img, __DIR__ . '/../uniapp/static/icons/' . $key . '.png');
+ }
+
+ imagedestroy($img);
+ echo " ✓ 生成图标: {$key}.png ({$cfg['name']})\n";
+}
+
+// 3. 定义需要生成和入库的分类
+$categoriesData = [
+ // 一级专区更正图标
+ 'cat-job' => [
+ 'is_parent' => true,
+ 'id' => 25,
+ 'name' => '招聘专区',
+ 'bg1' => [220, 252, 231], 'bg2' => [204, 251, 241],
+ 'main' => [16, 185, 129], 'sub' => [110, 231, 183],
+ 'shape' => 'briefcase'
+ ],
+ 'cat-poultry' => [
+ 'is_parent' => true,
+ 'id' => 26,
+ 'name' => '肉蛋家禽',
+ 'bg1' => [254, 243, 199], 'bg2' => [254, 215, 170],
+ 'main' => [234, 88, 12], 'sub' => [251, 146, 60],
+ 'shape' => 'egg'
+ ],
+
+ // 招聘专区 (PID = 25) 二级分类
+ 'job-tech' => [
+ 'pid' => 25,
+ 'name' => '普工技工',
+ 'bg1' => [236, 253, 245], 'bg2' => [209, 250, 229],
+ 'main' => [5, 150, 105], 'sub' => [52, 211, 153],
+ 'shape' => 'gear'
+ ],
+ 'job-catering' => [
+ 'pid' => 25,
+ 'name' => '餐饮服务',
+ 'bg1' => [255, 247, 237], 'bg2' => [254, 215, 170],
+ 'main' => [234, 88, 12], 'sub' => [251, 146, 60],
+ 'shape' => 'chef'
+ ],
+ 'job-clean' => [
+ 'pid' => 25,
+ 'name' => '家政保洁',
+ 'bg1' => [240, 253, 244], 'bg2' => [187, 247, 208],
+ 'main' => [22, 163, 74], 'sub' => [134, 239, 172],
+ 'shape' => 'clean'
+ ],
+ 'job-rider' => [
+ 'pid' => 25,
+ 'name' => '骑手配送',
+ 'bg1' => [254, 249, 195], 'bg2' => [254, 240, 138],
+ 'main' => [202, 138, 4], 'sub' => [250, 204, 21],
+ 'shape' => 'helmet'
+ ],
+ 'job-sales' => [
+ 'pid' => 25,
+ 'name' => '销售客服',
+ 'bg1' => [238, 242, 255], 'bg2' => [224, 231, 255],
+ 'main' => [79, 70, 229], 'sub' => [165, 180, 252],
+ 'shape' => 'headset'
+ ],
+ 'job-driver' => [
+ 'pid' => 25,
+ 'name' => '司机出行',
+ 'bg1' => [241, 245, 249], 'bg2' => [226, 232, 240],
+ 'main' => [71, 85, 105], 'sub' => [148, 163, 184],
+ 'shape' => 'steering'
+ ],
+ 'job-edu' => [
+ 'pid' => 25,
+ 'name' => '教育培训',
+ 'bg1' => [245, 243, 255], 'bg2' => [237, 233, 254],
+ 'main' => [124, 58, 237], 'sub' => [196, 181, 253],
+ 'shape' => 'cap'
+ ],
+ 'job-parttime' => [
+ 'pid' => 25,
+ 'name' => '兼职短工',
+ 'bg1' => [254, 242, 242], 'bg2' => [254, 226, 226],
+ 'main' => [220, 38, 38], 'sub' => [248, 113, 113],
+ 'shape' => 'clock'
+ ],
+
+ // 肉蛋家禽 (PID = 26) 二级分类
+ 'poultry-egg' => [
+ 'pid' => 26,
+ 'name' => '鲜土鸡蛋',
+ 'bg1' => [255, 251, 235], 'bg2' => [254, 243, 199],
+ 'main' => [217, 119, 6], 'sub' => [245, 158, 11],
+ 'shape' => 'egg'
+ ],
+ 'poultry-chicken' => [
+ 'pid' => 26,
+ 'name' => '散养土鸡',
+ 'bg1' => [255, 247, 237], 'bg2' => [255, 237, 213],
+ 'main' => [194, 65, 12], 'sub' => [249, 115, 22],
+ 'shape' => 'chicken'
+ ],
+ 'poultry-pork' => [
+ 'pid' => 26,
+ 'name' => '生鲜猪肉',
+ 'bg1' => [255, 241, 242], 'bg2' => [255, 228, 230],
+ 'main' => [225, 29, 72], 'sub' => [251, 113, 133],
+ 'shape' => 'meat'
+ ],
+ 'poultry-beef' => [
+ 'pid' => 26,
+ 'name' => '高山牛肉',
+ 'bg1' => [254, 242, 242], 'bg2' => [254, 202, 202],
+ 'main' => [185, 28, 28], 'sub' => [239, 68, 68],
+ 'shape' => 'meat'
+ ],
+ 'poultry-fish' => [
+ 'pid' => 26,
+ 'name' => '鲜活水产',
+ 'bg1' => [240, 249, 255], 'bg2' => [224, 242, 254],
+ 'main' => [2, 132, 199], 'sub' => [56, 189, 248],
+ 'shape' => 'fish'
+ ],
+];
+
+echo "=== 1. 开始生成微质感分类图标 ===\n";
+foreach ($categoriesData as $key => $cfg) {
+ drawCategoryIcon($key, $cfg, $fontFile);
+}
+
+// 4. 连接本地数据库写入数据
+echo "\n=== 2. 更新本地数据库 fa_fc_category ===\n";
+try {
+ $pdo = new PDO("mysql:host=127.0.0.1;port=3307;dbname=fenleixinxi;charset=utf8mb4", "fenleixinxi", "fenleixinxi", [
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
+ ]);
+
+ // 更新一级专区图标
+ $stmtUpd = $pdo->prepare("UPDATE `fa_fc_category` SET `image` = :image, `updatetime` = :now WHERE `id` = :id");
+ $stmtUpd->execute([
+ ':image' => '/static/categories/cat-job.png',
+ ':now' => time(),
+ ':id' => 25
+ ]);
+ echo " ✓ 更新一级【招聘专区】图标为 /static/categories/cat-job.png\n";
+
+ $stmtUpd->execute([
+ ':image' => '/static/categories/cat-poultry.png',
+ ':now' => time(),
+ ':id' => 26
+ ]);
+ echo " ✓ 更新一级【肉蛋家禽】图标为 /static/categories/cat-poultry.png\n";
+
+ // 插入二级子分类
+ $stmtChk = $pdo->prepare("SELECT `id` FROM `fa_fc_category` WHERE `pid` = :pid AND `name` = :name LIMIT 1");
+ $stmtIns = $pdo->prepare("INSERT INTO `fa_fc_category` (`pid`, `name`, `icon_key`, `image`, `kind`, `color`, `sort`, `enabled`, `post_count`, `createtime`, `updatetime`) VALUES (:pid, :name, :icon_key, :image, 'normal', '', :sort, 1, 0, :createtime, :updatetime)");
+
+ $sort = 10;
+ foreach ($categoriesData as $key => $cfg) {
+ if (!empty($cfg['is_parent'])) {
+ continue;
+ }
+
+ $stmtChk->execute([':pid' => $cfg['pid'], ':name' => $cfg['name']]);
+ $exists = $stmtChk->fetch(PDO::FETCH_ASSOC);
+
+ $imgUrl = '/static/categories/' . $key . '.png';
+ $now = time();
+
+ if ($exists) {
+ $stmtUpdateSub = $pdo->prepare("UPDATE `fa_fc_category` SET `image` = :image, `enabled` = 1, `updatetime` = :now WHERE `id` = :id");
+ $stmtUpdateSub->execute([':image' => $imgUrl, ':now' => $now, ':id' => $exists['id']]);
+ echo " ✓ 已存在更新子分类: {$cfg['name']} (ID: {$exists['id']})\n";
+ } else {
+ $stmtIns->execute([
+ ':pid' => $cfg['pid'],
+ ':name' => $cfg['name'],
+ ':icon_key' => '',
+ ':image' => $imgUrl,
+ ':sort' => $sort,
+ ':createtime' => $now,
+ ':updatetime' => $now,
+ ]);
+ $newId = $pdo->lastInsertId();
+ echo " ✓ 新增子分类成功: {$cfg['name']} (ID: {$newId}, PID: {$cfg['pid']})\n";
+ }
+ $sort += 10;
+ }
+
+ echo "\n=== 3. 统计当前本地分类总数 ===\n";
+ $count = $pdo->query("SELECT COUNT(*) FROM `fa_fc_category`")->fetchColumn();
+ echo " 当前 fa_fc_category 总记录数: {$count} 条\n";
+
+} catch (Exception $e) {
+ echo "✗ 数据库操作失败: " . $e->getMessage() . "\n";
+}
diff --git a/tools/more_fruits_result.json b/tools/more_fruits_result.json
new file mode 100644
index 0000000..aadc0f3
--- /dev/null
+++ b/tools/more_fruits_result.json
@@ -0,0 +1,58 @@
+[
+ {
+ "name": "芒果",
+ "filename": "fruit-mango.jpg",
+ "local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/f1a4f3c123c81aea2f0fd24c8688d988.jpg",
+ "rel_url": "\/uploads\/20261028\/f1a4f3c123c81aea2f0fd24c8688d988.jpg",
+ "size": 5060,
+ "sha1": "4f7f7508e021a54ca32108004bd1ce771c218064"
+ },
+ {
+ "name": "香蕉",
+ "filename": "fruit-banana.jpg",
+ "local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/d8b5420c12f7921e325050be088a011f.jpg",
+ "rel_url": "\/uploads\/20261028\/d8b5420c12f7921e325050be088a011f.jpg",
+ "size": 5441,
+ "sha1": "8f16d992f4b66528a33d662a3b0ce68db4d25fd2"
+ },
+ {
+ "name": "蓝莓",
+ "filename": "fruit-blueberry.jpg",
+ "local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/75dd5971bdfa99d64a62c5b1e72cac11.jpg",
+ "rel_url": "\/uploads\/20261028\/75dd5971bdfa99d64a62c5b1e72cac11.jpg",
+ "size": 13316,
+ "sha1": "40cad819b3faee26e4c98832c74ecd1493ce5d46"
+ },
+ {
+ "name": "车厘子",
+ "filename": "fruit-cherry.jpg",
+ "local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/e71ba37749da829c50d098168dbb4fa2.jpg",
+ "rel_url": "\/uploads\/20261028\/e71ba37749da829c50d098168dbb4fa2.jpg",
+ "size": 4106,
+ "sha1": "b7db86b40ec2335140575019305b1c0123eebf67"
+ },
+ {
+ "name": "火龙果",
+ "filename": "fruit-dragon.jpg",
+ "local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/65d91dba8121f8b49356f356aa6a3b28.jpg",
+ "rel_url": "\/uploads\/20261028\/65d91dba8121f8b49356f356aa6a3b28.jpg",
+ "size": 20986,
+ "sha1": "d8b8c0e89e84a46d1ceed6c57b1a72898f177fc0"
+ },
+ {
+ "name": "柠檬",
+ "filename": "fruit-lemon.jpg",
+ "local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/0391292554cf8efd457c44caedf67867.jpg",
+ "rel_url": "\/uploads\/20261028\/0391292554cf8efd457c44caedf67867.jpg",
+ "size": 13878,
+ "sha1": "c0a31fc168d78346edeef962704de7fadf7414aa"
+ },
+ {
+ "name": "荔枝",
+ "filename": "fruit-lychee.jpg",
+ "local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/49b39ce93043ec8a6f24bcef9bc45f96.jpg",
+ "rel_url": "\/uploads\/20261028\/49b39ce93043ec8a6f24bcef9bc45f96.jpg",
+ "size": 11459,
+ "sha1": "2a47e25354ac30f7c9fad88046b1a4049a77e4dd"
+ }
+]
\ No newline at end of file
diff --git a/tools/real_photos_result.json b/tools/real_photos_result.json
new file mode 100644
index 0000000..8607958
--- /dev/null
+++ b/tools/real_photos_result.json
@@ -0,0 +1,119 @@
+[
+ {
+ "id": 28,
+ "name": "餐饮服务",
+ "filename": "job-catering.jpg",
+ "local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/4e4f911ac45e9dc5471597a8ff7862f7.jpg",
+ "rel_url": "\/uploads\/20261028\/4e4f911ac45e9dc5471597a8ff7862f7.jpg",
+ "size": 16228,
+ "sha1": "99699e9e1ef1399488943dfc957299fa6064929f"
+ },
+ {
+ "id": 29,
+ "name": "家政保洁",
+ "filename": "job-clean.jpg",
+ "local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/915423715a937ad7ba4b737690c963d2.jpg",
+ "rel_url": "\/uploads\/20261028\/915423715a937ad7ba4b737690c963d2.jpg",
+ "size": 10120,
+ "sha1": "98ea384b78cb991a3e747b36ff482739f4013478"
+ },
+ {
+ "id": 30,
+ "name": "骑手配送",
+ "filename": "job-rider.jpg",
+ "local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/3fb1eee6654e4d5e7c1b5d3189643fae.jpg",
+ "rel_url": "\/uploads\/20261028\/3fb1eee6654e4d5e7c1b5d3189643fae.jpg",
+ "size": 15521,
+ "sha1": "88c40d6f5461a579b58957c616e72dd4c9fe2ff3"
+ },
+ {
+ "id": 31,
+ "name": "销售客服",
+ "filename": "job-sales.jpg",
+ "local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/d0cc33f85a8580be2f526eb360448867.jpg",
+ "rel_url": "\/uploads\/20261028\/d0cc33f85a8580be2f526eb360448867.jpg",
+ "size": 7681,
+ "sha1": "9feaf44299cf20b3e180242742077325b41f3505"
+ },
+ {
+ "id": 32,
+ "name": "司机出行",
+ "filename": "job-driver.jpg",
+ "local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/f5e2927ba040dd1b917b6fbd6b053458.jpg",
+ "rel_url": "\/uploads\/20261028\/f5e2927ba040dd1b917b6fbd6b053458.jpg",
+ "size": 10392,
+ "sha1": "e037657705d91da753808850d7bb037edb21d81e"
+ },
+ {
+ "id": 33,
+ "name": "教育培训",
+ "filename": "job-edu.jpg",
+ "local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/99c32f9508e3429ea02a4ce1c34f9df2.jpg",
+ "rel_url": "\/uploads\/20261028\/99c32f9508e3429ea02a4ce1c34f9df2.jpg",
+ "size": 13654,
+ "sha1": "8c0e15e6b1ce5e8c3db2433c14e3424c9c98a1d2"
+ },
+ {
+ "id": 34,
+ "name": "兼职短工",
+ "filename": "job-parttime.jpg",
+ "local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/0c913f9d7d72a10c0c598c36eea89b14.jpg",
+ "rel_url": "\/uploads\/20261028\/0c913f9d7d72a10c0c598c36eea89b14.jpg",
+ "size": 22802,
+ "sha1": "2b30e25c4867ed9131793a49d83697d8990849b7"
+ },
+ {
+ "id": 35,
+ "name": "鲜土鸡蛋",
+ "filename": "poultry-egg.jpg",
+ "local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/69b65e455081d5aa8834b48bd879009d.jpg",
+ "rel_url": "\/uploads\/20261028\/69b65e455081d5aa8834b48bd879009d.jpg",
+ "size": 10702,
+ "sha1": "edd5af80cbf31c7edec3bc652a4fd95394069e90"
+ },
+ {
+ "id": 36,
+ "name": "散养土鸡",
+ "filename": "poultry-chicken.jpg",
+ "local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/cafbe884f6616622dcdd5650bbbce3b3.jpg",
+ "rel_url": "\/uploads\/20261028\/cafbe884f6616622dcdd5650bbbce3b3.jpg",
+ "size": 12906,
+ "sha1": "a1b2f6aaf86426eb6d14d5325cb5e3fe2df8f8ab"
+ },
+ {
+ "id": 37,
+ "name": "生鲜猪肉",
+ "filename": "poultry-pork.jpg",
+ "local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/d2a3226fd6ffa12d24db06c9342a9118.jpg",
+ "rel_url": "\/uploads\/20261028\/d2a3226fd6ffa12d24db06c9342a9118.jpg",
+ "size": 19629,
+ "sha1": "0f385594128a9f1892fe883bcf483350c77e05a8"
+ },
+ {
+ "id": 38,
+ "name": "高山牛肉",
+ "filename": "poultry-beef.jpg",
+ "local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/70cafc924bc7e4018bfefbe883e27c78.jpg",
+ "rel_url": "\/uploads\/20261028\/70cafc924bc7e4018bfefbe883e27c78.jpg",
+ "size": 12415,
+ "sha1": "9f4c06215009ed68df1f1b688719f97506e2fe33"
+ },
+ {
+ "id": 27,
+ "name": "普工技工",
+ "filename": "job-tech.jpg",
+ "local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/cb17be7476ce1c59e14944f9fe60438c.jpg",
+ "rel_url": "\/uploads\/20261028\/cb17be7476ce1c59e14944f9fe60438c.jpg",
+ "size": 12036,
+ "sha1": "fa74bd9c17980d42c925999accf06569c4083a14"
+ },
+ {
+ "id": 39,
+ "name": "鲜活水产",
+ "filename": "poultry-fish.jpg",
+ "local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/bb63e21dae84dc15056a0b50753577d9.jpg",
+ "rel_url": "\/uploads\/20261028\/bb63e21dae84dc15056a0b50753577d9.jpg",
+ "size": 6027,
+ "sha1": "56885fb7817cfc0f3f809c8ef1f7f8955b6f5855"
+ }
+]
\ No newline at end of file
diff --git a/tools/simulate_upload_and_sync.py b/tools/simulate_upload_and_sync.py
new file mode 100644
index 0000000..f6f6be2
--- /dev/null
+++ b/tools/simulate_upload_and_sync.py
@@ -0,0 +1,243 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+1. 将二级分类图标模拟真实后台上传,存放到 public/uploads/20261028/{md5}.png
+2. 写入本地 fa_attachment 附件表与 fa_fc_category 分类表
+3. 将新增 uploads 文件及静态一级图标 SFTP 同步至线上服务器
+4. 将本地 fa_fc_category 与 fa_attachment 变更同步至线上数据库
+5. 清除缓存并校验线上接口
+"""
+
+import os
+import sys
+import io
+import time
+import hashlib
+import paramiko
+import pymysql
+
+sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
+
+# 本地路径
+LOCAL_ROOT = r"D:\Softwares\phpstudy_pro\WWW\fenleixinxi"
+PUBLIC_DIR = os.path.join(LOCAL_ROOT, "public")
+STATIC_CAT_DIR = os.path.join(PUBLIC_DIR, "static", "categories")
+DATE_DIR = "20261028"
+UPLOADS_TARGET_DIR = os.path.join(PUBLIC_DIR, "uploads", DATE_DIR)
+
+# 远程配置
+REMOTE_HOST = "8.137.181.63"
+REMOTE_PORT = 22
+REMOTE_USER = "root"
+REMOTE_PASS = "liyanxia@6688"
+REMOTE_ROOT = "/www/wwwroot/fenleixinxi.gzyakj.com.cn"
+DB_USER = "fenleixinxi"
+DB_PASS = "fenleixinxi"
+DB_NAME = "fenleixinxi"
+
+# 本地数据库配置
+LOCAL_DB_HOST = "127.0.0.1"
+LOCAL_DB_PORT = 3307
+
+# 13个二级分类映射关系: (分类ID, 本地原文件名, 分类名)
+SUB_CATS = [
+ (27, "job-tech.png", "普工技工"),
+ (28, "job-catering.png", "餐饮服务"),
+ (29, "job-clean.png", "家政保洁"),
+ (30, "job-rider.png", "骑手配送"),
+ (31, "job-sales.png", "销售客服"),
+ (32, "job-driver.png", "司机出行"),
+ (33, "job-edu.png", "教育培训"),
+ (34, "job-parttime.png", "兼职短工"),
+ (35, "poultry-egg.png", "鲜土鸡蛋"),
+ (36, "poultry-chicken.png", "散养土鸡"),
+ (37, "poultry-pork.png", "生鲜猪肉"),
+ (38, "poultry-beef.png", "高山牛肉"),
+ (39, "poultry-fish.png", "鲜活水产"),
+]
+
+def ssh_exec(client, cmd):
+ stdin, stdout, stderr = client.exec_command(cmd)
+ out = stdout.read().decode("utf-8", errors="replace").strip()
+ err = stderr.read().decode("utf-8", errors="replace").strip()
+ return out, err
+
+def main():
+ print("=== [第一步] 模拟真实上传:将二级分类图标移入 uploads 目录 ===")
+ os.makedirs(UPLOADS_TARGET_DIR, exist_ok=True)
+
+ # 连接本地数据库
+ local_conn = pymysql.connect(
+ host=LOCAL_DB_HOST,
+ port=LOCAL_DB_PORT,
+ user=DB_USER,
+ password=DB_PASS,
+ database=DB_NAME,
+ charset="utf8mb4",
+ autocommit=True
+ )
+
+ uploaded_files = [] # 保存 (本地绝对路径, 相对远程路径, URL)
+
+ with local_conn.cursor() as cur:
+ for cat_id, filename, cat_name in SUB_CATS:
+ src_path = os.path.join(STATIC_CAT_DIR, filename)
+ if not os.path.exists(src_path):
+ print(f" ✗ 未找到原始图标文件: {src_path}")
+ continue
+
+ with open(src_path, "rb") as f:
+ content = f.read()
+
+ file_md5 = hashlib.md5(content).hexdigest()
+ file_sha1 = hashlib.sha1(content).hexdigest()
+ file_size = len(content)
+
+ dest_filename = f"{file_md5}.png"
+ dest_abs_path = os.path.join(UPLOADS_TARGET_DIR, dest_filename)
+
+ # 写入本地 uploads
+ with open(dest_abs_path, "wb") as f:
+ f.write(content)
+
+ url_path = f"/uploads/{DATE_DIR}/{dest_filename}"
+ uploaded_files.append((dest_abs_path, f"public/uploads/{DATE_DIR}/{dest_filename}", url_path))
+
+ now_ts = int(time.time())
+
+ # 1. 写入/更新本地 fa_attachment
+ cur.execute("SELECT id FROM fa_attachment WHERE url = %s LIMIT 1", (url_path,))
+ att = cur.fetchone()
+ if not att:
+ cur.execute("""
+ INSERT INTO fa_attachment
+ (category, admin_id, user_id, url, imagewidth, imageheight, imagetype, imageframes, filename, filesize, mimetype, createtime, updatetime, uploadtime, storage, sha1)
+ VALUES
+ ('category', 1, 0, %s, 240, 240, 'png', 0, %s, %s, 'image/png', %s, %s, %s, 'local', %s)
+ """, (url_path, filename, file_size, now_ts, now_ts, now_ts, file_sha1))
+
+ # 2. 更新本地 fa_fc_category 记录的 image 字段
+ cur.execute("UPDATE fa_fc_category SET image = %s, updatetime = %s WHERE id = %s", (url_path, now_ts, cat_id))
+ print(f" ✓ 分类 [{cat_name}] (ID:{cat_id}) -> 模拟上传为: {url_path}")
+
+ print("\n=== [第二步] 连接线上服务器并上传文件 (SFTP) ===")
+ ssh = paramiko.SSHClient()
+ ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ ssh.connect(REMOTE_HOST, port=REMOTE_PORT, username=REMOTE_USER, password=REMOTE_PASS, timeout=30)
+ print("✓ 远程 SSH 连接成功")
+
+ # 创建远程 uploads 目录
+ remote_upload_dir = f"{REMOTE_ROOT}/public/uploads/{DATE_DIR}"
+ ssh_exec(ssh, f"mkdir -p {remote_upload_dir}")
+ ssh_exec(ssh, f"mkdir -p {REMOTE_ROOT}/public/static/categories")
+
+ sftp = ssh.open_sftp()
+
+ # 1. 上传新生成的二级分类 uploads 图片
+ print(f"正在上传 {len(uploaded_files)} 个二级分类图标到线上 uploads 目录...")
+ for local_file, remote_rel_file, url in uploaded_files:
+ remote_full = f"{REMOTE_ROOT}/{remote_rel_file}"
+ sftp.put(local_file, remote_full)
+ ssh_exec(ssh, f"chown www:www {remote_full}")
+ print("✓ 二级分类图片已全量上传至线上 uploads 目录")
+
+ # 2. 上传一级分类图标 (cat-job.png, cat-poultry.png)
+ parent_icons = ["cat-job.png", "cat-poultry.png"]
+ for icon_name in parent_icons:
+ loc = os.path.join(STATIC_CAT_DIR, icon_name)
+ if os.path.exists(loc):
+ rem = f"{REMOTE_ROOT}/public/static/categories/{icon_name}"
+ sftp.put(loc, rem)
+ ssh_exec(ssh, f"chown www:www {rem}")
+ print(f" ✓ 已同步一级图标: {icon_name}")
+
+ sftp.close()
+
+ print("\n=== [第三步] 同步本地分类数据与附件记录至线上数据库 ===")
+ # 导出本地分类表 SQL
+ with local_conn.cursor() as cur:
+ cur.execute("SELECT * FROM fa_fc_category;")
+ cat_rows = cur.fetchall()
+ cur.execute("DESCRIBE fa_fc_category;")
+ cat_cols = [f[0] for f in cur.fetchall()]
+
+ # 查询新增的 attachment 记录
+ cur.execute(f"SELECT * FROM fa_attachment WHERE url LIKE '/uploads/{DATE_DIR}/%';")
+ att_rows = cur.fetchall()
+ cur.execute("DESCRIBE fa_attachment;")
+ att_cols = [f[0] for f in cur.fetchall()]
+
+ # 构造同步 SQL
+ sync_sqls = ["SET FOREIGN_KEY_CHECKS = 0;"]
+
+ # 1. 线上清空并全量重写 fa_fc_category,确保和本地 100% 对齐
+ sync_sqls.append("TRUNCATE TABLE fa_fc_category;")
+ col_str = ", ".join([f"`{c}`" for c in cat_cols])
+ for row in cat_rows:
+ val_list = []
+ for v in row:
+ if v is None:
+ val_list.append("NULL")
+ elif isinstance(v, (int, float)):
+ val_list.append(str(v))
+ else:
+ esc = str(v).replace("\\", "\\\\").replace("'", "\\'")
+ val_list.append(f"'{esc}'")
+ sync_sqls.append(f"INSERT INTO fa_fc_category ({col_str}) VALUES ({', '.join(val_list)});")
+
+ # 2. 插入 attachment 记录
+ if att_rows:
+ att_col_str = ", ".join([f"`{c}`" for c in att_cols])
+ for row in att_rows:
+ val_list = []
+ for v in row:
+ if v is None:
+ val_list.append("NULL")
+ elif isinstance(v, (int, float)):
+ val_list.append(str(v))
+ else:
+ esc = str(v).replace("\\", "\\\\").replace("'", "\\'")
+ val_list.append(f"'{esc}'")
+ sync_sqls.append(f"INSERT IGNORE INTO fa_attachment ({att_col_str}) VALUES ({', '.join(val_list)});")
+
+ sync_sqls.append("SET FOREIGN_KEY_CHECKS = 1;")
+ full_sql = "\n".join(sync_sqls)
+
+ # 写入远程执行
+ mysql_cmd = f'mysql -u{DB_USER} -p{DB_PASS} --default-character-set=utf8mb4 {DB_NAME} 2>/dev/null <<\'SQLEOF\'\n{full_sql}\nSQLEOF'
+ out, err = ssh_exec(ssh, mysql_cmd)
+ if err and "error" in err.lower():
+ print(f"✗ 线上数据库更新失败: {err}")
+ else:
+ print("✓ 线上数据库 fa_fc_category 与 fa_attachment 已经完美同步!")
+
+ print("\n=== [第四步] 清理缓存与验证 ===")
+ ssh_exec(ssh, f"rm -rf {REMOTE_ROOT}/runtime/temp/* {REMOTE_ROOT}/runtime/cache/*")
+ print("✓ 线上运行时缓存清理完成")
+
+ # 校验线上分类数量
+ out, _ = ssh_exec(ssh, f'mysql -u{DB_USER} -p{DB_PASS} {DB_NAME} -e "SELECT COUNT(*) FROM fa_fc_category;" 2>/dev/null')
+ print(f" 线上分类总数: {out.strip().split(chr(10))[-1].strip()} 条 (本地: {len(cat_rows)} 条)")
+
+ # 测试线上图片 URL 可访问性
+ sample_url = uploaded_files[0][2]
+ test_img_url = f"https://fenleixinxi.gzyakj.com.cn{sample_url}"
+ cmd_curl = f"curl -s -o /dev/null -w '%{{http_code}}' -k '{test_img_url}'"
+ code, _ = ssh_exec(ssh, cmd_curl)
+ print(f" 线上二级分类上传图片可访问性验证 ({sample_url}): HTTP {code}")
+
+ # 测试线上分类 API
+ api_url = "https://fenleixinxi.gzyakj.com.cn/api/v1/home/categories"
+ cmd_api = f"curl -s -k '{api_url}'"
+ api_res, _ = ssh_exec(ssh, cmd_api)
+ if '"code":1' in api_res and sample_url in api_res:
+ print(" ✓ 线上分类 API 成功返回新二级分类及真实 uploads 图片地址!")
+ else:
+ print(f" API 响应采样: {api_res[:200]}")
+
+ ssh.close()
+ local_conn.close()
+ print("\n✓ 全部同步工作圆满完成!")
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/sync_more_fruits_to_online.py b/tools/sync_more_fruits_to_online.py
new file mode 100644
index 0000000..3951d70
--- /dev/null
+++ b/tools/sync_more_fruits_to_online.py
@@ -0,0 +1,200 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+1. 校验/补齐 7~8 个新增水果二级分类实拍图
+2. 插入本地 fa_attachment 与 fa_fc_category 表 (PID: 2)
+3. SFTP 上传图片到线上服务器
+4. 同步线上数据库 fa_fc_category 与 fa_attachment
+5. 清除缓存并验证
+"""
+
+import os
+import sys
+import io
+import json
+import time
+import paramiko
+import pymysql
+
+sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
+
+LOCAL_ROOT = r"D:\Softwares\phpstudy_pro\WWW\fenleixinxi"
+UPLOADS_DIR = os.path.join(LOCAL_ROOT, "public", "uploads", "20261028")
+JSON_FILE = os.path.join(LOCAL_ROOT, "tools", "more_fruits_result.json")
+
+REMOTE_HOST = "8.137.181.63"
+REMOTE_PORT = 22
+REMOTE_USER = "root"
+REMOTE_PASS = "liyanxia@6688"
+REMOTE_ROOT = "/www/wwwroot/fenleixinxi.gzyakj.com.cn"
+DB_USER = "fenleixinxi"
+DB_PASS = "fenleixinxi"
+DB_NAME = "fenleixinxi"
+
+LOCAL_DB_HOST = "127.0.0.1"
+LOCAL_DB_PORT = 3307
+
+def ssh_exec(client, cmd):
+ stdin, stdout, stderr = client.exec_command(cmd)
+ out = stdout.read().decode("utf-8", errors="replace").strip()
+ err = stderr.read().decode("utf-8", errors="replace").strip()
+ return out, err
+
+def main():
+ with open(JSON_FILE, "r", encoding="utf-8") as f:
+ fruits_list = json.load(f)
+
+ # 过滤出实际存在本地文件且大小 > 0 的项
+ valid_fruits = []
+ for item in fruits_list:
+ local_p = item['local']
+ if os.path.exists(local_p) and os.path.getsize(local_p) > 0:
+ valid_fruits.append(item)
+
+ print(f"=== [1/4] 本地写入新增水果二级分类 ({len(valid_fruits)} 项,PID: 2) ===")
+ conn = pymysql.connect(
+ host=LOCAL_DB_HOST,
+ port=LOCAL_DB_PORT,
+ user=DB_USER,
+ password=DB_PASS,
+ database=DB_NAME,
+ charset="utf8mb4",
+ autocommit=True
+ )
+
+ now_ts = int(time.time())
+ with conn.cursor() as cur:
+ # 获取水果专区当前最大 sort
+ cur.execute("SELECT MAX(sort) FROM fa_fc_category WHERE pid = 2;")
+ max_sort = cur.fetchone()[0] or 100
+
+ for item in valid_fruits:
+ name = item['name']
+ url = item['rel_url']
+ size = item['size']
+ sha1 = item['sha1']
+ filename = item['filename']
+
+ # 1. 检查是否存在该水果分类
+ cur.execute("SELECT id FROM fa_fc_category WHERE pid = 2 AND name = %s LIMIT 1;", (name,))
+ existing = cur.fetchone()
+
+ max_sort += 10
+ if existing:
+ cat_id = existing[0]
+ cur.execute("UPDATE fa_fc_category SET image = %s, enabled = 1, updatetime = %s WHERE id = %s;", (url, now_ts, cat_id))
+ print(f" ✓ 更新水果子分类: [{name}] (ID:{cat_id}) -> {url}")
+ else:
+ cur.execute("""
+ INSERT INTO fa_fc_category
+ (pid, name, icon_key, image, kind, color, sort, enabled, post_count, createtime, updatetime)
+ VALUES
+ (2, %s, '', %s, 'normal', '', %s, 1, 0, %s, %s);
+ """, (name, url, max_sort, now_ts, now_ts))
+ cat_id = cur.lastrowid
+ print(f" ✓ 新增水果子分类成功: [{name}] (ID:{cat_id}) -> {url}")
+
+ # 2. 插入 attachment 记录
+ cur.execute("SELECT id FROM fa_attachment WHERE url = %s LIMIT 1;", (url,))
+ if not cur.fetchone():
+ cur.execute("""
+ INSERT INTO fa_attachment
+ (category, admin_id, user_id, url, imagewidth, imageheight, imagetype, imageframes, filename, filesize, mimetype, createtime, updatetime, uploadtime, storage, sha1)
+ VALUES
+ ('category', 1, 0, %s, 240, 240, 'jpg', 0, %s, %s, 'image/jpeg', %s, %s, %s, 'local', %s);
+ """, (url, filename, size, now_ts, now_ts, now_ts, sha1))
+
+ # 查询当前水果专区所有子分类
+ with conn.cursor() as cur:
+ cur.execute("SELECT id, name, image FROM fa_fc_category WHERE pid = 2 ORDER BY sort, id;")
+ all_fruits = cur.fetchall()
+ print(f"\n本地【水果专区】当前子分类总数: {len(all_fruits)} 个")
+ for f in all_fruits:
+ print(f" * ID:{f[0]:<2} | {f[1]:<6} | {f[2]}")
+
+ # 读取全量 category 表与 attachment 表用于线上同步
+ cur.execute("SELECT * FROM fa_fc_category;")
+ cat_rows = cur.fetchall()
+ cur.execute("DESCRIBE fa_fc_category;")
+ cat_cols = [c[0] for c in cur.fetchall()]
+
+ cur.execute("SELECT * FROM fa_attachment WHERE url LIKE '/uploads/20261028/%.jpg';")
+ att_rows = cur.fetchall()
+ cur.execute("DESCRIBE fa_attachment;")
+ att_cols = [c[0] for c in cur.fetchall()]
+
+ conn.close()
+
+ print("\n=== [2/4] 连接线上服务器并 SFTP 上传新增水果图片 ===")
+ ssh = paramiko.SSHClient()
+ ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ ssh.connect(REMOTE_HOST, port=REMOTE_PORT, username=REMOTE_USER, password=REMOTE_PASS, timeout=30)
+ print("✓ 远程 SSH 连接成功")
+
+ sftp = ssh.open_sftp()
+ for item in valid_fruits:
+ local_p = item['local']
+ rel_url = item['rel_url'].lstrip('/')
+ remote_p = f"{REMOTE_ROOT}/public/{rel_url}"
+
+ sftp.put(local_p, remote_p)
+ ssh_exec(ssh, f"chown www:www {remote_p}")
+ print(f" ✓ SFTP 上传成功: {remote_p}")
+
+ sftp.close()
+
+ print("\n=== [3/4] 同步更新线上数据库 fa_fc_category 与 fa_attachment ===")
+ sync_sqls = ["SET FOREIGN_KEY_CHECKS = 0;"]
+ sync_sqls.append("TRUNCATE TABLE fa_fc_category;")
+ col_str = ", ".join([f"`{c}`" for c in cat_cols])
+ for row in cat_rows:
+ val_list = []
+ for v in row:
+ if v is None:
+ val_list.append("NULL")
+ elif isinstance(v, (int, float)):
+ val_list.append(str(v))
+ else:
+ esc = str(v).replace("\\", "\\\\").replace("'", "\\'")
+ val_list.append(f"'{esc}'")
+ sync_sqls.append(f"INSERT INTO fa_fc_category ({col_str}) VALUES ({', '.join(val_list)});")
+
+ if att_rows:
+ att_col_str = ", ".join([f"`{c}`" for c in att_cols])
+ for row in att_rows:
+ val_list = []
+ for v in row:
+ if v is None:
+ val_list.append("NULL")
+ elif isinstance(v, (int, float)):
+ val_list.append(str(v))
+ else:
+ esc = str(v).replace("\\", "\\\\").replace("'", "\\'")
+ val_list.append(f"'{esc}'")
+ sync_sqls.append(f"INSERT IGNORE INTO fa_attachment ({att_col_str}) VALUES ({', '.join(val_list)});")
+
+ sync_sqls.append("SET FOREIGN_KEY_CHECKS = 1;")
+ full_sql = "\n".join(sync_sqls)
+
+ mysql_cmd = f'mysql -u{DB_USER} -p{DB_PASS} --default-character-set=utf8mb4 {DB_NAME} 2>/dev/null <<\'SQLEOF\'\n{full_sql}\nSQLEOF'
+ out, err = ssh_exec(ssh, mysql_cmd)
+ if err and "error" in err.lower():
+ print(f"✗ 线上数据库更新失败: {err}")
+ else:
+ print("✓ 线上数据库 fa_fc_category 与 fa_attachment 已经完美同步!")
+
+ print("\n=== [4/4] 清除线上缓存并验证接口 ===")
+ ssh_exec(ssh, f"rm -rf {REMOTE_ROOT}/runtime/temp/* {REMOTE_ROOT}/runtime/cache/*")
+
+ cmd_api = "curl -s -k 'https://fenleixinxi.gzyakj.com.cn/api/v1/home/categories'"
+ api_res, _ = ssh_exec(ssh, cmd_api)
+ if "芒果" in api_res and "香蕉" in api_res:
+ print(" ✓ 线上分类 API 成功返回新增水果二级分类!")
+ else:
+ print(" API 返回未找到新增水果")
+
+ ssh.close()
+ print("\n✓ 水果专区二级分类扩充及线上同步全部圆满完成!")
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/sync_real_photos_to_online.py b/tools/sync_real_photos_to_online.py
new file mode 100644
index 0000000..a7526dc
--- /dev/null
+++ b/tools/sync_real_photos_to_online.py
@@ -0,0 +1,178 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+将 13 张真实商业摄影实拍图更新至本地与线上数据库,并 SFTP 上传至线上服务器
+"""
+
+import os
+import sys
+import io
+import json
+import time
+import paramiko
+import pymysql
+
+sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
+
+LOCAL_ROOT = r"D:\Softwares\phpstudy_pro\WWW\fenleixinxi"
+JSON_FILE = os.path.join(LOCAL_ROOT, "tools", "real_photos_result.json")
+
+REMOTE_HOST = "8.137.181.63"
+REMOTE_PORT = 22
+REMOTE_USER = "root"
+REMOTE_PASS = "liyanxia@6688"
+REMOTE_ROOT = "/www/wwwroot/fenleixinxi.gzyakj.com.cn"
+DB_USER = "fenleixinxi"
+DB_PASS = "fenleixinxi"
+DB_NAME = "fenleixinxi"
+
+LOCAL_DB_HOST = "127.0.0.1"
+LOCAL_DB_PORT = 3307
+
+def ssh_exec(client, cmd):
+ stdin, stdout, stderr = client.exec_command(cmd)
+ out = stdout.read().decode("utf-8", errors="replace").strip()
+ err = stderr.read().decode("utf-8", errors="replace").strip()
+ return out, err
+
+def main():
+ with open(JSON_FILE, "r", encoding="utf-8") as f:
+ photo_list = json.load(f)
+
+ print(f"=== [1/4] 更新本地数据库 fa_fc_category 与 fa_attachment ({len(photo_list)} 项) ===")
+ conn = pymysql.connect(
+ host=LOCAL_DB_HOST,
+ port=LOCAL_DB_PORT,
+ user=DB_USER,
+ password=DB_PASS,
+ database=DB_NAME,
+ charset="utf8mb4",
+ autocommit=True
+ )
+
+ with conn.cursor() as cur:
+ now_ts = int(time.time())
+ for item in photo_list:
+ cat_id = item['id']
+ url = item['rel_url']
+ size = item['size']
+ sha1 = item['sha1']
+ filename = item['filename']
+
+ # 更新分类表
+ cur.execute("UPDATE fa_fc_category SET image = %s, updatetime = %s WHERE id = %s", (url, now_ts, cat_id))
+
+ # 插入/更新 attachment 表
+ cur.execute("SELECT id FROM fa_attachment WHERE url = %s LIMIT 1", (url,))
+ att = cur.fetchone()
+ if not att:
+ cur.execute("""
+ INSERT INTO fa_attachment
+ (category, admin_id, user_id, url, imagewidth, imageheight, imagetype, imageframes, filename, filesize, mimetype, createtime, updatetime, uploadtime, storage, sha1)
+ VALUES
+ ('category', 1, 0, %s, 240, 240, 'jpg', 0, %s, %s, 'image/jpeg', %s, %s, %s, 'local', %s)
+ """, (url, filename, size, now_ts, now_ts, now_ts, sha1))
+
+ print(f" ✓ 本地更新: [{item['name']}] (ID:{cat_id}) -> {url}")
+
+ # 读取本地最新 category 和 attachment
+ with conn.cursor() as cur:
+ cur.execute("SELECT * FROM fa_fc_category;")
+ cat_rows = cur.fetchall()
+ cur.execute("DESCRIBE fa_fc_category;")
+ cat_cols = [f[0] for f in cur.fetchall()]
+
+ cur.execute("SELECT * FROM fa_attachment WHERE url LIKE '/uploads/20261028/%.jpg';")
+ att_rows = cur.fetchall()
+ cur.execute("DESCRIBE fa_attachment;")
+ att_cols = [f[0] for f in cur.fetchall()]
+
+ conn.close()
+
+ print("\n=== [2/4] 连接线上服务器并通过 SFTP 上传真实照片文件 ===")
+ ssh = paramiko.SSHClient()
+ ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ ssh.connect(REMOTE_HOST, port=REMOTE_PORT, username=REMOTE_USER, password=REMOTE_PASS, timeout=30)
+ print("✓ 远程 SSH 连接成功")
+
+ sftp = ssh.open_sftp()
+ remote_upload_dir = f"{REMOTE_ROOT}/public/uploads/20261028"
+ ssh_exec(ssh, f"mkdir -p {remote_upload_dir}")
+
+ for item in photo_list:
+ local_path = item['local']
+ rel_url = item['rel_url'].lstrip('/')
+ remote_path = f"{REMOTE_ROOT}/public/{rel_url}"
+
+ sftp.put(local_path, remote_path)
+ ssh_exec(ssh, f"chown www:www {remote_path}")
+ print(f" ✓ SFTP 已上传: {remote_path} ({os.path.basename(local_path)})")
+
+ sftp.close()
+
+ print("\n=== [3/4] 同步更新线上数据库 fa_fc_category 与 fa_attachment ===")
+ sync_sqls = ["SET FOREIGN_KEY_CHECKS = 0;"]
+
+ # 全量同步分类
+ sync_sqls.append("TRUNCATE TABLE fa_fc_category;")
+ col_str = ", ".join([f"`{c}`" for c in cat_cols])
+ for row in cat_rows:
+ val_list = []
+ for v in row:
+ if v is None:
+ val_list.append("NULL")
+ elif isinstance(v, (int, float)):
+ val_list.append(str(v))
+ else:
+ esc = str(v).replace("\\", "\\\\").replace("'", "\\'")
+ val_list.append(f"'{esc}'")
+ sync_sqls.append(f"INSERT INTO fa_fc_category ({col_str}) VALUES ({', '.join(val_list)});")
+
+ # 插入 attachment
+ if att_rows:
+ att_col_str = ", ".join([f"`{c}`" for c in att_cols])
+ for row in att_rows:
+ val_list = []
+ for v in row:
+ if v is None:
+ val_list.append("NULL")
+ elif isinstance(v, (int, float)):
+ val_list.append(str(v))
+ else:
+ esc = str(v).replace("\\", "\\\\").replace("'", "\\'")
+ val_list.append(f"'{esc}'")
+ sync_sqls.append(f"INSERT IGNORE INTO fa_attachment ({att_col_str}) VALUES ({', '.join(val_list)});")
+
+ sync_sqls.append("SET FOREIGN_KEY_CHECKS = 1;")
+ full_sql = "\n".join(sync_sqls)
+
+ mysql_cmd = f'mysql -u{DB_USER} -p{DB_PASS} --default-character-set=utf8mb4 {DB_NAME} 2>/dev/null <<\'SQLEOF\'\n{full_sql}\nSQLEOF'
+ out, err = ssh_exec(ssh, mysql_cmd)
+ if err and "error" in err.lower():
+ print(f"✗ 线上数据库更新失败: {err}")
+ else:
+ print("✓ 线上数据库已更新全部真实实拍照路径与附件数据!")
+
+ print("\n=== [4/4] 清理缓存并校验真实实拍照访问 ===")
+ ssh_exec(ssh, f"rm -rf {REMOTE_ROOT}/runtime/temp/* {REMOTE_ROOT}/runtime/cache/*")
+
+ # 抽样验证 2 张真实照片的 HTTP 返回码
+ for sample in photo_list[:2]:
+ test_url = f"https://fenleixinxi.gzyakj.com.cn{sample['rel_url']}"
+ cmd_curl = f"curl -s -o /dev/null -w '%{{http_code}}' -k '{test_url}'"
+ code, _ = ssh_exec(ssh, cmd_curl)
+ print(f" 真实照片 [{sample['name']}] HTTP 校验 ({test_url}): HTTP {code}")
+
+ # 线上分类接口校验
+ cmd_api = "curl -s -k 'https://fenleixinxi.gzyakj.com.cn/api/v1/home/categories'"
+ api_res, _ = ssh_exec(ssh, cmd_api)
+ if photo_list[0]['rel_url'] in api_res:
+ print(" ✓ 线上分类接口已成功返回最新真实实拍照 URL!")
+ else:
+ print(f" 接口返回采样: {api_res[:200]}")
+
+ ssh.close()
+ print("\n✓ 真实照片更新与线上同步圆满完成!")
+
+if __name__ == "__main__":
+ main()
diff --git a/uniapp/components/fc-call-modal/fc-call-modal.vue b/uniapp/components/fc-call-modal/fc-call-modal.vue
index 49d05fc..56111d9 100644
--- a/uniapp/components/fc-call-modal/fc-call-modal.vue
+++ b/uniapp/components/fc-call-modal/fc-call-modal.vue
@@ -13,19 +13,29 @@
{{ contactLabel }}
-
+
{{ quotaText }}
+
+
+
+
+ 开通同城 VIP 会员
+ 每日无限次拨打 · 免费浏览 · 极速发布
+
+ 去开通 ›
+
+
请自行鉴别农产品信息真伪,在未核实清楚前不要提前给对方转账,慎防诈骗!
-
-
+
+
✓
@@ -33,14 +43,17 @@
《平台免责声明》
-
+
-
- {{ loading ? '连接中…' : '立即拨打' }}
+
+ {{ isQuotaEmpty ? '开通会员立即拨打' : (loading ? '连接中…' : '立即拨打') }}
@@ -83,12 +96,25 @@ export default {
},
},
computed: {
+ /**
+ * 联系对象名称脱敏或提示
+ */
contactLabel() {
return this.contactName ? `联系 ${this.contactName}` : '确认后直接拨打'
},
+ /**
+ * 额度是否已用尽
+ */
+ isQuotaEmpty() {
+ return !this.unlimited && Number(this.quotaTotal) > 0 && Number(this.quotaLeft) <= 0
+ },
+ /**
+ * 额度展示文案
+ */
quotaText() {
if (this.unlimited || this.quotaTotal === 0) return '会员尊享 · 不限拨打次数'
const left = Math.max(0, Number(this.quotaLeft) || 0)
+ if (left <= 0) return '今日拨打额度已用完(0 次)'
return `今日剩余 ${left} 次拨打机会`
},
},
@@ -101,9 +127,13 @@ export default {
this.$emit('close')
},
/**
- * 点击拨打:先校验免责声明勾选,再交由父级执行拨打接口
+ * 点击拨打或引导开通会员
*/
handleDial() {
+ if (this.isQuotaEmpty) {
+ this.goMember()
+ return
+ }
if (this.loading) return
if (!this.agree) {
uni.showToast({ title: '请先勾选同意平台免责声明', icon: 'none' })
@@ -111,6 +141,13 @@ export default {
}
this.$emit('confirm')
},
+ /**
+ * 跳转会员中心开通特权
+ */
+ goMember() {
+ this.handleClose()
+ uni.navigateTo({ url: '/pages/member/index' })
+ },
/**
* 查看平台免责声明详情
*/
@@ -185,6 +222,44 @@ export default {
margin-top: 12rpx;
font-size: 22rpx;
color: #64748b;
+
+ &--warn {
+ color: #e65100;
+ font-weight: 700;
+ }
+}
+
+.call-modal__vip-box {
+ margin-top: 20rpx;
+ padding: 18rpx 20rpx;
+ border-radius: 18rpx;
+ background: linear-gradient(135deg, #f0fdf4, #fff8e8);
+ border: 1rpx solid #ffe3a6;
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+}
+.call-modal__vip-info {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+}
+.call-modal__vip-title {
+ font-size: 26rpx;
+ font-weight: 700;
+ color: #8a5a00;
+}
+.call-modal__vip-desc {
+ margin-top: 4rpx;
+ font-size: 20rpx;
+ color: #b45309;
+}
+.call-modal__vip-arrow {
+ font-size: 24rpx;
+ font-weight: 700;
+ color: #f5a623;
+ flex-shrink: 0;
}
.call-modal__warn {
@@ -266,5 +341,11 @@ export default {
&--disabled {
opacity: 0.6;
}
+
+ &--vip {
+ background: linear-gradient(135deg, #00e070, #00d26a 45%, #f5a623);
+ color: #04301e;
+ box-shadow: 0 8rpx 24rpx rgba(245, 166, 35, 0.35);
+ }
}
diff --git a/uniapp/pages/category/index.vue b/uniapp/pages/category/index.vue
index ad716c7..aa2d2be 100644
--- a/uniapp/pages/category/index.vue
+++ b/uniapp/pages/category/index.vue
@@ -69,7 +69,7 @@
- 全部{{ currentCategory ? currentCategory.name : '' }}
+ 全部
+
+
+
+
+
+
+
+
+ 开通同城 VIP 会员
+ 尊享特权
+
+ 每日免费浏览 · 无限拨打电话 · 专属尊贵标识
+
+
+
+ 立即开通
+ ›
+
+
+
+
+
+
+
+
+
+ VIP 尊享特权生效中
+ VIP 会员
+
+ 您已享今日免费浏览与不限拨打电话特权
+
+
+
+ 会员中心
+ ›
+
+
+
@@ -317,23 +355,37 @@
-
-
-
- {{ faved ? '已收藏' : '收藏' }}
+
+
+
+
+ 今日拨打次数已用完,开通 VIP 尊享不限次拨打
+ 去开通 ›
-
-
-
-
-
- 海报
-
-
- {{ bottomBarText }}
+
+
+
+
+ {{ faved ? '已收藏' : '收藏' }}
+
+
+
+
+
+
+ 海报
+
+
+
+ {{ bottomBarText }}
+
@@ -563,12 +615,23 @@ export default {
callQuotaUnlimited() {
return Number(this.callQuotaTotal) === 0
},
+ /**
+ * 今日拨打额度是否已用完(非不限次数且总额度>0,剩余额度<=0)
+ * @returns {boolean}
+ */
+ isCallQuotaOut() {
+ if (this.callQuotaUnlimited) return false
+ return Number(this.callQuotaTotal) > 0 && Number(this.callQuotaLeft) <= 0
+ },
/**
* 底部主操作栏文案
* @returns {string}
*/
bottomBarText() {
if (this.loading && !this.loaded) return '加载中…'
+ if (this.isCallQuotaOut) {
+ return '开通会员立即拨打'
+ }
return '拨打电话'
},
timeText() {
@@ -828,10 +891,31 @@ export default {
} catch (e) {}
},
/**
- * 底部主按钮响应:打开免责声明拨打弹窗
+ * 底部主按钮响应:拨打电话或引导开通会员
*/
onMainAction() {
if (this.loading && !this.loaded) return
+ if (!this.hasPhone) {
+ uni.showToast({ title: '发布者未预留联系电话', icon: 'none' })
+ return
+ }
+ if (!this.ensureLogin()) return
+ // 当额度用尽时直接引导开通会员,杜绝无效点击
+ if (this.isCallQuotaOut) {
+ uni.showModal({
+ title: '今日拨打次数已用完',
+ content: this.isMember
+ ? '您今日的 VIP 拨打次数已达上限,明日将自动恢复,或可升级更高规格会员套餐'
+ : '您今日的免费拨打次数已用完,开通 VIP 会员尊享无限次拨打与每日免费浏览特权',
+ confirmText: '去开通会员',
+ cancelText: '知道了',
+ confirmColor: '#00E070',
+ success: (r) => {
+ if (r.confirm) uni.navigateTo({ url: '/pages/member/index' })
+ },
+ })
+ return
+ }
this.openCallModal()
},
/**
@@ -936,8 +1020,9 @@ export default {
}
} catch (e) {
const msg = (e && e.message) || '拨打失败,请稍后重试'
- // 当日拨打次数超限(后端 403):引导开通 VIP 会员提升每日拨打次数
- if (/上限/.test(msg)) {
+ // 当日拨打次数超限(后端 403):同步本地额度并引导开通 VIP 会员提升每日拨打次数
+ if (/上限|额度|次数/.test(msg)) {
+ this.callQuotaLeft = 0
this.showCallModal = false
uni.showModal({
title: '今日拨打次数已用完',
@@ -2497,12 +2582,35 @@ export default {
.detail__bar-space {
height: calc(150rpx + env(safe-area-inset-bottom));
}
-.detail__bar {
+.detail__bar-wrapper {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 99;
+}
+.detail__bar-quota-tip {
+ background: linear-gradient(90deg, #fffbeb, #fef3c7);
+ border-top: 1rpx solid #fde68a;
+ padding: 12rpx 28rpx;
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+}
+.detail__bar-quota-txt {
+ flex: 1;
+ font-size: 22rpx;
+ color: #92400e;
+ font-weight: 500;
+}
+.detail__bar-quota-go {
+ font-size: 22rpx;
+ color: #d97706;
+ font-weight: 700;
+ flex-shrink: 0;
+}
+.detail__bar {
+ position: relative;
display: flex;
align-items: center;
padding: 14rpx 24rpx calc(14rpx + env(safe-area-inset-bottom));
@@ -2550,7 +2658,123 @@ export default {
display: flex;
align-items: center;
justify-content: center;
+ gap: 10rpx;
box-shadow: 0 6rpx 20rpx rgba(0, 224, 112, 0.35);
+
+ &--vip {
+ background: linear-gradient(135deg, #00e070, #00d26a 45%, #f5a623) !important;
+ color: #04301e !important;
+ box-shadow: 0 8rpx 24rpx rgba(245, 166, 35, 0.32) !important;
+ }
+}
+.detail__bar-vip-icon {
+ width: 32rpx;
+ height: 32rpx;
+}
+
+/* 详情预览页:同城 VIP 会员特权卡片 */
+.detail__vip-card {
+ margin: 20rpx 24rpx 0;
+ padding: 24rpx 26rpx;
+ background: linear-gradient(135deg, #f0fdf4, #ffffff 60%, #fffbf0);
+ border: 2rpx solid #d4f3e3;
+ border-radius: 24rpx;
+ box-shadow: 0 4rpx 16rpx rgba(0, 168, 84, 0.05);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ &--active {
+ background: linear-gradient(135deg, #f7fee7, #ffffff 60%, #ecfdf5);
+ border-color: #bef264;
+ }
+}
+.detail__vip-card-left {
+ display: flex;
+ align-items: center;
+ gap: 18rpx;
+ flex: 1;
+ min-width: 0;
+}
+.detail__vip-card-badge {
+ width: 64rpx;
+ height: 64rpx;
+ border-radius: 18rpx;
+ background: linear-gradient(135deg, #fef3c7, #fffbeb);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ box-shadow: 0 2rpx 8rpx rgba(245, 166, 35, 0.15);
+
+ &--active {
+ background: linear-gradient(135deg, #dcfce7, #f0fdf4);
+ }
+}
+.detail__vip-card-info {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+}
+.detail__vip-card-title-row {
+ display: flex;
+ align-items: center;
+ gap: 10rpx;
+}
+.detail__vip-card-title {
+ font-size: 28rpx;
+ font-weight: 700;
+ color: #1e293b;
+}
+.detail__vip-card-pill {
+ padding: 2rpx 12rpx;
+ border-radius: 999rpx;
+ background: linear-gradient(135deg, #f5a623, #d97706);
+ color: #ffffff;
+ font-size: 20rpx;
+ font-weight: 700;
+
+ &--active {
+ background: linear-gradient(135deg, #00e070, #00d26a);
+ }
+}
+.detail__vip-card-desc {
+ margin-top: 6rpx;
+ font-size: 22rpx;
+ color: #64748b;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.detail__vip-card-btn {
+ flex-shrink: 0;
+ margin-left: 16rpx;
+ padding: 10rpx 22rpx;
+ border-radius: 999rpx;
+ background: #ffffff;
+ border: 1rpx solid #00a854;
+ display: flex;
+ align-items: center;
+ gap: 4rpx;
+ box-shadow: 0 2rpx 8rpx rgba(0, 168, 84, 0.08);
+
+ &--active {
+ border-color: #00d26a;
+ }
+}
+.detail__vip-card-btn-txt {
+ font-size: 24rpx;
+ font-weight: 700;
+ color: #00a854;
+}
+.detail__vip-card-btn-arrow {
+ font-size: 22rpx;
+ font-weight: 700;
+ color: #00a854;
+}
+.detail__vip-card:active {
+ opacity: 0.88;
}
/* 通用弹层遮罩(评价 / 回复 / 海报等底部弹层共用) */
diff --git a/uniapp/pages/merchant/detail.vue b/uniapp/pages/merchant/detail.vue
index 9118446..aeedf81 100644
--- a/uniapp/pages/merchant/detail.vue
+++ b/uniapp/pages/merchant/detail.vue
@@ -152,9 +152,13 @@
地图导航
-
-
- 联系店长
+
+
+ {{ isCallQuotaOut ? '开通会员拨打' : '联系店长' }}
@@ -252,6 +256,14 @@ export default {
callQuotaUnlimited() {
return Number(this.callQuotaTotal) === 0
},
+ /**
+ * 今日拨打额度是否已用完
+ * @returns {boolean}
+ */
+ isCallQuotaOut() {
+ if (this.callQuotaUnlimited) return false
+ return Number(this.callQuotaTotal) > 0 && Number(this.callQuotaLeft) <= 0
+ },
/**
* 免责弹窗拨打展示文案(联系人姓氏 + 脱敏手机号)
* @returns {string}
@@ -477,6 +489,22 @@ export default {
return
}
} catch (e) {}
+
+ // 拨打次数已用完直接引导开通会员
+ if (this.isCallQuotaOut) {
+ uni.showModal({
+ title: '今日拨打次数已用完',
+ content: '您今日的免费拨打次数已用完,开通 VIP 会员尊享每日更多拨打特权',
+ confirmText: '去开通会员',
+ cancelText: '知道了',
+ confirmColor: '#00E070',
+ success: (r) => {
+ if (r.confirm) uni.navigateTo({ url: '/pages/member/index' })
+ },
+ })
+ return
+ }
+
this.showCallModal = true
},
/**
@@ -499,7 +527,8 @@ export default {
} catch (e) {
const msg = (e && e.message) || '拨打失败,请稍后重试'
// 当日拨打次数超限(后端 403):引导开通 VIP 会员提升每日拨打次数
- if (/上限/.test(msg)) {
+ if (/上限|额度|次数/.test(msg)) {
+ this.callQuotaLeft = 0
this.showCallModal = false
uni.showModal({
title: '今日拨打次数已用完',
@@ -883,5 +912,11 @@ export default {
transform: scale(0.98);
opacity: 0.9;
}
+
+ &--vip {
+ background: linear-gradient(135deg, #00e070, #00d26a 45%, #f5a623) !important;
+ color: #04301e !important;
+ box-shadow: 0 8rpx 24rpx rgba(245, 166, 35, 0.32) !important;
+ }
}
diff --git a/uniapp/pages/post/create.vue b/uniapp/pages/post/create.vue
index ce978c6..102f36b 100644
--- a/uniapp/pages/post/create.vue
+++ b/uniapp/pages/post/create.vue
@@ -3,16 +3,6 @@
-
-
-
-
- 开通会员
- 每日免费发布、免费浏览、更多拨打次数
-
- 去开通 ›
-
-
@@ -33,12 +23,12 @@
产品品类
- *
+ *
-
-
- {{ currentSubCat.name }}
+
+
+ {{ selectedSubCatDisplay.name }}
{{ currentCat ? '请选择品类' : '请先选择大类' }}
›
@@ -313,6 +303,23 @@
+
+
+
+
+
+ {{ isMember ? 'VIP 会员发帖特权生效中' : '开通同城 VIP 会员' }}
+ 已开通
+
+
+ {{ isMember ? '尊享每日海量免费发布、专属身份认证与极速展示' : '尊享每日免费发布、信息置顶折扣、更多拨打次数' }}
+
+
+
+ {{ isMember ? '权益中心 ›' : '去开通 ›' }}
+
+
+
@@ -446,7 +453,7 @@
- 全部{{ currentCat ? currentCat.name : '' }}
+ 全部
✓
@@ -520,6 +527,7 @@ export default {
catDrawerAnim: false,
// 分类两步选择:1 = 选一级大类,2 = 选二级品类
catDrawerStep: 1,
+ selectedAllSubCat: false, // 是否显式选中全部二级品类
customTagInput: '',
outputRangeOptions: [
'1000斤以下',
@@ -661,6 +669,35 @@ export default {
const children = (this.currentCat && this.currentCat.children) || []
return children.find((s) => Number(s.id) === Number(this.form.subCategoryId)) || null
},
+ /**
+ * 当前一级大类是否包含二级子品类
+ * @returns {boolean}
+ */
+ hasSubCategories() {
+ return !!(this.currentCat && Array.isArray(this.currentCat.children) && this.currentCat.children.length > 0)
+ },
+ /**
+ * 页面回显的产品品类信息(支持具体二级分类对象或全部)
+ * @returns {{ name: string, icon: string } | null}
+ */
+ selectedSubCatDisplay() {
+ if (!this.currentCat) return null
+ // 1. 若选了具体二级分类
+ if (this.currentSubCat) {
+ return {
+ name: this.currentSubCat.name,
+ icon: this.catIcon(this.currentSubCat),
+ }
+ }
+ // 2. 若该一级大类本身没有二级分类,或者显式选择了全部
+ if (!this.hasSubCategories || this.selectedAllSubCat) {
+ return {
+ name: '全部',
+ icon: this.catIcon(this.currentCat),
+ }
+ }
+ return null
+ },
/**
* 当前选中分类是否为农产品类(kind=farm):
* 决定「种植面积/采摘时间/预计量产」的显隐与必填校验;历史数据无 kind 时按农产品处理
@@ -1153,26 +1190,35 @@ export default {
}, 200)
},
/**
- * 选择一级大类(蔬菜 / 水果),选中即回填并关闭
+ * 选择一级大类(蔬菜 / 水果等),选中即回填并关闭
* @param {Object} c 一级大类对象
*/
onSelectDrawerParentCat(c) {
if (!c) return
this.form.categoryId = Number(c.id)
- this.form.subCategoryId = 0
+ const children = (c.children && Array.isArray(c.children)) ? c.children : []
+ if (!children.length) {
+ // 若该一级分类下无二级子品类,自动归为全部
+ this.form.subCategoryId = 0
+ this.selectedAllSubCat = true
+ } else {
+ this.form.subCategoryId = 0
+ this.selectedAllSubCat = false
+ }
this.triggerAutoSave()
this.closeCatDrawer()
},
/**
- * 第二步:仅按一级大类发布(不选具体二级品类)
+ * 第二步:仅按一级大类发布(选择全部)
*/
onSelectDrawerParentOnly() {
this.form.subCategoryId = 0
+ this.selectedAllSubCat = true
this.triggerAutoSave()
this.closeCatDrawer()
},
/**
- * 第二步:选中二级分类(如:花菜、白菜、紫甘蓝、青甘蓝、青菜等)并完成选择
+ * 第二步:选中二级分类并完成选择
* @param {Object} sub 二级分类对象
*/
onSelectDrawerSubCat(sub) {
@@ -1181,6 +1227,7 @@ export default {
this.form.categoryId = Number(sub.pid)
}
this.form.subCategoryId = Number(sub.id)
+ this.selectedAllSubCat = false
this.triggerAutoSave()
this.closeCatDrawer()
},
@@ -1415,8 +1462,8 @@ export default {
* @returns {string} 错误信息,为空则验证通过
*/
validate() {
- if (!this.form.categoryId) return '请选择所属分类'
- if (!this.form.subCategoryId && this.drawerSubCategories.length) return '请选择二级品类'
+ if (!this.form.categoryId) return '请选择所属大类'
+ if (this.hasSubCategories && !this.form.subCategoryId) return '请选择产品品类'
if (!this.form.title || !this.form.title.trim()) return '请填写标题'
if (!this.form.content || !this.form.content.trim()) return '请填写详细内容'
// 校验顺序与表单视觉顺序一致(价格 → 产地规格 → 区域 → 手机号),报错时用户可自上而下定位
@@ -1562,15 +1609,21 @@ export default {
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
}
-/* 非会员开通引导条(仅未开通会员可见) */
+/* 会员开通/权益引导条(置于表单底部,自然顺畅) */
.create__member-banner {
- margin: 20rpx 28rpx 0;
- padding: 22rpx 26rpx;
+ margin: 24rpx 28rpx 0;
+ padding: 24rpx 26rpx;
display: flex;
align-items: center;
- background: linear-gradient(135deg, #fff8e8, #fffdf5);
- border: 1rpx solid #ffe3a6;
- border-radius: 22rpx;
+ background: linear-gradient(135deg, #f0fdf4, #ffffff 60%, #fffbf0);
+ border: 2rpx solid #d4f3e3;
+ border-radius: 24rpx;
+ box-shadow: 0 4rpx 16rpx rgba(0, 168, 84, 0.05);
+
+ &--vip {
+ background: linear-gradient(135deg, #f7fee7, #ffffff 60%, #ecfdf5);
+ border-color: #bef264;
+ }
}
.create__member-icon {
width: 56rpx;
@@ -1584,28 +1637,48 @@ export default {
display: flex;
flex-direction: column;
}
+.create__member-title-row {
+ display: flex;
+ align-items: center;
+}
.create__member-title {
font-size: 28rpx;
font-weight: 700;
- color: #8a5a00;
+ color: #1e293b;
+}
+.create__member-tag {
+ margin-left: 12rpx;
+ padding: 2rpx 12rpx;
+ border-radius: 999rpx;
+ background: linear-gradient(135deg, #00e070, #00d26a);
+ color: #ffffff;
+ font-size: 20rpx;
+ font-weight: 600;
}
.create__member-desc {
- margin-top: 4rpx;
+ margin-top: 6rpx;
font-size: 22rpx;
- color: #b08a3e;
+ color: #64748b;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
-.create__member-go {
+.create__member-btn-box {
flex-shrink: 0;
margin-left: 16rpx;
+ padding: 8rpx 22rpx;
+ border-radius: 999rpx;
+ background: #ffffff;
+ border: 1rpx solid #00a854;
+ box-shadow: 0 2rpx 8rpx rgba(0, 168, 84, 0.08);
+}
+.create__member-go {
font-size: 24rpx;
font-weight: 700;
- color: #f5a623;
+ color: #00a854;
}
.create__member-banner:active {
- opacity: 0.85;
+ opacity: 0.88;
}
/* 表单卡片区域 */
diff --git a/uniapp/static/categories/cat-job.png b/uniapp/static/categories/cat-job.png
new file mode 100644
index 0000000..1479d09
Binary files /dev/null and b/uniapp/static/categories/cat-job.png differ
diff --git a/uniapp/static/categories/cat-poultry.png b/uniapp/static/categories/cat-poultry.png
new file mode 100644
index 0000000..01a86e7
Binary files /dev/null and b/uniapp/static/categories/cat-poultry.png differ
diff --git a/uniapp/static/icons/cat-poultry.png b/uniapp/static/icons/cat-poultry.png
new file mode 100644
index 0000000..01a86e7
Binary files /dev/null and b/uniapp/static/icons/cat-poultry.png differ