仰望星辰工作室

feat(zjmfmanager_reserve): 完善商品管理功能,优化展示与同步逻辑

Y yang1145 提交于 2026-08-12 12:16 · f731ef8 ·父提交 a76cec1
feat(zjmfmanager_reserve): 完善商品管理功能,优化展示与同步逻辑

1.  移除商品描述的nl2br和htmlspecialchars转义,支持原生HTML渲染
2.  添加文本换行和图片最大宽度样式,优化描述展示效果
3.  重构上游同步逻辑,重复同步时不覆盖本地商品名称和简介
4.  新增商品价格和模块类型提取工具方法
5.  开放商品列表页游客访问权限
6.  新增商品名称、简介的后台管理与校验功能
7.  优化后台商品列表UI,新增价格和模块列,完善表单交互
6 个文件变更 +89 -27 13172193298@163.com
•app_plugins/zjmfmanager_reserve/assets/style.css +4 -2
•app_plugins/zjmfmanager_reserve/bootstrap.php +18 -5
•app_plugins/zjmfmanager_reserve/lib/upstream.php +29 -4
•app_plugins/zjmfmanager_reserve/views/admin/products.php +36 -14
•app_plugins/zjmfmanager_reserve/views/order.php +1 -1
•app_plugins/zjmfmanager_reserve/views/shop.php +1 -1
变更内容
diff --git a/app_plugins/zjmfmanager_reserve/assets/style.css b/app_plugins/zjmfmanager_reserve/assets/style.css
index 08396ad..f9c88c6 100644
--- a/app_plugins/zjmfmanager_reserve/assets/style.css
+++ b/app_plugins/zjmfmanager_reserve/assets/style.css
@@ -23,8 +23,10 @@
 .zj-card-head h2{font-size:18px;color:#222;margin:0;}
 .zj-tag{display:inline-block;padding:2px 10px;font-size:11px;background:#e6f4ff;
   color:#1e9fff;border-radius:10px;margin-top:6px;}
-.zj-card-desc{padding:14px 20px;font-size:13px;color:#666;line-height:1.7;}
-.zj-desc{padding:10px 2px 18px;font-size:13px;color:#666;line-height:1.7;}
+.zj-card-desc{padding:14px 20px;font-size:13px;color:#666;line-height:1.7;overflow-wrap:break-word;}
+.zj-card-desc img{max-width:100%;}
+.zj-desc{padding:10px 2px 18px;font-size:13px;color:#666;line-height:1.7;overflow-wrap:break-word;}
+.zj-desc img{max-width:100%;}
 .zj-price{display:flex;gap:14px;padding:12px 20px;background:#fafbfc;border-top:1px solid #f0f0f0;flex-wrap:wrap;}
 .zj-price-item{display:flex;align-items:center;gap:4px;font-size:12px;}
 .zj-price-label{background:#e8e8e8;color:#666;padding:1px 6px;border-radius:3px;}
diff --git a/app_plugins/zjmfmanager_reserve/bootstrap.php b/app_plugins/zjmfmanager_reserve/bootstrap.php
index ec6d809..477f0f3 100644
--- a/app_plugins/zjmfmanager_reserve/bootstrap.php
+++ b/app_plugins/zjmfmanager_reserve/bootstrap.php
@@ -253,6 +253,8 @@ mnbt_register_ajax('admin', 'p_zjmf_admin_upstream_products', function () {
 			'id'          => (int)($item['id'] ?? 0),
 			'name'        => (string)($item['name'] ?? ''),
 			'description' => (string)($item['description'] ?? ''),
+			'module'      => ZjmfUpstream::itemModule($item),
+			'price'       => ZjmfUpstream::itemPrice($item),
 			'synced'      => isset($existing[(int)($item['id'] ?? 0)]),
 		];
 	}
@@ -331,7 +333,7 @@ mnbt_register_ajax('admin', 'p_zjmf_admin_add_product', function () {
 	json_exit_success('商品已添加,价格同步完成');
 });
 
-// 保存商品加价/排序/状态
+// 保存商品名称/简介/加价/排序/状态
 mnbt_register_ajax('admin', 'p_zjmf_admin_save_product', function () {
 	mnbt_plugin_require_admin();
 
@@ -341,6 +343,18 @@ mnbt_register_ajax('admin', 'p_zjmf_admin_save_product', function () {
 		json_exit_error('商品不存在');
 	}
 
+	$name = trim((string)($_POST['name'] ?? ''));
+	$description = (string)($_POST['description'] ?? '');
+	if ($name === '') {
+		json_exit_error('请填写商品名称');
+	}
+	if (mb_strlen($name) > 100) {
+		json_exit_error('商品名称过长');
+	}
+	if (mb_strlen($description) > 65535) {
+		json_exit_error('商品简介过长');
+	}
+
 	$markupType = in_array((string)($_POST['markup_type'] ?? ''), ['0', '1'], true)
 		? (int)$_POST['markup_type'] : 0;
 	$markupValue = max(0, (int)($_POST['markup_value'] ?? 0));
@@ -352,9 +366,9 @@ mnbt_register_ajax('admin', 'p_zjmf_admin_save_product', function () {
 	$now = $date ?: date('Y-m-d H:i:s');
 	$ok = $DB->query_prepare(
 		"UPDATE MN_plugin_zjmf_product
-		 SET markup_type=?, markup_value=?, sort=?, status=?, updated_at=?
+		 SET name=?, description=?, markup_type=?, markup_value=?, sort=?, status=?, updated_at=?
 		 WHERE id=?",
-		[$markupType, $markupValue, $sort, $status, $now, $id]
+		[$name, $description, $markupType, $markupValue, $sort, $status, $now, $id]
 	);
 	if (!$ok) {
 		json_exit_error('保存失败');
@@ -388,9 +402,8 @@ mnbt_register_ajax('admin', 'p_zjmf_admin_toggle_product', function () {
  *  用户端页面路由
  * ============================================================ */
 
-// 商品列表
+// 商品列表(对游客开放,无需登录)
 mnbt_register_route('GET', '/reserve/shop', function ($params, $ctx) {
-	zjmf_require_user();
 	zjmf_render('shop', [
 		'page_title' => '商品选购',
 		'products'   => zjmf_product_list_active(),
diff --git a/app_plugins/zjmfmanager_reserve/lib/upstream.php b/app_plugins/zjmfmanager_reserve/lib/upstream.php
index 1490a0b..539e951 100644
--- a/app_plugins/zjmfmanager_reserve/lib/upstream.php
+++ b/app_plugins/zjmfmanager_reserve/lib/upstream.php
@@ -225,14 +225,12 @@ class ZjmfUpstream
 
 		$existing = zjmf_product_get_by_up($supplierId, $upId);
 		if ($existing) {
+			// 重复同步仅刷新价格/周期,不覆盖本地已修改的名称与简介
 			$ok = $DB->query_prepare(
 				"UPDATE MN_plugin_zjmf_product
-				 SET name=?, description=?, currency=?, agent_price_cents=?,
-				     cycles=?, synced_at=?, updated_at=?
+				 SET currency=?, agent_price_cents=?, cycles=?, synced_at=?, updated_at=?
 				 WHERE id=?",
 				[
-					(string)($item['name'] ?? ''),
-					(string)($item['description'] ?? ''),
 					$currency,
 					$agentCents,
 					$cyclesJson,
@@ -584,6 +582,33 @@ class ZjmfUpstream
 		return (int)round((float)$val * 100);
 	}
 
+	/** 上游列表项价格(分),支持嵌套 pricing 结构,无价格返回 0。 */
+	public static function itemPrice($item)
+	{
+		if (!is_array($item)) {
+			return 0;
+		}
+		$cents = self::toCents(self::pickPrice($item));
+		if ($cents <= 0 && isset($item['pricing']) && is_array($item['pricing'])) {
+			foreach ($item['pricing'] as $v) {
+				$cents = self::toCents(is_array($v) ? self::pickPrice($v) : $v);
+				if ($cents > 0) {
+					break;
+				}
+			}
+		}
+		return $cents;
+	}
+
+	/** 上游列表项模块类型(魔方财务列表返回字段为 type),无则返回空串。 */
+	public static function itemModule($item)
+	{
+		if (!is_array($item)) {
+			return '';
+		}
+		return (string)($item['type'] ?? $item['module'] ?? $item['module_name'] ?? '');
+	}
+
 	/** 从 data 中找通用 ID(订单 ID)。 */
 	protected static function findId($arr)
 	{
diff --git a/app_plugins/zjmfmanager_reserve/views/admin/products.php b/app_plugins/zjmfmanager_reserve/views/admin/products.php
index 82692cc..b3be28b 100644
--- a/app_plugins/zjmfmanager_reserve/views/admin/products.php
+++ b/app_plugins/zjmfmanager_reserve/views/admin/products.php
@@ -89,11 +89,13 @@ function zjmf_admin_markup_label($product)
                 </th>
                 <th>上游ID</th>
                 <th>名称</th>
+                <th>价格</th>
+                <th>模块</th>
                 <th>状态</th>
               </tr>
             </thead>
             <tbody id="zjf-sync-list">
-              <tr><td colspan="4" class="text-center text-muted">
+              <tr><td colspan="6" class="text-center text-muted">
                 请先选择供应商并加载商品列表
               </td></tr>
             </tbody>
@@ -123,11 +125,12 @@ function zjmf_admin_markup_label($product)
           </div>
           <div class="col-md-3 mb-2">
             <label class="small text-muted">商品名称 *</label>
-            <input type="text" id="zjf-add-name" class="form-control form-control-sm">
+            <input type="text" id="zjf-add-name" class="form-control form-control-sm" maxlength="100">
           </div>
           <div class="col-md-4 mb-2">
-            <label class="small text-muted">描述</label>
-            <input type="text" id="zjf-add-desc" class="form-control form-control-sm">
+            <label class="small text-muted">描述(支持 HTML)</label>
+            <textarea id="zjf-add-desc" class="form-control form-control-sm" rows="2"
+                      placeholder="支持 HTML 标签,如 &lt;b&gt;加粗&lt;/b&gt;"></textarea>
           </div>
         </div>
         <div class="small text-muted mb-2">
@@ -143,8 +146,9 @@ function zjmf_admin_markup_label($product)
         <input type="hidden" id="zjf-edit-id" value="0">
         <div class="form-row">
           <div class="col-md-4 mb-2">
-            <label class="small text-muted">商品名</label>
-            <div id="zjf-edit-name" class="pt-1"></div>
+            <label class="small text-muted">商品名 *</label>
+            <input type="text" id="zjf-edit-name" class="form-control form-control-sm"
+                   maxlength="100">
           </div>
           <div class="col-md-2 mb-2">
             <label class="small text-muted">加价方式</label>
@@ -170,9 +174,14 @@ function zjmf_admin_markup_label($product)
               <option value="0">下架</option>
             </select>
           </div>
+          <div class="col-md-12 mb-2">
+            <label class="small text-muted">商品简介(支持 HTML)</label>
+            <textarea id="zjf-edit-desc" class="form-control form-control-sm" rows="4"
+                      placeholder="支持 HTML 标签,如 &lt;b&gt;加粗&lt;/b&gt;、&lt;br&gt; 换行。同步商品不会覆盖此处内容。"></textarea>
+          </div>
         </div>
         <div class="small text-muted mb-2">
-          留空加价数值表示使用所属供应商的加价规则
+          留空加价数值表示使用所属供应商的加价规则;重新同步上游仅更新价格,不覆盖本地名称与简介。
         </div>
         <button type="button" class="btn btn-sm btn-primary" id="zjf-edit-save">保存</button>
         <button type="button" class="btn btn-sm btn-secondary" id="zjf-edit-cancel">取消</button>
@@ -225,6 +234,7 @@ function zjmf_admin_markup_label($product)
                     <button type="button" class="btn btn-sm btn-outline-primary zjf-edit"
                             data-id="<?= (int)$p['id'] ?>"
                             data-name="<?= htmlspecialchars($p['name'], ENT_QUOTES) ?>"
+                            data-desc="<?= htmlspecialchars($p['description'] ?? '', ENT_QUOTES) ?>"
                             data-type="<?= (int)$p['markup_type'] ?>"
                             data-value="<?= (int)$p['markup_value'] ?>"
                             data-sort="<?= (int)$p['sort'] ?>"
@@ -259,6 +269,11 @@ function zjmf_admin_markup_label($product)
     }
     if (ok && reload) setTimeout(function () { location.reload(); }, 600);
   }
+  function esc(s) {
+    return String(s == null ? '' : s)
+      .replace(/&/g, '&amp;').replace(/</g, '&lt;')
+      .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
+  }
   function post(data, reload) {
     $.post('ajax.php', data, function (r) { notify(res(r), reload); });
   }
@@ -284,7 +299,7 @@ function zjmf_admin_markup_label($product)
     btn.disabled = true;
     btn.textContent = '加载中...';
     syncTip.textContent = '正在拉取上游商品列表...';
-    syncList.innerHTML = '<tr><td colspan="4" class="text-center text-muted">加载中...</td></tr>';
+    syncList.innerHTML = '<tr><td colspan="6" class="text-center text-muted">加载中...</td></tr>';
     $.post('ajax.php', {gn: 'p_zjmf_admin_upstream_products', id: supplierId},
       function (r) {
         var d = res(r);
@@ -292,26 +307,28 @@ function zjmf_admin_markup_label($product)
         btn.textContent = '加载商品列表';
         if (d.qk != 1 && !d.success) {
           syncTip.textContent = '';
-          syncList.innerHTML = '<tr><td colspan="4" class="text-center text-muted">'
+          syncList.innerHTML = '<tr><td colspan="6" class="text-center text-muted">'
             + (d.msg || d.code || '拉取失败') + '</td></tr>';
           return;
         }
         var list = d.list || (d.data && d.data.list) || [];
         if (!list.length) {
-          syncList.innerHTML = '<tr><td colspan="4" class="text-center text-muted">'
+          syncList.innerHTML = '<tr><td colspan="6" class="text-center text-muted">'
             + '该供应商暂无商品</td></tr>';
           return;
         }
         syncTip.textContent = '共 ' + list.length + ' 个商品,勾选要同步的项目后点击「开始同步」'
-          + '(已同步过的将更新价格,不覆盖加价与上架状态)';
+          + '(已同步过的将更新价格,不覆盖本地名称/简介与加价配置)';
         var html = '';
         for (var i = 0; i < list.length; i++) {
           var it = list[i];
-          html += '<tr class="zjf-up-row" data-name="' + (it.name || '').toLowerCase() + '">'
+          html += '<tr class="zjf-up-row" data-name="' + esc((it.name || '').toLowerCase()) + '">'
             + '<td><input type="checkbox" class="zjf-up-check" value="' + it.id + '"'
             + (it.synced ? ' checked' : '') + '></td>'
             + '<td>' + it.id + '</td>'
-            + '<td>' + (it.name || '-') + '</td>'
+            + '<td>' + esc(it.name || '-') + '</td>'
+            + '<td>' + (it.price ? '¥' + (it.price / 100).toFixed(2) : '-') + '</td>'
+            + '<td>' + (it.module ? '<span class="badge badge-info">' + esc(it.module) + '</span>' : '-') + '</td>'
             + '<td>' + (it.synced ? '<span class="badge badge-success">已同步</span>'
                 : '<span class="badge badge-secondary">未同步</span>') + '</td>'
             + '</tr>';
@@ -399,6 +416,7 @@ function zjmf_admin_markup_label($product)
   /* ---------------- 编辑商品 ---------------- */
   var editId = document.getElementById('zjf-edit-id');
   var editName = document.getElementById('zjf-edit-name');
+  var editDesc = document.getElementById('zjf-edit-desc');
   var editType = document.getElementById('zjf-edit-type');
   var editValue = document.getElementById('zjf-edit-value');
   var editSort = document.getElementById('zjf-edit-sort');
@@ -409,7 +427,8 @@ function zjmf_admin_markup_label($product)
       syncWrap.style.display = 'none';
       addWrap.style.display = 'none';
       editId.value = btn.getAttribute('data-id');
-      editName.textContent = btn.getAttribute('data-name');
+      editName.value = btn.getAttribute('data-name');
+      editDesc.value = btn.getAttribute('data-desc') || '';
       editType.value = btn.getAttribute('data-type');
       editValue.value = btn.getAttribute('data-value');
       editSort.value = btn.getAttribute('data-sort');
@@ -426,9 +445,12 @@ function zjmf_admin_markup_label($product)
   document.getElementById('zjf-edit-save').addEventListener('click', function () {
     var id = parseInt(editId.value, 10);
     if (!id) return;
+    if (!editName.value) { alert('请填写商品名称'); return; }
     post({
       gn: 'p_zjmf_admin_save_product',
       id: id,
+      name: editName.value,
+      description: editDesc.value,
       markup_type: editType.value,
       markup_value: editValue.value,
       sort: editSort.value,
diff --git a/app_plugins/zjmfmanager_reserve/views/order.php b/app_plugins/zjmfmanager_reserve/views/order.php
index d8c5bf0..3f7a2ea 100644
--- a/app_plugins/zjmfmanager_reserve/views/order.php
+++ b/app_plugins/zjmfmanager_reserve/views/order.php
@@ -19,7 +19,7 @@ ob_start();
       <a class="layui-btn layui-btn-xs layui-btn-primary" href="<?= zjmf_url('reserve/shop') ?>">返回商品列表</a>
     </div>
 
-    <div class="zj-desc"><?= nl2br(htmlspecialchars($product['description'])) ?></div>
+    <div class="zj-desc"><?= $product['description'] ?></div>
 
     <form class="zj-order-form" id="zjf-order-form">
       <div class="layui-form-item">
diff --git a/app_plugins/zjmfmanager_reserve/views/shop.php b/app_plugins/zjmfmanager_reserve/views/shop.php
index 2f8dcf9..4d8b588 100644
--- a/app_plugins/zjmfmanager_reserve/views/shop.php
+++ b/app_plugins/zjmfmanager_reserve/views/shop.php
@@ -49,7 +49,7 @@ ob_start();
                 <span class="zj-tag"><?= htmlspecialchars($product['currency']) ?></span>
               <?php endif; ?>
             </div>
-            <div class="zj-card-desc"><?= nl2br(htmlspecialchars($product['description'])) ?></div>
+            <div class="zj-card-desc"><?= $product['description'] ?></div>
             <div class="zj-price">
               <?php $cycles = zjmf_product_cycles($product); ?>
               <?php foreach ($cycles as $cycle => $cfg): ?>