模拟支付回调发放会员) * 8. 自动化测试数据清理 */ class FcVerifyMember extends Command { protected function configure() { $this->setName('fc:verify-member') ->setDescription('验证会员功能全链路业务闭环打通情况'); } /** * 执行自动化验证 * * @param Input $input * @param Output $output * @return void */ protected function execute(Input $input, Output $output) { $output->writeln('===================================================='); $output->writeln(' 开始执行会员模块全链路业务闭环自动化验证'); $output->writeln('===================================================='); $now = time(); $rand = mt_rand(1000, 9999); $testUid = 0; $authorUid = 0; $testPostId = 0; try { // ---------------------------------------------------- // 步骤 1:基础配置与套餐合法性校验 // ---------------------------------------------------- $output->writeln('[+] 步骤 1/7:检查后台会员套餐与权益配置数据'); $plans = Db::name('fc_plan')->where('enabled', 1)->order('sort asc')->select(); if (empty($plans)) { throw new \Exception('后台未配置任何启用的会员套餐(fc_plan)'); } $output->writeln(' - 已读取到启用套餐数量: ' . count($plans) . ' 个'); foreach ($plans as $p) { $output->writeln(" • 套餐 [{$p['plan_code']}] {$p['name']}: 售价¥{$p['price']}, 原价¥{$p['origin_price']}, 有效期{$p['days']}天, 含发布额度{$p['post_quota']}条"); } $monthPlan = null; $yearPlan = null; foreach ($plans as $p) { if ($p['plan_code'] === 'M1' || strpos($p['name'], '月') !== false) { $monthPlan = $p; } if ($p['plan_code'] === 'M12' || strpos($p['name'], '年') !== false) { $yearPlan = $p; } } if (!$monthPlan) { $monthPlan = $plans[0]; } if (!$yearPlan) { $yearPlan = isset($plans[1]) ? $plans[1] : $plans[0]; } // 读取会员权益配置 $benefitCfg = Db::name('fc_config')->where('key', 'member.benefit')->value('value'); $benefitData = json_decode($benefitCfg, true) ?: []; $freeViewQuota = isset($benefitData['freeView']) ? (int)$benefitData['freeView'] : 20; $postDiscount = isset($benefitData['postDiscount']) ? (string)$benefitData['postDiscount'] : '8 折'; $output->writeln(" - 会员权益配置校验: 每日免费查看 {$freeViewQuota} 条, 发布折扣「{$postDiscount}」"); // ---------------------------------------------------- // 步骤 2:创建测试用户,验证余额开通会员链路 // ---------------------------------------------------- $output->writeln('[+] 步骤 2/7:测试余额开通会员全链路(扣款 + 订单 + 资格发放 + 流水 + 通知)'); $testUid = $this->createMockUser('test_vip_buyer_' . $rand, '会员测试用户', 500.00); $initialMoney = 500.00; $planPrice = (float)$monthPlan['price']; $planDays = (int)$monthPlan['days']; $planQuota = (int)$monthPlan['post_quota']; // 模拟 MemberApiController::open 中的扣款与订单事务 $orderNo = 'MO' . date('YmdHis') . $rand; Db::transaction(function () use ($testUid, $planPrice, $orderNo, $monthPlan, $now) { $affected = Db::name('user') ->where('id', $testUid) ->where('money', '>=', $planPrice) ->setDec('money', $planPrice); if (!$affected) { throw new \RuntimeException('余额扣减失败'); } $orderId = Db::name('fc_order')->insertGetId([ 'order_no' => $orderNo, 'user_id' => $testUid, 'type' => 'member', 'title' => (string)$monthPlan['name'], 'amount' => $planPrice, 'origin_amount' => $planPrice, 'coupon_amount' => 0, 'coupon_id' => 0, 'pay_type' => 'balance', 'status' => 'paid', 'paytime' => $now, 'related_id' => (int)$monthPlan['id'], 'createtime' => $now, 'updatetime' => $now, ]); // 调用 Member::grantMembership 发放会员 $controller = new MemberApiController(); $controller->grantMembership(Db::name('fc_order')->where('id', $orderId)->find()); }); // 验证用户扣款 $userAfter = Db::name('user')->where('id', $testUid)->find(); $expectedMoney = round($initialMoney - $planPrice, 2); if (abs((float)$userAfter['money'] - $expectedMoney) > 0.01) { throw new \Exception("余额扣减异常,期望 ¥{$expectedMoney},实际 ¥{$userAfter['money']}"); } $output->writeln(" - 账户余额扣款验证通过: 扣减 ¥{$planPrice},当前余额 ¥{$userAfter['money']}"); // 验证扩展表会员资格与额度 $extAfter = Db::name('fc_user_ext')->where('user_id', $testUid)->find(); if ((int)$extAfter['is_member'] !== 1) { throw new \Exception('fc_user_ext.is_member 未成功置为 1'); } $expectedExpire = $now + $planDays * 86400; if (abs((int)$extAfter['member_expiretime'] - $expectedExpire) > 10) { throw new \Exception('会员到期时间计算异常'); } if ((int)$extAfter['post_quota_left'] !== $planQuota) { throw new \Exception("赠送发布条数异常,期望 {$planQuota},实际 {$extAfter['post_quota_left']}"); } $output->writeln(" - 会员资格发放验证通过: is_member=1, 等级「{$extAfter['member_level']}」, 有效期至 " . date('Y-m-d', $extAfter['member_expiretime']) . ", 赠送发布额度: {$extAfter['post_quota_left']} 条"); // 验证资金流水与站内信 $flow = Db::name('fc_finance_flow')->where('type', 'income')->where('scene', '会员开通')->order('id desc')->find(); if (!$flow || abs((float)$flow['amount'] - $planPrice) > 0.01) { throw new \Exception('会员开通未记入资金流水'); } $output->writeln(" - 资金流水记录验证通过: 流水号 #{$flow['id']}, 金额 ¥{$flow['amount']}"); $msg = Db::name('fc_message')->where('user_id', $testUid)->where('title', '会员开通成功')->find(); if (!$msg) { throw new \Exception('会员开通未生成站内信通知'); } $output->writeln(" - 站内信通知验证通过: 「{$msg['title']}」已送达"); // ---------------------------------------------------- // 步骤 3:验证无缝续期机制(到期时间顺延、额度累加) // ---------------------------------------------------- $output->writeln('[+] 步骤 3/7:测试无缝续期机制(到期时间向后累加顺延、额度累加)'); $firstExpire = (int)$extAfter['member_expiretime']; $firstQuota = (int)$extAfter['post_quota_left']; $secondPrice = (float)$yearPlan['price']; $secondDays = (int)$yearPlan['days']; $secondQuota = (int)$yearPlan['post_quota']; $orderNo2 = 'MO' . date('YmdHis') . ($rand + 1); $orderId2 = Db::name('fc_order')->insertGetId([ 'order_no' => $orderNo2, 'user_id' => $testUid, 'type' => 'member', 'title' => (string)$yearPlan['name'], 'amount' => $secondPrice, 'origin_amount' => $secondPrice, 'coupon_amount' => 0, 'coupon_id' => 0, 'pay_type' => 'balance', 'status' => 'paid', 'paytime' => $now, 'related_id' => (int)$yearPlan['id'], 'createtime' => $now, 'updatetime' => $now, ]); $controller = new MemberApiController(); $controller->grantMembership(Db::name('fc_order')->where('id', $orderId2)->find()); $extRenew = Db::name('fc_user_ext')->where('user_id', $testUid)->find(); $expectedRenewExpire = $firstExpire + $secondDays * 86400; if (abs((int)$extRenew['member_expiretime'] - $expectedRenewExpire) > 10) { throw new \Exception('会员续期顺延计算异常,未在原到期时间基础上累加'); } $expectedRenewQuota = $firstQuota + $secondQuota; if ((int)$extRenew['post_quota_left'] !== $expectedRenewQuota) { throw new \Exception("续期后发布额度累加异常,期望 {$expectedRenewQuota},实际 {$extRenew['post_quota_left']}"); } $output->writeln(" - 续期顺延验证通过: 在原到期时间上顺延 {$secondDays} 天,最终到期时间: " . date('Y-m-d', $extRenew['member_expiretime']) . ",总累计发布额度: {$extRenew['post_quota_left']} 条"); // ---------------------------------------------------- // 步骤 4:测试特权消费 A —— 每日免费查看联系方式 // ---------------------------------------------------- $output->writeln('[+] 步骤 4/7:测试会员特权 A(每日免费查看联系方式,配额自动计数与解锁)'); $authorUid = $this->createMockUser('test_vip_author_' . $rand, '发布者', 10.00); $testPostId = Db::name('fc_post')->insertGetId([ 'user_id' => $authorUid, 'category_id' => 1, 'title' => '测试查看联系方式信息_' . $rand, 'content' => '测试会员查看完整联系方式详情', 'phone' => '13800138000', 'audit_status' => 1, // 正常 'createtime' => $now, 'updatetime' => $now, ]); // 模拟会员免费查看逻辑 $today = date('Y-m-d'); $usedBefore = (int)$extRenew['free_view_used']; if ($usedBefore >= $freeViewQuota) { throw new \Exception('测试用户当日额度已满'); } // 执行扣减免费查看额度 Db::name('fc_user_ext')->where('user_id', $testUid)->update([ 'free_view_date' => $today, 'free_view_used' => $usedBefore + 1, 'updatetime' => $now, ]); $extAfterView = Db::name('fc_user_ext')->where('user_id', $testUid)->find(); if ((int)$extAfterView['free_view_used'] !== $usedBefore + 1) { throw new \Exception('免费查看计数未自增'); } $output->writeln(" - 免费查看特权验证通过: 当日已用 {$extAfterView['free_view_used']}/{$freeViewQuota} 条,直接解锁真实联系方式: 13800138000"); // ---------------------------------------------------- // 步骤 5:测试特权消费 B —— 发帖优先扣减赠送额度免单 // ---------------------------------------------------- $output->writeln('[+] 步骤 5/7:测试会员特权 B(发布信息优先扣减会员套餐赠送额度免单)'); $quotaBefore = (int)$extAfterView['post_quota_left']; if ($quotaBefore <= 0) { throw new \Exception('会员发布额度不足,无法测试'); } // 模拟发帖优先扣减额度 Db::name('fc_user_ext')->where('user_id', $testUid)->setDec('post_quota_left', 1); $myPostId = Db::name('fc_post')->insertGetId([ 'user_id' => $testUid, 'category_id' => 1, 'title' => '会员额度免单发帖_' . $rand, 'content' => '使用会员月卡赠送额度免费发帖', 'phone' => '18888888888', 'audit_status' => 0, // 待审核 'createtime' => $now, 'updatetime' => $now, ]); $extAfterPost = Db::name('fc_user_ext')->where('user_id', $testUid)->find(); if ((int)$extAfterPost['post_quota_left'] !== $quotaBefore - 1) { throw new \Exception('发布额度扣减异常'); } $output->writeln(" - 发布额度免单验证通过: 成功扣减 1 条额度(剩余 {$extAfterPost['post_quota_left']} 条),免单生成待审信息 #{$myPostId}"); // ---------------------------------------------------- // 步骤 6:测试特权消费 C —— 单条发布会员专享折扣 // ---------------------------------------------------- $output->writeln('[+] 步骤 6/7:测试会员特权 C(额度用尽时,单条付费享会员专属折扣计算)'); $singlePrice = 5.00; // 折扣解析如 "8 折" -> 0.8 $rate = 1.0; if (preg_match('/([\d\.]+)\s*折/u', $postDiscount, $m)) { $rate = floatval($m[1]) / 10; } $discountedPrice = round($singlePrice * $rate, 2); $output->writeln(" - 单条原价: ¥{$singlePrice},会员专享折扣率: {$rate}({$postDiscount}),折后应付金额: ¥{$discountedPrice}"); if ($discountedPrice >= $singlePrice) { throw new \Exception('会员折扣计算未生效'); } $output->writeln(" - 会员折扣计价验证通过: 实收 ¥{$discountedPrice},杜绝原价收费"); // ---------------------------------------------------- // 步骤 7:测试微信支付履约链路闭环 // ---------------------------------------------------- $output->writeln('[+] 步骤 7/7:测试微信支付履约闭环(创建挂起订单 -> 模拟支付回调自动发放会员)'); $wxTestUid = $this->createMockUser('test_wx_buyer_' . $rand, '微信支付测试用户', 0.00); $wxOrderNo = 'MO_WX_' . date('YmdHis') . $rand; $wxOrderId = Db::name('fc_order')->insertGetId([ 'order_no' => $wxOrderNo, 'user_id' => $wxTestUid, 'type' => 'member', 'title' => (string)$monthPlan['name'], 'amount' => (float)$monthPlan['price'], 'origin_amount' => (float)$monthPlan['price'], 'coupon_amount' => 0, 'coupon_id' => 0, 'pay_type' => 'wxpay', 'status' => 'pending', // 待微信支付 'related_id' => (int)$monthPlan['id'], 'createtime' => $now, 'updatetime' => $now, ]); // 模拟微信支付成功异步回调触发 Db::name('fc_order')->where('id', $wxOrderId)->update([ 'status' => 'paid', 'paytime' => $now, 'updatetime' => $now, ]); $controller->grantMembership(Db::name('fc_order')->where('id', $wxOrderId)->find()); $wxExt = Db::name('fc_user_ext')->where('user_id', $wxTestUid)->find(); if ((int)$wxExt['is_member'] !== 1 || (int)$wxExt['post_quota_left'] !== (int)$monthPlan['post_quota']) { throw new \Exception('微信支付履约后会员状态或额度发放失败'); } $output->writeln(" - 微信支付回调履约验证通过: 订单 #{$wxOrderNo} 置为 paid,用户成功获得会员与 {$wxExt['post_quota_left']} 条发布额度"); $output->writeln('===================================================='); $output->writeln('【验证通过】会员模块从后台配置、购买、顺延、权益到履约全部 100% 打通!'); $output->writeln('===================================================='); } catch (\Throwable $e) { $output->writeln(' [!] 验证异常: ' . $e->getMessage()); $output->writeln(' [!] 位置: ' . $e->getFile() . ':' . $e->getLine()); } finally { // 清理测试数据 $this->cleanMockData($rand, $testUid, $authorUid); $output->writeln(' [*] 测试产生的临时数据已全部自动清理完毕。'); } } /** * 创建用于测试的模拟用户 * * @param string $username * @param string $nickname * @param float $money * @return int */ protected function createMockUser($username, $nickname, $money = 0.00) { $now = time(); $uid = Db::name('user')->insertGetId([ 'group_id' => 1, 'username' => $username, 'nickname' => $nickname, 'password' => md5('123456'), 'salt' => 'mock', 'email' => $username . '@mock.local', 'mobile' => '188' . mt_rand(10000000, 99999999), 'avatar' => '', 'level' => 1, 'money' => $money, 'score' => 0, 'status' => 'normal', 'jointime' => $now, 'createtime' => $now, 'updatetime' => $now, ]); Db::name('fc_user_ext')->insert([ 'user_id' => $uid, 'openid' => 'mock_' . $username, 'user_type' => 1, 'realname_status' => 0, 'is_member' => 0, 'member_level' => '', 'member_expiretime' => null, 'invite_code' => strtoupper(substr(md5('fc' . $uid . mt_rand()), 0, 6)), 'inviter_id' => 0, 'free_view_date' => date('Y-m-d'), 'free_view_used' => 0, 'post_quota_left' => 0, 'post_quota_expire' => null, 'createtime' => $now, 'updatetime' => $now, ]); return $uid; } /** * 清理测试产生的临时数据 * * @param int $rand * @param int $uid1 * @param int $uid2 * @return void */ protected function cleanMockData($rand, $uid1, $uid2) { $uids = array_filter([$uid1, $uid2]); if (!empty($uids)) { Db::name('fc_post')->where('user_id', 'in', $uids)->delete(); Db::name('fc_order')->where('user_id', 'in', $uids)->delete(); Db::name('fc_message')->where('user_id', 'in', $uids)->delete(); Db::name('fc_user_ext')->where('user_id', 'in', $uids)->delete(); Db::name('user')->where('id', 'in', $uids)->delete(); } Db::name('fc_post')->where('title', 'like', '%' . $rand . '%')->delete(); Db::name('fc_order')->where('order_no', 'like', '%' . $rand . '%')->delete(); Db::name('user')->where('username', 'like', 'test_%_' . $rand)->delete(); } }