diff --git a/application/admin/controller/fc/Category.php b/application/admin/controller/fc/Category.php index b223417..043cf68 100644 --- a/application/admin/controller/fc/Category.php +++ b/application/admin/controller/fc/Category.php @@ -6,7 +6,7 @@ use app\common\controller\Backend; use think\Db; /** - * 同城-分类管理(支持一级分类「蔬菜/水果」与带实拍图的二级分类) + * 同城-分类管理(支持一级主分类与各二级子品类) * * @icon fa fa-circle-o */ @@ -125,19 +125,32 @@ class Category extends Backend } /** - * 编辑分类(自动同步 image 与 icon_key) + * 编辑分类(自动补全图片路径并同步 image 与 icon_key) * * @param mixed $ids * @return string */ public function edit($ids = null) { + $row = $this->model->get($ids); + if (!$row) { + $this->error(__('No Results were found')); + } + $adminIds = $this->getDataLimitAdminIds(); + if (is_array($adminIds) && !in_array($row[$this->dataLimitField], $adminIds)) { + $this->error(__('You have no permission')); + } if ($this->request->isPost()) { $params = $this->request->post('row/a', []); $params = $this->normalizeCategoryParams($params, (int)$ids); $this->request->post(['row' => $params]); + return parent::edit($ids); } - return parent::edit($ids); + if (empty($row['image']) && !empty($row['icon_key'])) { + $row['image'] = $this->resolveImageFromKey($row['icon_key']); + } + $this->view->assign("row", $row); + return $this->view->fetch(); } /** @@ -193,7 +206,7 @@ class Category extends Backend { $iconKey = trim((string)$iconKey); if ($iconKey === '') { - return '/static/categories/cat-veg.png'; + return '/static/icons/cat-service.png'; } if (preg_match('/^(\/uploads\/|\/static\/|https?:\/\/)/i', $iconKey)) { return $iconKey; diff --git a/application/admin/view/fc/category/add.html b/application/admin/view/fc/category/add.html index a1e260e..1293630 100644 --- a/application/admin/view/fc/category/add.html +++ b/application/admin/view/fc/category/add.html @@ -1,9 +1,9 @@ @@ -323,13 +514,13 @@
归属与类型 - 决定发布页展示哪些字段 + 决定前台分类层级与发布表单
{:build_select('row[pid]', $parentList, 0, ['class'=>'form-control selectpicker', 'id'=>'c-pid', 'data-rule'=>'required'])} - 选「顶级分类」创建一级大类;选「蔬菜」「水果」则创建其下属二级品类。 + 选「顶级分类」创建一级大类频道;选指定分类(如「招聘专区」「肉蛋家禽」)则创建下属二级品类。
@@ -337,12 +528,12 @@
二级分类自动跟随上级分类的类型,无需单独设置。 @@ -358,27 +549,31 @@
- +
- +
- 当前图片 + 当前图标 +
+ 招聘专区 + 用于前台金刚区及分类列表展示 +
- 优先上传真实农产品照片,农民用户靠「看图识物」选择品类;建议 500×500 以上方形图,单张不超过 500KB,过大的图片会拖慢小程序加载。 - + 推荐从平台预设图片库中直接选用;也支持上传高清实拍方形图(建议 200×200 以上,单张不超过 500KB)。 +
@@ -399,11 +594,11 @@
推荐:
+
-
-
-
+
+
@@ -432,13 +627,14 @@ @@ -452,38 +648,96 @@ - +
- 选择分类实拍图 + 挑选分类图标 / 实拍图 + 点击卡片即可选用
+ +
+ +
+ + + + + + +
+
+ +
-
icon
-
icon
-
icon
-
icon
-
icon
-
icon
-
icon
-
icon
-
icon
-
icon
-
icon
-
icon
-
icon
-
icon
-
icon
-
icon
-
icon
-
icon
+ +
icon招聘专区
+
icon肉蛋家禽
+
icon蔬菜专区
+
icon水果专区
+
icon便民服务
+
icon房产租售
+
icon二手闲置
+
icon车辆交易
+ + +
icon骑手配送
+
icon司机出行
+
icon普工技工
+
icon餐饮服务
+
icon家政保洁
+
icon销售客服
+
icon教育培训
+
icon兼职短工
+ + +
icon鲜土鸡蛋
+
icon散养土鸡
+
icon生鲜猪肉
+
icon高山牛肉
+
icon鲜活水产
+ + +
icon苹果
+
icon柑橘
+
icon猕猴桃
+
icon草莓
+
icon西瓜
+
icon葡萄
+
icon蜜桃
+
icon百香果
+
icon芒果
+
icon香蕉
+
icon蓝莓
+
icon车厘子
+
icon火龙果
+
icon柠檬
+
icon荔枝
+ + +
icon白菜
+
icon花菜
+
icon青菜
+
icon西红柿
+
icon辣椒
+
icon萝卜
+
icon紫甘蓝
+
icon青甘蓝
+ +
diff --git a/application/admin/view/fc/category/edit.html b/application/admin/view/fc/category/edit.html index 498b829..cc3bba6 100644 --- a/application/admin/view/fc/category/edit.html +++ b/application/admin/view/fc/category/edit.html @@ -1,9 +1,9 @@ @@ -323,13 +514,13 @@
归属与类型 - 决定发布页展示哪些字段 + 决定前台分类层级与发布表单
{:build_select('row[pid]', $parentList, $row.pid, ['class'=>'form-control selectpicker', 'id'=>'c-pid', 'data-rule'=>'required'])} - 选「顶级分类」创建一级大类;选「蔬菜」「水果」则创建其下属二级品类。 + 选「顶级分类」创建一级大类频道;选指定分类(如「招聘专区」「肉蛋家禽」)则创建下属二级品类。
@@ -337,15 +528,15 @@
- 修改一级分类类型时,下属二级分类自动同步。 + 修改一级分类类型时,下属二级分类将自动同步。
@@ -358,26 +549,30 @@
- +
- +
- 当前图片 + 当前图片 +
+ {$row.name|htmlentities} + 当前设置标识:{$row.icon_key|htmlentities} +
- 优先上传真实农产品照片,农民用户靠「看图识物」选择品类;建议 500×500 以上方形图,单张不超过 500KB,过大的图片会拖慢小程序加载。 + 推荐从平台预设图片库中直接选用;也支持上传高清实拍方形图(建议 200×200 以上,单张不超过 500KB)。
@@ -399,11 +594,11 @@
推荐:
+
-
-
-
+
+
@@ -439,13 +634,14 @@ @@ -459,38 +655,96 @@ - +
- 选择分类实拍图 + 挑选分类图标 / 实拍图 + 点击卡片即可选用
+ +
+ +
+ + + + + + +
+
+ +
-
icon
-
icon
-
icon
-
icon
-
icon
-
icon
-
icon
-
icon
-
icon
-
icon
-
icon
-
icon
-
icon
-
icon
-
icon
-
icon
-
icon
-
icon
+ +
icon招聘专区
+
icon肉蛋家禽
+
icon蔬菜专区
+
icon水果专区
+
icon便民服务
+
icon房产租售
+
icon二手闲置
+
icon车辆交易
+ + +
icon骑手配送
+
icon司机出行
+
icon普工技工
+
icon餐饮服务
+
icon家政保洁
+
icon销售客服
+
icon教育培训
+
icon兼职短工
+ + +
icon鲜土鸡蛋
+
icon散养土鸡
+
icon生鲜猪肉
+
icon高山牛肉
+
icon鲜活水产
+ + +
icon苹果
+
icon柑橘
+
icon猕猴桃
+
icon草莓
+
icon西瓜
+
icon葡萄
+
icon蜜桃
+
icon百香果
+
icon芒果
+
icon香蕉
+
icon蓝莓
+
icon车厘子
+
icon火龙果
+
icon柠檬
+
icon荔枝
+ + +
icon白菜
+
icon花菜
+
icon青菜
+
icon西红柿
+
icon辣椒
+
icon萝卜
+
icon紫甘蓝
+
icon青甘蓝
+ +
diff --git a/application/admin/view/fc/category/index.html b/application/admin/view/fc/category/index.html index 05ddf64..10ed61b 100644 --- a/application/admin/view/fc/category/index.html +++ b/application/admin/view/fc/category/index.html @@ -97,7 +97,7 @@
-
分类直接联动小程序首页金刚区与分类频道,按排序权重升序展示。点击开关可一键上架/下架,鼠标悬停实拍图可放大预览。
+
分类直接联动小程序首页金刚区与分类频道,按排序权重升序展示。点击开关可一键上架/下架,鼠标悬停分类图标可放大预览。
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