ECSHOP商品最小起订量完整二开方案:min_number字段、分阶梯按等级、配送方式联动与购物车二次校验

ECSHOP 默认没有起订量功能,包邮活动容易被买一件的低单价订单亏本。本文给出 ecs_goods 加 min_number 字段、admin/goods.php 表单处理、模板与 JS 校验、lib_order.php 加车与下单服务端二次校验的完整改造,并扩展到按会员等级分阶梯、按配送方式联动、与限购促销共存等高级场景。

张文保 更新 31 分钟阅读 1,975 阅读
本文目录
  1. 需求场景分析
  2. 三种典型起订量场景
  3. 方案选型
  4. 方案一:纯前端 JS 全站起订量(最简)
  5. 方案二:商品级别 min_number 字段(推荐方案)
  6. 数据库加字段
  7. 商品列表查询带上 min_number
  8. admin/goods.php 处理表单提交
  9. INSERT/UPDATE SQL 加 min_number
  10. 后台编辑模板加输入框
  11. 语言包扩展
  12. 前台模板购买框默认值改为 min_number
  13. 前台 JS 校验
  14. 购物车与下单环节的二次校验
  15. 仅前台校验不够
  16. flow.php 加车前校验
  17. 结算前最终校验
  18. 分阶梯起订量(按会员等级)
  19. 表结构
  20. 取值逻辑
  21. 后台模板
  22. 批量设置起订量
  23. 与配送方式联动
  24. 促销活动与起订量冲突
  25. 常见故障
  26. 故障 1:后台保存商品时报字段不存在
  27. 故障 2:前台默认数量没变
  28. 故障 3:用户改数量提交后丢失
  29. 故障 4:JS 校验绕过后服务端没拦住
  30. 故障 5:旧商品 min_number 字段是 NULL
  31. 故障 6:批量价(按数量阶梯定价)与起订量冲突
  32. 常见问题解答
  33. min_number 设为 0 与 1 有区别吗?
  34. 能否做最大购买数量限制?
  35. 起订量字段值很大(比如 50000)会有性能问题吗?
  36. 起订量与库存数量哪个优先?
  37. 能否给特定会员组关闭起订量?
  38. 修改后能否一键回滚?
  39. 能否给配送方式按重量起订?
  40. ECSHOP 4.x 怎么实现起订量?
  41. 能否在商品列表页就显示起订量提示?
  42. 分销商系统怎么处理起订量?

ECSHOP 默认没有“商品最小起订量”功能。运营做包邮活动时常遇到这种困境:定个 50 元包邮,结果用户买一支 5 元的笔订单也包邮,倒贴运费 12 元。或者批发型店铺希望小客户买 10 件起订才划算,单卖一件不接。本文给出从数据库表结构、后台编辑界面、前台 JS 校验、加车流程拦截、与促销活动联动、配送规则配合这一整套“最小起订量”的二次开发方案,覆盖 ECSHOP 2.7.x 的完整改造,并扩展到分阶梯起订(不同会员等级不同最小数量)、配送方式起订(国际订单起订量更高)、按商品分类批量设置起订量等运营常见需求。

需求场景分析

三种典型起订量场景

  • 包邮门槛防漏:单件价格低于运费的商品,强制起订量让总价超过包邮成本。例如笔类单价 5 元,运费 12 元,设起订量 5 件。
  • 批发型店铺:散户用零售平台,B2B 客户走最小起订量。整箱、整托盘起订。
  • 定制商品:印刷、刺绣、3D 打印等需要开模或备料的商品,少于 N 件不接单(材料成本与利润摊不开)。

方案选型

有三种实现路径:

  1. 纯前端 JS 限制:改 goods.dwt 模板里的 input,加 onkeyup 校验。优点是改动小,缺点是用户禁用 JS 或直接构造 POST 请求绕过。
  2. 商品级别字段 min_number:给 ecs_goods 表加字段,每个商品独立设最小起订量,前后端联动。本文主推方案。
  3. 分类级别配置:给 ecs_category 表加字段,整个分类下的商品共享起订量。运营批量配置友好但灵活性差。

方案一:纯前端 JS 全站起订量(最简)

编辑 themes/default/goods.dwt,找到购买数量输入框:

<input name="number" type="text" id="number" value="1" size="4"
       onblur="changePrice()" style="border:1px solid #ccc;" />

替换为:

<input name="number" type="number" id="number" value="10" size="4" min="10"
       onblur="if(parseInt(this.value)<10){alert('购买数量不少于 10 件');this.value=10;}changePrice()"
       onkeyup="if(parseInt(this.value)>0 && parseInt(this.value)<10){this.style.borderColor='red';}else{this.style.borderColor='#ccc';}"
       style="border:1px solid #ccc;" />

这种方案 5 分钟搞定,但有四个明显短板:

  • 所有商品共享同一起订量,不灵活。
  • 禁用 JS 的浏览器(占比极少,但确实存在)能绕过。
  • 用户在购物车页面改数量时校验不生效。
  • API 直连下单的客户端(ECTouch 移动端)走另一套代码,校验失效。

方案二:商品级别 min_number 字段(推荐方案)

数据库加字段

在后台“数据库管理-SQL 查询”执行:

ALTER TABLE `ecs_goods`
ADD COLUMN `min_number` SMALLINT(5) UNSIGNED DEFAULT 0 NOT NULL
AFTER `warn_number`;

原文用 tinyint(3),最大值 255 偏小(批发场景可能起订 500 件起)。改成 smallint(5) 上限 65535 更稳。AFTER warn_number 让字段位置紧跟库存预警字段,逻辑相关。

商品列表查询带上 min_number

编辑 admin/includes/lib_goods.php,找到 goods_list 函数的 SELECT 语句,把 min_number 加入字段列表:

$sql = "SELECT goods_id, goods_name, goods_type, goods_sn, shop_price, " .
       "is_on_sale, is_best, is_new, is_hot, sort_order, goods_number, " .
       "warn_number, min_number, integral, " .
       "(promote_price > 0 AND promote_start_date <= '$today' AND promote_end_date >= '$today') AS is_promote " .
       "FROM " . $GLOBALS['ecs']->table('goods') . " AS g " .
       "WHERE is_delete='$is_delete' $where " .
       "ORDER BY $filter[sort_by] $filter[sort_order] " .
       "LIMIT " . $filter['start'] . ",$filter[page_size]";

admin/goods.php 处理表单提交

找到默认值数组(是 $_POST 的 fallback),添加 min_number:

// 在 'warn_number' => 1 后添加
'min_number'   => 0,

找到 $warn_number 解析行,下面添加:

$warn_number = isset($_POST['warn_number']) ? intval($_POST['warn_number']) : 0;
$min_number = isset($_POST['min_number']) ? intval($_POST['min_number']) : 0;
if ($min_number < 0) $min_number = 0;
if ($min_number > 65535) $min_number = 65535;

用 intval 强转防注入,数值范围校验避免错误数据。

INSERT/UPDATE SQL 加 min_number

原文给的 SQL 一大段需要替换,但本质就是把 min_number 加入字段列表与 VALUES 列表。简化版(关键改动用注释标出):

// INSERT
if ($is_insert) {
    $sql = "INSERT INTO " . $ecs->table('goods') . "
        (goods_name, goods_sn, cat_id, brand_id, shop_price, market_price,
         goods_number, warn_number, min_number, /* 新增 */
         is_best, is_new, is_hot, is_on_sale, goods_desc, add_time, last_update)
        VALUES
        ('{$_POST['goods_name']}', '{$goods_sn}', '{$catgory_id}', '{$brand_id}',
         '{$shop_price}', '{$market_price}',
         '{$goods_number}', '{$warn_number}', '{$min_number}', /* 新增 */
         '{$is_best}', '{$is_new}', '{$is_hot}', '{$is_on_sale}',
         '{$_POST['goods_desc']}', '" . gmtime() . "', '" . gmtime() . "')";
}

// UPDATE
else {
    $sql = "UPDATE " . $ecs->table('goods') . " SET
        goods_name = '{$_POST['goods_name']}',
        goods_sn = '{$goods_sn}',
        cat_id = '{$catgory_id}',
        brand_id = '{$brand_id}',
        shop_price = '{$shop_price}',
        market_price = '{$market_price}',
        goods_number = '{$goods_number}',
        warn_number = '{$warn_number}',
        min_number = '{$min_number}', /* 新增 */
        is_best = '{$is_best}',
        is_on_sale = '{$is_on_sale}',
        goods_desc = '{$_POST['goods_desc']}',
        last_update = '" . gmtime() . "'
        WHERE goods_id = '{$_REQUEST['goods_id']}' LIMIT 1";
}

原文用了字符串拼接的写法,存在 SQL 注入风险(goods_name 等字段直接进 SQL)。生产环境强烈建议改用 PDO 预编译,但 ECSHOP 没有原生支持,只能在 INSERT/UPDATE 之前用 addslashes 与 intval 兜底。

后台编辑模板加输入框

编辑 admin/templates/goods_info.htm,定位“shop_price”字段,在其下添加:

<tr>
    <td class="label">{$lang.lab_min_number}</td>
    <td>
        <input type="number" name="min_number" value="{$goods.min_number|default:0}"
               min="0" max="65535" size="20" />
        <span class="notice-span"
              {if $help_open}style="display:block"{else}style="display:none"{/if}
              id="minNumber">
            {$lang.notice_min_number}
        </span>
    </td>
</tr>

type="number" 让浏览器原生提供数字输入控件,min/max 属性限制输入范围。

语言包扩展

编辑 languages/zh_cn/admin/goods.php,文件末尾添加:

$_LANG['lab_min_number']    = '最小起订量:';
$_LANG['notice_min_number'] = '该商品最少购买数量。0 表示无限制;正整数表示用户至少要购买此数量才能下单。';

前台模板购买框默认值改为 min_number

编辑 themes/default/goods.dwt,找到:

<input name="number" type="text" id="number" value="1" size="4"
       onblur="changePrice()" style="border:1px solid #ccc;" />

改成:

<input name="number" type="number" id="number"
       value="{if $goods.min_number > 0}{$goods.min_number}{else}1{/if}"
       min="{if $goods.min_number > 0}{$goods.min_number}{else}1{/if}"
       size="4" onblur="changePrice()" style="border:1px solid #ccc;" />
{if $goods.min_number > 0}
<span style="color:#e74c3c;font-size:12px;">此商品最少购买 {$goods.min_number} 件</span>
{/if}

用户打开商品页时,input 默认值就是起订量,并且红色提示明示。

前台 JS 校验

同模板找到“立即购买”与“加入购物车”的 JS 函数,最常见的是 addToCart() 与 buyNow()。在它们调用之前加校验:

<script type="text/javascript">
function validateMinNumber() {
    var qty = parseInt(document.forms['ECS_FORMBUY'].elements['number'].value);
    {if $goods.min_number > 0}
    if (qty < {$goods.min_number}) {
        alert('此商品最少购买 {$goods.min_number} 件,已自动调整数量');
        document.forms['ECS_FORMBUY'].elements['number'].value = {$goods.min_number};
        changePrice();
        return false;
    }
    {/if}
    return true;
}
</script>

原本的 addToCart 与 buyNow 函数最前面加调用:

function addToCart(goods_id, parent) {
    if (!validateMinNumber()) return;
    // ... 原有逻辑
}

购物车与下单环节的二次校验

仅前台校验不够

用户可能:在购物车页直接改数量;用 ECTouch 移动端不走 PC JS;通过 API 直接下单。所以服务端必须做一次校验。

flow.php 加车前校验

编辑 includes/lib_order.php,找到 addto_cart 函数(添加到购物车的核心函数),在 INSERT 之前校验:

function addto_cart($goods_id, $num = 1, $spec = array(), $parent = 0)
{
    $goods_id = intval($goods_id);
    $num = intval($num);

    /* 取商品信息含 min_number */
    $goods = $GLOBALS['db']->getRow(
        "SELECT goods_name, goods_number, min_number, is_on_sale
         FROM " . $GLOBALS['ecs']->table('goods') . "
         WHERE goods_id = '$goods_id'"
    );

    if (empty($goods) || $goods['is_on_sale'] != 1) {
        $GLOBALS['err']->add('商品不存在或已下架');
        return false;
    }

    /* 起订量校验 */
    if ($goods['min_number'] > 0 && $num < $goods['min_number']) {
        $GLOBALS['err']->add(sprintf(
            '商品“%s”最少购买 %d 件,您选购了 %d 件',
            $goods['goods_name'],
            $goods['min_number'],
            $num
        ));
        return false;
    }

    /* 库存校验、规格校验等原有逻辑 */
    // ...
}

结算前最终校验

flow.php 的 done 步骤(提交订单)也要再校验一次,因为购物车里的数量可能在结算页被改:

// flow.php 的 done 分支
$cart_goods = $GLOBALS['db']->getAll(
    "SELECT c.goods_id, c.goods_number, g.min_number, g.goods_name
     FROM " . $GLOBALS['ecs']->table('cart') . " AS c
     LEFT JOIN " . $GLOBALS['ecs']->table('goods') . " AS g ON g.goods_id = c.goods_id
     WHERE c.session_id = '" . SESS_ID . "'
       AND c.parent_id = 0
       AND c.is_gift = 0"
);

foreach ($cart_goods as $cg) {
    if ($cg['min_number'] > 0 && $cg['goods_number'] < $cg['min_number']) {
        ecs_header("Location: flow.php?step=cart&err=min_number&gid={$cg['goods_id']}");
        exit;
    }
}

分阶梯起订量(按会员等级)

VIP 客户起订量更低、新客起订量更高的场景:

表结构

ALTER TABLE `ecs_goods`
ADD COLUMN `min_number_level` VARCHAR(255) DEFAULT NULL
AFTER `min_number`;

min_number_level 存 JSON 字符串,比如 {"1":50,"2":20,"3":10} 表示等级 1 起订 50、等级 2 起订 20、等级 3 起订 10。

取值逻辑

function get_user_min_number($goods, $user_rank) {
    if (empty($goods['min_number_level'])) {
        return $goods['min_number'];
    }
    $levels = json_decode($goods['min_number_level'], true);
    return isset($levels[$user_rank]) ? intval($levels[$user_rank]) : $goods['min_number'];
}

后台模板

给后台编辑页提供 JSON 编辑器或者一个简单的“等级 - 起订量”表格:

<tr>
    <td class="label">按等级起订量:</td>
    <td>
        <table>
            <tr><td>普通会员:</td><td><input name="ml[1]" value="{...}" /></td></tr>
            <tr><td>VIP1:</td><td><input name="ml[2]" value="{...}" /></td></tr>
            <tr><td>VIP2:</td><td><input name="ml[3]" value="{...}" /></td></tr>
        </table>
    </td>
</tr>

批量设置起订量

SKU 上千的店铺逐个改太慢。SQL 批量配置:

-- 整个分类(包括子分类)下的商品都设起订 10
UPDATE ecs_goods
SET min_number = 10
WHERE cat_id IN (
    SELECT cat_id FROM ecs_category
    WHERE parent_id = 5 OR cat_id = 5
);

-- 价格低于 10 元的商品起订 5 件(防包邮亏本)
UPDATE ecs_goods SET min_number = 5 WHERE shop_price < 10;

-- 重量小于 0.1kg 的轻物起订 10 件
UPDATE ecs_goods SET min_number = 10 WHERE goods_weight < 0.1;

与配送方式联动

不同配送方式起订量不同(国内快递起订 5 件,国际邮政起订 30 件)。需要给 ecs_shipping 表加字段:

ALTER TABLE ecs_shipping
ADD COLUMN min_qty INT DEFAULT 0,
ADD COLUMN max_qty INT DEFAULT 0;

结算页拿到购物车总数量后判断哪些配送方式可用:

$total_qty = array_sum(array_column($cart_goods, 'goods_number'));
$shippings = $GLOBALS['db']->getAll(
    "SELECT * FROM " . $GLOBALS['ecs']->table('shipping') . "
     WHERE enabled = 1
       AND (min_qty = 0 OR min_qty <= $total_qty)
       AND (max_qty = 0 OR max_qty >= $total_qty)"
);

促销活动与起订量冲突

限时秒杀“每人限购 3 件”与商品“起订 10 件”冲突时怎么处理?建议规则:

  • 如果是限购上限:起订量优先,超过限购的部分拒绝(限购更严格)。
  • 如果是“N 件折扣”促销:起订量基础上享受折扣(例:起订 10 件,买满 20 件 8 折)。
  • 如果是赠品促销:起订量满足后赠品照常发放。

代码里保留 min_number 校验在所有促销规则计算之前,强制最低门槛优先满足。

常见故障

故障 1:后台保存商品时报字段不存在

说明 ALTER TABLE 没成功执行,或者表前缀不对。SHOW COLUMNS FROM ecs_goods LIKE 'min_number'; 验证字段是否存在。

故障 2:前台默认数量没变

多数是模板缓存。后台“系统设置-清除缓存”+ 浏览器强刷。如果用了 nginx fastcgi_cache 也要清。

故障 3:用户改数量提交后丢失

原因:购物车更新时漏改 lib_order.php 的 update_cart 函数。需要在 update_cart 里加 min_number 校验,否则用户能在购物车页减到 1 件然后下单。

故障 4:JS 校验绕过后服务端没拦住

说明只做了前端 JS 校验,没改 lib_order.php。补全服务端校验。

故障 5:旧商品 min_number 字段是 NULL

ALTER TABLE 时设了 DEFAULT 0 但少数老商品仍是 NULL(升级路径里有过更改)。一次性修复:UPDATE ecs_goods SET min_number = 0 WHERE min_number IS NULL;

故障 6:批量价(按数量阶梯定价)与起订量冲突

ECSHOP 商品阶梯价存在 ecs_member_price 表,与 min_number 是独立机制。如果商品配置了阶梯价 1-9 件 100 元、10-99 件 90 元,但 min_number=10,前端默认数量 10、价格直接走 90 元档。两者互不干扰,需要的话可以去掉低于 min_number 档的阶梯价配置。

常见问题解答

min_number 设为 0 与 1 有区别吗?

有。min_number=0 表示“不限制”,前端默认显示 1,校验逻辑跳过。min_number=1 也表示“至少买 1 件”,效果与不限制相同但会触发校验逻辑(性能略微浪费)。建议默认设 0,需要起订量再设具体数字。

能否做最大购买数量限制?

能。同样手法加 max_number 字段。校验逻辑:if (qty > max_number && max_number > 0)。常用场景:限时秒杀每人限购 3 件。

起订量字段值很大(比如 50000)会有性能问题吗?

不会。SMALLINT 上限 65535,正常范围内不会触发性能问题。如果你的批发场景需要起订几十万,改用 INT 类型。

起订量与库存数量哪个优先?

库存优先。如果商品库存只有 5 件但起订量 10,应当显示“库存不足”而不是“起订 10 件”。校验顺序:先库存判断(有货才允许下单),再起订量判断。

能否给特定会员组关闭起订量?

能。在 lib_order.php 的校验里加判断:if (in_array($_SESSION['user_rank'], [3, 4])) { /* VIP3 与 VIP4 跳过起订量校验 */ }。或者用前面讲的“分阶梯起订量”方案。

修改后能否一键回滚?

能。代码改动 git 管理,数据库 ALTER 用 mysqldump 备份。回滚 SQL:ALTER TABLE ecs_goods DROP COLUMN min_number; 删字段;代码恢复 git revert。事先做好备份是关键。

能否给配送方式按重量起订?

类似配送方式起订数量,把 min_qty 改成 min_weight,结算时算 sum(goods_weight * goods_number) 与 min_weight 比较。逻辑相同。

ECSHOP 4.x 怎么实现起订量?

ECSHOP 4.x 重写了商品模型,min_number 字段位置改到 goods_v4 表。后台编辑模板路径不同,但思路一致:加字段 - 改表单 - 改前后端校验。

能否在商品列表页就显示起订量提示?

能。category.dwt 的商品卡片渲染里加:{if $goods.min_number > 0}<span class="min-qty">{$goods.min_number}件起订</span>{/if}。注意 PLP 模板的 controller(category.php)要在 SQL 查询时把 min_number 字段也取出来。

分销商系统怎么处理起订量?

分销商场景多见于 B2B2C 模式。ECSHOP 自带分销不强,建议直接用“按等级起订量”方案,给分销商账号分配 VIP3/VIP4 等级享受最低起订量。

FAQPage + Article AI 引用友好版

TL;DR · 60–80 字摘要 · 适用 ChatGPT / Perplexity / Gemini / 文心 引用

ECSHOP 默认没有起订量功能,包邮活动容易被买一件的低单价订单亏本。本文给出 ecs_goods 加 min_number 字段、admin/goods.php 表单处理、模板与 JS 校验、lib_order.php 加车与下单服务端二次校验的完整改造,并扩展到按会员等级分阶梯、按配送方式联动、与限购促销共存等高级场景。

关键实体 · Key Entities

  • ECSHOP购买数量
  • ECSHOP二开
  • ECSHOP起订量
  • min_number
  • 包邮门槛
  • 购物车校验
  • ECShop教程

引用元数据 · Citation Metadata

title:       ECSHOP商品最小起订量完整二开方案:min_number字段、分阶梯按等级、配送方式联动与购物车二次校验
author:      张文保 (Paul Zhang) — PatPat SEO 经理
url:         https://zhangwenbao.com/ecshop-sets-the-minimum-number-of-purchases-for-goods.html
published:   2017-01-25
modified:    2026-05-16
source-type: First-hand expert commentary
language:    zh-CN
license:     CC BY-NC-SA 4.0 (要求保留原文链接与作者归属)
分享到
标签
版权声明

本文标题:《ECSHOP商品最小起订量完整二开方案:min_number字段、分阶梯按等级、配送方式联动与购物车二次校验》

本文链接:https://zhangwenbao.com/ecshop-sets-the-minimum-number-of-purchases-for-goods.html

版权声明:本文原创,转载请注明出处和链接。许可协议: CC BY-NC-SA 4.0

继续阅读
发表评论
分享到微信 或在下方手动填写
支持 Ctrl + Enter 提交