☰
牧场养牛系统设计:积分闭环、商城与抽奖防刷实战
2026/10/7 16:39:43 网站建设 项目流程

简介:这套牧场养牛系统源码是一套可直接部署运营的 PHP 商城类站点,内置积分商城、大转盘抽奖、会员特权三大核心模块,适合站长、PHP 开发者以及需要快速搭建养殖或农业主题电商平台的用户。项目基于 Linux + CentOS7 + 宝塔面板环境运行,亲测在 Nginx 1.18.0、PHP5.6、MySQL5.5 下稳定,二次开发空间充足,更换标题与图片即可适配不同养殖品类。压缩包共 2000 个文件,约 165.78MB。其中 PHP 文件 1487 个,负责后端业务逻辑与接口;HTML、JS、CSS 文件分别为 236、154、87 个,构成前端页面与交互特效;另附 SQL 数据库文件、配置文件及少量 C 源码工具,便于完整搭建和本地调试。目前已有 116 人学习下载,适合作为商城源码研究或快速上线运营的参考项目。资源内包含完整的会员体系、积分规则、抽奖逻辑与商城订单流程,可帮助读者理解这类变现型站点的前后端协作方式,也能基于现有代码做功能扩展、界面美化或移动端适配。

1. 牧场养牛系统到底在做什么:不是游戏,是积分闭环

“牧场养牛系统”听起来像一款种菜小游戏,我第一次接触它时也以为是短期玩具,但真正把需求拆开才发现,它是一个把用户活跃、积分消耗、会员成长串起来的业务中台。用户在这里买牛、养牛、出栏换取积分,积分进入商城兑换商品,大转盘再用来消耗剩余积分,会员等级又反过来影响积分产出速度。这套循环一旦跑通,比单纯的签到送积分留存效果好很多。适合手里已有会员体系、想加一个趣味化留存场景的团队,也适合做私域积分运营的中小项目。下面按我的实测落地路径来讲,数据库、接口、定时任务都能直接抄走改,我用的 ThinkPHP + MySQL + Redis,换 Spring Boot 思路也通用。

2. 先把数据结构钉死:用户、牛只、积分、订单四张核心表

这套系统最容易翻车的就是数据结构没提前想清楚。我用过最糟糕的一版,把牛只生长记录和积分流水混在一张表里,后来查账和对账全部卡死。重做之后,我按四个域拆开:用户域、牛只域、积分域、订单域,各管各的,再通过 user_id 和业务单号关联。这样不管是每日产出结算,还是商城兑换、抽奖消耗,每一笔积分都能追溯。

2.1 用户与牛只的绑定关系:牛只表里放“快照字段”而不是实时计算

用户表 user 不用动。牛只表是关键,它记录的是每一头牛的实例,而不是用户养了“一种”牛。牛只和用户是一对多关系,所以建一个 cattle 表就够了。牛种配置单独放 cattle_type 表,存牛种的产出倍率、成长周期、售价,cattle 表只存实例数据,这样以后加活动牛种,不用改主表结构。

CREATE TABLE `cattle` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL COMMENT '用户ID', `type_id` int(11) NOT NULL COMMENT '牛种ID', `name` varchar(32) NOT NULL DEFAULT '' COMMENT '牛只名称', `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0待成长 1成长中 2可出栏 3已出栏', `buy_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '购入时间', `grow_start_time` datetime DEFAULT NULL COMMENT '本次成长周期开始时间', `output_days` int(11) NOT NULL DEFAULT '0' COMMENT '已产出天数', `total_earn_credit` int(11) NOT NULL DEFAULT '0' COMMENT '累计产出积分', PRIMARY KEY (`id`), KEY `idx_user_status` (`user_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里的 status 字段我用 0、1、2、3 表示,含义是“待成长 / 成长中 / 可出栏 / 已出栏”。为什么不直接把“可出栏”叫“成年”?因为实际业务里,牛只到一定生长天数后可以出栏领取积分,但用户可以暂不出栏,继续养着每日产积分,所以“可出栏”是状态,“已出栏”是终态。output_days 是累计产出天数,不是根据当前时间实时算的,方便定时任务只更新已计算过的日期。total_earn_credit 是累计积分,用来对账,每天由 growth_log 累加而来,不允许手动改。

每天产出的明细要单独记,不能只更新 cattle 表上的累计数。因为一旦用户投诉积分少了,没有明细根本查不出来。我建了 cattle_growth_log 表,每个牛只每天只产生一条记录。

CREATE TABLE `cattle_growth_log` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `cattle_id` int(11) NOT NULL, `user_id` int(11) NOT NULL, `credit` int(11) NOT NULL COMMENT '本次产出积分', `grow_date` date NOT NULL COMMENT '产出日期', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_cattle_date` (`cattle_id`, `grow_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

唯一键uk_cattle_date是必加的,它保证同一头牛同一天不会被执行两次结算。如果定时任务重复跑,这个唯一键会直接报错,而不是偷偷重复加积分。这一点在后面的避坑章里我会再提。

2.2 积分流水必须单独建表,并且和业务单号强关联

积分流水是整个系统的账本,最忌讳只存 user_id 和 change_amount。我给 point_log 表设计了 source_type 和 source_id 两个字段,source_type 区分是成长产出、商城兑换、抽奖消耗、抽奖发放还是会员奖励,source_id 存对应的业务单号或记录 ID。这样用户问“我这个积分怎么没的”,一条 SQL 就能反查出来。

CREATE TABLE `point_log` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `change_amount` int(11) NOT NULL COMMENT '正数收入 负数支出', `balance_after` int(11) NOT NULL DEFAULT '0' COMMENT '变动后余额', `source_type` tinyint(1) NOT NULL COMMENT '1成长产出 2商城兑换 3抽奖消耗 4抽奖发放 5会员奖励', `source_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '业务单号/记录ID', `remark` varchar(255) NOT NULL DEFAULT '', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_created` (`user_id`, `created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

balance_after 字段很多人嫌冗余,但我强烈建议保留。它记录的是这条流水发生后的积分余额,对账时直接拿最后一条 balance_after 和用户表的当前余额比就行,否则要从头把积分流水累加一遍,几万条数据跑不起。source_type 我固定写在代码枚举里,不允许数据库里出现魔法数字之外的解释。

商城订单也需要单独一张表。注意订单号必须是业务单号,不能直接用自增 ID。

CREATE TABLE `mall_order` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '业务订单号', `user_id` int(11) NOT NULL, `goods_id` int(11) NOT NULL, `goods_name` varchar(120) NOT NULL, `price_credit` int(11) NOT NULL COMMENT '消耗积分(单价)', `quantity` int(11) NOT NULL DEFAULT '1', `total_credit` int(11) NOT NULL, `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0待发货 1已发货 2已取消', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

order_no 我一般用date('YmdHis') . str_pad(mt_rand(1, 999999), 6, '0', STR_PAD_LEFT)生成,或者更稳定一点用 Redis INCR 拼接。总之要在插入前生成,不要用数据库主键。

2.3 用状态机管理“购入-成长-出栏”流程

牛只状态从 0 到 3 是有顺序的,但也经常出现业务需要“跳过”的情况,比如后台管理员为活动补偿,直接手动把牛只设为可出栏。为了不写一堆 if else,我抽了一个状态机类。

class CattleStateMachine { public const WAIT = 0; public const GROWING = 1; public const OUTPUT = 2; public const DONE = 3; private const ALLOW = [ self::WAIT => [self::GROWING], self::GROWING => [self::OUTPUT], self::OUTPUT => [self::DONE], ]; public static function can(int $from, int $to): bool { return in_array($to, self::ALLOW[$from] ?? [], true); } }

所有对 status 的更新,进入 Service 层之前都先跑一次CattleStateMachine::can()。比如用户点击“出栏”,前端传过来 cattle_id,后端读当前状态,如果当前是 1(成长中),才能转到 2(可出栏)。如果当前已经是 2,再点出栏就走 2->3 的流程。如果用数据库触发器做同样的事,业务规则一变就要改表结构,很麻烦。状态机类的好处是规则集中在一处,回头加“加速成长”“一键出栏”这些活动,只需要往 ALLOW 里加映射。

每日产出积分的逻辑,我放在定时任务里:每分钟扫一次 cattle 表,找到 status=1、且grow_start_time距离当前时间超过 24 小时的记录,按牛种倍率计算积分,插入 growth_log,再更新 cattle 表的 output_days 和 total_earn_credit。这里的定时任务要写成游标分页,不能一次性 SELECT 全表,具体问题在第 5 章讲。

3. 把积分商城跑起来:订单、库存与并发扣减

积分商城是这个系统里最直接涉及成本的地方,出问题就是真金白银的损失。我经历过一次库存超卖,用户用积分买走了超出库存的实物商品,最后只能按订单顺序砍单并补发优惠券。所以这章我会重点讲库存扣减的原子操作和订单状态控制。

3.1 商品与多规格库存:一张商品表 + 实时库存字段

积分商城初期不建议搞太复杂的 SKU 体系,大部分商品要么是虚拟卡密,要么是实物单规格。我用一张 mall_goods 表存商品信息,库存直接放在商品表上,不单独起 stock 表。这样下单扣库存就是一条 UPDATE 的事,还能用行锁保证正确性。

CREATE TABLE `mall_goods` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(120) NOT NULL, `category_id` int(11) NOT NULL DEFAULT '0', `credit_price` int(11) NOT NULL COMMENT '积分单价', `stock_total` int(11) NOT NULL DEFAULT '0', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1上架 0下架', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

如果以后要做规格,比如不同颜色、不同尺码,再拆 mall_goods_sku 表。库存在 sku 表上,商品表只保留总库存。我一开始图省事,用SELECT COUNT(*) FROM mall_order WHERE goods_id=?反推剩余库存,结果一有取消订单就乱了,而且并发统计极慢。后来改成库存字段后,问题直接消失。

3.2 下单扣积分的原子操作:先锁行再扣库存

积分商城的下单流程比现金订单简单,因为没有支付回调,但同样不能“先查一下库存,够了再 UPDATE”。那是典型的检查-执行竞态。正确做法是把库存判断和扣减放在同一条 UPDATE 语句里,用 stock_total > 0 作为条件。

Db::startTrans(); try { // 1. 扣库存,where 里必须带上 stock_total > 0 $affected = Db::name('mall_goods') ->where('id', $goodsId) ->where('status', 1) ->where('stock_total', '>', 0) ->dec('stock_total', 1) ->update(); if (!$affected) { throw new Exception('库存不足'); } // 2. 扣用户积分,同样用余额条件 $updated = Db::name('user') ->where('id', $userId) ->where('credit_balance', '>=', $credit) ->dec('credit_balance', $credit) ->update(); if (!$updated) { throw new Exception('积分不足'); } // 3. 插入 mall_order 和 point_log,两者和前面操作在同一个事务里 $orderNo = generateOrderNo(); Db::name('mall_order')->insert([ 'order_no' => $orderNo, 'user_id' => $userId, 'goods_id' => $goodsId, 'goods_name' => $goodsName, 'price_credit' => $credit, 'quantity' => 1, 'total_credit' => $credit, 'status' => 0, ]); Db::name('point_log')->insert([ 'user_id' => $userId, 'change_amount' => -$credit, 'balance_after' => $newBalance, 'source_type' => 2, 'source_id' => $orderNo, 'remark' => '积分商城兑换:' . $goodsName, ]); Db::commit(); } catch (\Throwable $e) { Db::rollback(); throw $e; }

这里要注意几点:dec() 在 ThinkPHP 里会生成SET stock_total = stock_total - 1的原生 SQL,它是原子操作。数据库里stock_total > 0条件配合 InnoDB 的行锁,两个并发请求同时执行 UPDATE 时,后者会被锁住,等前者提交后再执行,这时库存已经减到 0,条件不成立,affected rows 为 0,直接抛异常。这是解决超卖最可靠的一层。积分扣减同理,谁先抢到算谁的,不会出现负数。订单和积分流水必须在同一个事务里,否则用户积分扣了但订单没生成,后面补单更麻烦。

3.3 订单状态机与发货回调

订单状态我定义 0 待发货、1 已发货、2 已取消。用户未发货前可以在后台取消订单,但发货后不能取消。管理员发货后,调用发货接口更新状态,同时回调虚拟商品发货逻辑。

对于实物商品,后台填快递单号后更新订单状态即可。对于虚拟卡密,预售时把卡密导入 card_secret 表,用户下单后不要立刻绑定,而是通过队列异步发放。我在第一版里同步发卡,结果用户点击兑换后页面卡了 5 秒,因为要查卡密、标记已发放、写流水。后来改成下单成功后先返回“兑换成功”,真正的卡密通过站内信或接口异步推送。

取消订单要注意回补库存和积分。回补操作同样要原子:UPDATE mall_goods SET stock_total = stock_total + 1 WHERE id=?,UPDATE user SET credit_balance = credit_balance + ? WHERE id=?,然后在 point_log 里再记一笔正数流水,备注为“订单 XX 取消退回”。这里最容易被忽略的是订单状态判断,必须在事务里先锁住订单行,确保订单当前是待发货状态才允许取消,否则同一笔订单被并发取消两次,就会回补两次。

4. 大转盘抽奖系统:概率控制与防刷

大转盘是消耗积分最快的功能,也是薅羊毛重灾区。我做的时候被刷过两轮:第一次是概率算法不均匀,三等奖比二等奖还少;第二次是接口没限流,一个用户用脚本把当天奖品刷空。后来我把概率配置、幂等控制、发奖对账分开处理,才算稳下来。

4.1 奖品配置与概率分段:用“权重”而不是百分比

抽奖概率不要直接在代码里写死百分比,因为运营会频繁调。更不要在后端存一个 0.001 这种浮点概率,累计误差很难排查。我用的是权重法:每个奖品配一个整数权重,抽奖时把所有启用奖品的权重相加,然后随机一个 1 到总权重的数,依次遍历奖品,落到哪个区间就中哪个。

CREATE TABLE `lottery_prize` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `prize_name` varchar(64) NOT NULL, `level` tinyint(1) NOT NULL COMMENT '奖项等级', `weight` int(11) NOT NULL DEFAULT '1', `total_quantity` int(11) NOT NULL DEFAULT '0' COMMENT '总中奖次数', `remain_quantity` int(11) NOT NULL DEFAULT '0' COMMENT '剩余中奖次数', `daily_limit` int(11) NOT NULL DEFAULT '0' COMMENT '每日中奖上限', `status` tinyint(1) NOT NULL DEFAULT '1', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

抽奖时把所有 status=1 且 remain_quantity>0 的奖品取出来,再执行下面的函数。

function lotteryDraw(array $prizes): array { $totalWeight = array_sum(array_column($prizes, 'weight')); $rand = random_int(1, $totalWeight); $cursor = 0; foreach ($prizes as $prize) { $cursor += $prize['weight']; if ($rand <= $cursor) { return $prize; } } }

这里我用的random_int()而不是mt_rand(),因为要防止可预测性。random_int()基于系统随机源,脚本很难通过枚举攻击。权重法的好处是,运营想调整一等奖概率,直接改 weight 从 1 改成 2,不需要改代码和发布。注意一定要过滤掉库存为 0 的奖品,否则总权重里包含了不可中奖项,实际中奖率会偏低,用户会觉得抽奖是假的。

4.2 抽奖接口的幂等与限次:Redis 计次 + 用户锁

大转盘接口最怕用户并发点击。前端按钮可以置灰,但后端必须防一手。我做了两层:第一层是每日次数限制,用 Redis INCR;第二层是用户级锁,防止同一个用户同时发起多次抽奖请求,导致一次抽奖变两次扣积分。

$dayKey = 'lottery:cnt:' . $userId . ':' . date('Ymd'); $count = Redis::incr($dayKey); if ($count == 1) { Redis::expire($dayKey, 86400); } if ($count > $dailyLimit) { throw new Exception('今日抽奖次数已用完'); } $lockKey = 'lottery:lock:' . $userId; $lockAcquired = Redis::set($lockKey, '1', ['nx', 'ex' => 5]); if (!$lockAcquired) { throw new Exception('抽奖操作正在处理中,请稍后'); } try { // 执行抽奖逻辑 // ... } finally { Redis::del($lockKey); }

nx表示只有在 key 不存在时才能设置成功,ex表示 5 秒过期。这个锁能挡住绝大多数重复提交。要注意锁的释放必须放在 finally 里,如果抽奖逻辑抛了异常,锁也必须删掉,否则这个用户 5 秒内会被一直卡住,体验很差。另外每日次数不能只靠 Redis,因为 Redis 有可能被清掉。我会在用户抽奖成功后把次数同步写进数据库 lottery_record,每天对账时以数据库为准。

还有一点:抽奖接口参数里绝不能让客户端传“抽几次”或者“中奖种子”。我见过有人把随机种子从客户端传进来,然后用种子选奖品,结果用户把 1 到 1000 的种子全遍历一遍,直接找到一等奖的种子值。正确做法是客户端只传一个抽奖请求 token,真实的随机数由服务端在请求处理时生成。

4.3 高并发下的抽奖记录与发奖对账

抽奖结果要立即落地。我先插一条 lottery_record 表,状态为“待发奖”,然后直接返回奖品名称给前端。如果这时发奖逻辑比较复杂,比如要发优惠券、生成卡密,我会丢进队列异步处理,不让用户等。

CREATE TABLE `lottery_record` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `prize_id` int(11) NOT NULL, `prize_name` varchar(64) NOT NULL, `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0待发放 1已发放 2已放弃', `unique_token` varchar(64) NOT NULL COMMENT '幂等令牌', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_token` (`unique_token`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

unique_token 是每次抽奖请求时生成的 UUID,前端提交抽奖时带上,后端先检查这个 token 是否已存在,如果存在就说明是重复请求,直接返回上一次的抽奖结果。这样即使用户疯狂重试,也只会产生一条抽奖记录。异步发奖后,worker 更新 lottery_record.status 为 1,同时写积分流水或者发放卡密。

对账脚本每天早上跑一次,核对三组数:lottery_record 里昨天的记录数是否等于 lottery_prize 里各奖品昨天消耗数之和;用户积分抽奖消耗的总和是否等于所有记录里记录为“已消耗积分”的总和;实际发奖数量是否等于中奖记录中 status=1 的数量。对不上就告警,优先看 lottery_record 表的 unique_token 是否有重复,或者异步发奖任务是否有死信。

5. 避坑指南:亲测最容易翻车的 5 个问题

下面这几个问题是这套系统上线后我真实遇到的,每个都按“现象 → 原因 → 解决”写清楚。你直接在方案里提前规避,能少走至少两周弯路。

5.1 定时任务扫表让数据库 CPU 飙高

现象:上线第二周凌晨 3 点,MySQL CPU 到了 100%,日志里一条SELECT * FROM cattle WHERE status=1的慢查询把库拖垮了。原因:牛只表到了 10 万行,定时任务每分钟全表扫一遍,还要关联 user 表,索引没走,每个查询都在做全表扫描。解决:给 cattle 表加复合索引(status, grow_start_time),并把一次扫全表改成游标分页,每次只取 1000 条。注意不要在一个死循环里数十条十条地查,而是用时间分片,比如只处理grow_start_time < date_sub(now(), interval 24 hour)的记录,同时把待结算的 cattle_id 放进 Redis Set,由 worker 消费。

5.2 积分商城库存扣成负数

现象:用户同时下单,后台显示商品库存变成 -3。原因:下单代码先SELECT stock_total判断大于 0,然后再执行UPDATE ... SET stock_total = stock_total - 1,两个并发请求都查到有货,都成功扣减,于是超卖。解决:像第三章那样,把库存判断和扣减放在同一条 UPDATE 语句的 where 里,MySQL 行锁会串行化这个操作,affected rows = 0 就是没抢到。我后来还加了一条 Redis 预减库存的逻辑,但最终还是以数据库行锁为准,Redis 只做展示层缓存。

5.3 会员折扣与积分日志对不上账

现象:用户是黄金会员,兑换商品时按 9 折计算,但积分流水里扣的是原价,月度对账差了几千积分。原因:下单时把折扣价算进了订单表,但积分流水写在事务外,而且只记了原价。另外会员信息读的是缓存,缓存过期时回源数据库,读到旧等级。解决:把“计算应付积分”收敛为一个纯函数,输入会员等级和商品原价,输出应付积分,订单表和积分流水必须在同事务内写入。会员缓存过期时用版本号校验,或者直接用数据库字段实时读,避免脏读。

5.4 大转盘“首抽必中”失灵

现象:运营配置了首抽必中三等奖,但有些用户首抽抽到“谢谢参与”。原因:我把首抽判断写在抽奖接口入口,用户第一次请求时网络超时,接口被并发重试,第一次调用已经消耗了次数,第二次才真正抽奖,于是命中不了首抽。解决:用 Redis 记录用户是否抽过奖,第一次抽奖时先原子设置lottery:first:{userId}为 1,成功设置的人走必中分支,设置失败的人走普通分支。这个标记必须和抽奖次数扣减在同一个原子流程里,我就吃过这个亏。

5.5 抽奖结果可预测

现象:用户用脚本通过不同参数请求,直接拿到了最高奖。原因:大转盘前端请求里带了rand参数,后端直接用这个参数取模选奖品,而且奖品列表接口把权重值也返回给了前端。解决:随机数只由服务端生成,客户端只传一个抽奖请求唯一 ID。抽奖函数改用random_int()。奖品列表接口只返回奖品名称、图片、剩余数量,绝不返回权重和随机值。这个坑不踩一遍真的很难意识到,概率类功能最容易在这里翻车。

6. 会员特权的联动验证:从积分翻倍到专属抽奖的灰度技巧

会员特权如果只做几个等级放着,用户根本感觉不到区别。我最终把特权做成了可配置的“增益开关”,而不是写死在代码里。比如黄金会员:牛只每日产出积分 +20%、商城兑换 9 折、每天额外一次大转盘抽奖。这些全部放 member_level 表的字段里。

验证特权有没有效果,我习惯从小流量灰度开始。比如先让 5% 的用户体验新版特权逻辑,其余走旧逻辑,对比两组用户的次日留存和积分消耗。

if ($userId % 100 < 5) { // 新特权逻辑 } else { // 旧逻辑 }

灰度一周后,看实验组的日均积分产出是否上升、兑换率是否提高、抽奖参与次数是否增多。如果没有负反馈,再把比例提到 20%,然后再全量。这个百分比条件在代码里必须依赖用户 ID 取模,不能用登录时间或者随机数,否则同一个用户一会新一会旧,数据没法对比。

我最后的习惯是每加一个特权开关,都要在后台留一个维度的日志,至少能回答三个问题:这个特权今天被多少人触发过、平均带来多少额外积分、有没有导致积分消耗下降。没有日志支撑的特权,本质上都是拍脑袋。这套牧场养牛系统做完以后,我最大的感受是功能本身不难,难的是把每一分积分的来源和去向都闭环,抽奖、商城、会员都只是积分流转的表层形式,希望这些经验帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询