diff --git a/addons/shopro/controller/commission/Agent.php b/addons/shopro/controller/commission/Agent.php index bc5e31a..6cb2fdd 100644 --- a/addons/shopro/controller/commission/Agent.php +++ b/addons/shopro/controller/commission/Agent.php @@ -6,6 +6,7 @@ use think\Db; use app\admin\model\shopro\user\User as UserModel; use app\admin\model\shopro\commission\Agent as AgentModel; use app\admin\model\shopro\goods\Goods as GoodsModel; +use app\admin\model\shopro\partner\Reward as RewardModel; use addons\shopro\service\Wallet; @@ -268,11 +269,21 @@ class Agent extends Commission $user->team_level = ($user->parent_user_id == $agentId) ? 1 : 2; $rawMobile = $user->mobile ? (string) $user->mobile : ''; + $maskedMobile = $rawMobile ? (strlen($rawMobile) == 11 ? substr_replace($rawMobile, '****', 3, 4) : $rawMobile) : ''; + $user->mobile = $maskedMobile; + $user->mobile_hide = $maskedMobile; $user->mobile_raw = $rawMobile; - $user->mobile_hide = $rawMobile ? (strlen($rawMobile) == 11 ? substr_replace($rawMobile, '****', 3, 4) : $rawMobile) : ''; + + // 间推所属推荐人手机号脱敏 + if ($user->parentUser && $user->parentUser->mobile) { + $pMobile = (string) $user->parentUser->mobile; + $pMasked = strlen($pMobile) == 11 ? substr_replace($pMobile, '****', 3, 4) : $pMobile; + $user->parentUser->mobile = $pMasked; + $user->parentUser->mobile_hide = $pMasked; + } }); - // 9. 计算整体团队健康度统计 + // 9. 计算整体团队健康度与综合收益业绩统计 $summary = $this->getTeamHealthSummary($agentId, $level1Ids, $windowStart, $expiringStart); $result = $data->toArray(); @@ -282,7 +293,7 @@ class Agent extends Commission } /** - * 计算团队保活健康度统计 + * 计算团队保活健康度与综合收益统计 */ protected function getTeamHealthSummary($agentId, $level1Ids, $windowStart, $expiringStart) { @@ -298,6 +309,10 @@ class Agent extends Commission 'expiring_count' => 0, 'inactive_count' => 0, 'partner_count' => 0, + 'team_total_consume' => '0.00', + 'team_total_reward' => '0.00', + 'team_month_reward' => '0.00', + 'team_order_count' => 0, ]; } @@ -306,6 +321,7 @@ class Agent extends Commission $expiringCount = 0; $inactiveCount = 0; $partnerCount = 0; + $teamOrderCount = 0; foreach ($agents as $ag) { $level = intval($ag->level ?? 0); @@ -323,6 +339,8 @@ class Agent extends Commission } else { $inactiveCount++; } + + $teamOrderCount += intval($ag->child_order_count_0 ?? 0); } $missingAgentCount = count($teamUserIds) - count($agents); @@ -330,12 +348,31 @@ class Agent extends Commission $inactiveCount += $missingAgentCount; } + // 1. 团队累计自购总业绩 + $teamTotalConsume = UserModel::whereIn('id', $teamUserIds)->sum('total_consume'); + + // 2. 团队成员为当前用户贡献的累计收益与本月收益 + $monthStart = strtotime(date('Y-m-01 00:00:00')); + $teamTotalReward = RewardModel::where('user_id', $agentId) + ->whereIn('buyer_id', $teamUserIds) + ->where('status', 1) + ->sum('amount'); + $teamMonthReward = RewardModel::where('user_id', $agentId) + ->whereIn('buyer_id', $teamUserIds) + ->where('status', 1) + ->where('createtime', '>=', $monthStart) + ->sum('amount'); + return [ 'total_count' => count($teamUserIds), 'active_count' => $activeCount, 'expiring_count' => $expiringCount, 'inactive_count' => $inactiveCount, 'partner_count' => $partnerCount, + 'team_total_consume' => \addons\shopro\service\partner\IdentityService::formatMoney($teamTotalConsume), + 'team_total_reward' => \addons\shopro\service\partner\IdentityService::formatMoney($teamTotalReward), + 'team_month_reward' => \addons\shopro\service\partner\IdentityService::formatMoney($teamMonthReward), + 'team_order_count' => $teamOrderCount, ]; } diff --git a/addons/shopro/controller/partner/Index.php b/addons/shopro/controller/partner/Index.php index b2af48e..7913112 100644 --- a/addons/shopro/controller/partner/Index.php +++ b/addons/shopro/controller/partner/Index.php @@ -40,6 +40,7 @@ class Index extends Commission { $userId = $this->service->user->id; $tab = $this->request->param('tab', 'all'); + $buyerId = intval($this->request->param('buyer_id', 0)); $query = RewardModel::where('user_id', $userId)->with(['buyer', 'order']); $tabMap = [ @@ -52,6 +53,9 @@ class Index extends Commission if ($tab !== 'all' && isset($tabMap[$tab])) { $query->whereIn('reward_type', $tabMap[$tab]); } + if ($buyerId > 0) { + $query->where('buyer_id', $buyerId); + } $list = $query->order('id', 'desc')->paginate($this->request->param('list_rows', 10)); $rows = []; @@ -74,11 +78,26 @@ class Index extends Commission ]; } + $buyerInfo = null; + if ($buyerId > 0) { + $buyerUser = \app\admin\model\shopro\user\User::get($buyerId); + if ($buyerUser) { + $rawMobile = (string) ($buyerUser->mobile ?? ''); + $buyerInfo = [ + 'id' => $buyerUser->id, + 'nickname' => $buyerUser->nickname, + 'avatar' => $buyerUser->avatar, + 'mobile' => $rawMobile ? (strlen($rawMobile) == 11 ? substr_replace($rawMobile, '****', 3, 4) : $rawMobile) : '', + ]; + } + } + $this->success('ok', [ 'total' => $list->total(), 'per_page' => $list->listRows(), 'current_page' => $list->currentPage(), 'data' => $rows, + 'buyer_info' => $buyerInfo, ]); } diff --git a/addons/shopro/controller/third/Wechat.php b/addons/shopro/controller/third/Wechat.php index f34456f..a11434f 100644 --- a/addons/shopro/controller/third/Wechat.php +++ b/addons/shopro/controller/third/Wechat.php @@ -44,7 +44,10 @@ class Wechat extends Common }); if ($result) { - $this->success('登陆成功'); + $auth = \app\common\library\Auth::instance(); + $this->success('登陆成功', [ + 'token' => $auth->isLogin() ? $auth->getToken() : '' + ]); } $this->error('登陆失败'); } diff --git a/addons/shopro/filter/commission/AgentFilter.php b/addons/shopro/filter/commission/AgentFilter.php index 444b4df..3bacf90 100644 --- a/addons/shopro/filter/commission/AgentFilter.php +++ b/addons/shopro/filter/commission/AgentFilter.php @@ -39,9 +39,60 @@ class AgentFilter extends BaseFilter if ($value === 'county') { return $query->where('is_county', 1); } + if ($value === 'partner') { + return $query->where('is_partner', 1); + } return $query; } + /** + * 等级筛选 (如 1 为品酒师) + */ + public function level($query, $data) + { + $value = $this->plainValue($data); + if ($value === '' || $value === null) { + return $query; + } + return $query->where('level', (int)$value); + } + + /** + * 酒道馆筛选 (1 为酒道馆) + */ + public function isVenue($query, $data) + { + $value = $this->plainValue($data); + if ($value === '' || $value === null) { + return $query; + } + return $query->where('is_venue', (int)$value); + } + + /** + * 区县代理筛选 (1 为区县代理) + */ + public function isCounty($query, $data) + { + $value = $this->plainValue($data); + if ($value === '' || $value === null) { + return $query; + } + return $query->where('is_county', (int)$value); + } + + /** + * 合伙股东筛选 (1 为合伙股东) + */ + public function isPartner($query, $data) + { + $value = $this->plainValue($data); + if ($value === '' || $value === null) { + return $query; + } + return $query->where('is_partner', (int)$value); + } + public function active30d($query, $data) { $value = $this->plainValue($data); diff --git a/addons/shopro/service/order/OrderCreate.php b/addons/shopro/service/order/OrderCreate.php index c352fd0..91daf40 100755 --- a/addons/shopro/service/order/OrderCreate.php +++ b/addons/shopro/service/order/OrderCreate.php @@ -66,7 +66,7 @@ class OrderCreate /** * 用户选择的兑换券抵扣金额 */ - protected $voucher_fee = 0; + protected $voucher_fee = null; /** * 用户备注 @@ -158,7 +158,7 @@ class OrderCreate $this->invoice_id = $params['invoice_id'] ?? 0; $this->coupon_id = $params['coupon_id'] ?? 0; - $this->voucher_fee = isset($params['voucher_fee']) ? floatval($params['voucher_fee']) : 0; + $this->voucher_fee = (isset($params['voucher_fee']) && $params['voucher_fee'] !== '') ? floatval($params['voucher_fee']) : null; $this->remark = $params['remark'] ?? ''; $this->money = (isset($params['money']) && $params['money'] > 0) ? $params['money'] : 0; @@ -611,7 +611,14 @@ class OrderCreate $available = (new \addons\shopro\service\partner\VoucherService())->available($this->user->id); } $this->orderData['voucher_available'] = $available; - $want = max(0, round((float) $this->voucher_fee, 2)); + + // 兑换商城订单:若未显式指定抵扣额度,默认自动全额应用可用兑换券 + if ($this->voucher_fee === null) { + $want = $this->order_type == 'score' ? (float) $available : 0; + } else { + $want = max(0, round((float) $this->voucher_fee, 2)); + } + $apply = min($want, (float) $available, (float) $this->orderData['pay_fee']); $apply = $apply > 0 ? $apply : 0; $this->orderData['voucher_fee'] = $apply; diff --git a/addons/shopro/service/partner/MakerDiffReward.php b/addons/shopro/service/partner/MakerDiffReward.php index 56bfcc3..46463ab 100644 --- a/addons/shopro/service/partner/MakerDiffReward.php +++ b/addons/shopro/service/partner/MakerDiffReward.php @@ -7,9 +7,11 @@ namespace addons\shopro\service\partner; */ class MakerDiffReward { - public function run($order, $buyerId, $chain, ActiveService $active) + public function run($order, $buyerId, $chain, ActiveService $active, $rules = null) { - $bonus = PartnerConfig::makerBonus(); + $bonus = (!empty($rules['maker_bonus']) && is_array($rules['maker_bonus'])) + ? $rules['maker_bonus'] + : PartnerConfig::makerBonus(); $seen = 0; $paid = []; diff --git a/addons/shopro/service/partner/MakerPeerReward.php b/addons/shopro/service/partner/MakerPeerReward.php index 5c76a17..5cbb26a 100644 --- a/addons/shopro/service/partner/MakerPeerReward.php +++ b/addons/shopro/service/partner/MakerPeerReward.php @@ -7,11 +7,17 @@ namespace addons\shopro\service\partner; */ class MakerPeerReward { - public function run($order, $buyerId, $chain, $diffResult, ActiveService $active) + public function run($order, $buyerId, $chain, $diffResult, ActiveService $active, $rules = null) { - $bonus = PartnerConfig::makerBonus(); - $peerRate = (float) PartnerConfig::get('partner_maker_peer_rate', 0.10); - $overRate = (float) PartnerConfig::get('partner_maker_overtaken_rate', 0.05); + $bonus = (!empty($rules['maker_bonus']) && is_array($rules['maker_bonus'])) + ? $rules['maker_bonus'] + : PartnerConfig::makerBonus(); + $peerRate = (isset($rules['maker_peer_rate']) && is_numeric($rules['maker_peer_rate'])) + ? (float) $rules['maker_peer_rate'] + : (float) PartnerConfig::get('partner_maker_peer_rate', 0.10); + $overRate = (isset($rules['maker_overtaken_rate']) && is_numeric($rules['maker_overtaken_rate'])) + ? (float) $rules['maker_overtaken_rate'] + : (float) PartnerConfig::get('partner_maker_overtaken_rate', 0.05); $minAmount = (float) PartnerConfig::get('partner_peer_min_amount', 0.10); if ($minAmount <= 0) { $minAmount = 0.10; diff --git a/addons/shopro/service/partner/PartnerConfig.php b/addons/shopro/service/partner/PartnerConfig.php index 411333c..d45441a 100644 --- a/addons/shopro/service/partner/PartnerConfig.php +++ b/addons/shopro/service/partner/PartnerConfig.php @@ -167,8 +167,143 @@ class PartnerConfig ]; } - public static function dailyTypes() + public static function defaultRules() { - return ['venue_share', 'county_share', 'madow_share', 'voucher_release']; + return [ + 'push3_amounts' => self::push3Amounts(), + 'maker_bonus' => self::makerBonus(), + 'maker_peer_rate' => (float) self::get('partner_maker_peer_rate', 0.10), + 'maker_overtaken_rate' => (float) self::get('partner_maker_overtaken_rate', 0.05), + 'venue_split' => (float) self::get('partner_venue_split', 50), + 'county_full' => (float) self::get('partner_county_full', 100), + 'venue_peer_rate' => (float) self::get('partner_venue_peer_rate', 0.10), + 'county_peer_rate' => (float) self::get('partner_county_peer_rate', 0.10), + 'voucher_times' => (float) self::get('partner_voucher_times', 1.5), + 'voucher_days' => (int) self::get('partner_voucher_days', 365), + 'venue_share' => (float) self::get('partner_venue_share_per_order', 100), + 'county_share' => (float) self::get('partner_county_share_per_order', 50), + 'is_custom' => false, + ]; + } + + /** + * 获取指定商品/规格的生效分成规则(优先独立规则,未设置项回退默认规则) + */ + public static function getEffectiveRules($goodsId, $skuPriceId = 0) + { + $rules = self::defaultRules(); + $goodsId = intval($goodsId); + if ($goodsId <= 0) { + return $rules; + } + + try { + $cg = \think\Db::name('shopro_commission_goods')->where('goods_id', $goodsId)->find(); + if (!$cg || intval($cg['status'] ?? 0) !== 1) { + // 未参与计奖 + $rules['status'] = 0; + return $rules; + } + $rules['status'] = 1; + $rules['commission_order_status'] = intval($cg['commission_order_status'] ?? 1); + $selfRules = intval($cg['self_rules'] ?? 0); + $rules['self_rules'] = $selfRules; + + if ($selfRules !== 1 && $selfRules !== 2) { + return $rules; // 默认规则 + } + + $raw = $cg['commission_rules'] ?? ''; + $custom = is_array($raw) ? $raw : (json_decode($raw, true) ?: []); + if (empty($custom)) { + return $rules; + } + + // 多规格支持:优先匹配特定规格配置 + $skuKey = intval($skuPriceId); + if ($skuKey > 0 && isset($custom[$skuKey]) && is_array($custom[$skuKey])) { + $custom = $custom[$skuKey]; + } elseif ($skuKey > 0 && isset($custom['sku_' . $skuKey]) && is_array($custom['sku_' . $skuKey])) { + $custom = $custom['sku_' . $skuKey]; + } + + if (isset($custom['push3_amounts']) && is_array($custom['push3_amounts']) && count($custom['push3_amounts']) >= 3) { + $rules['push3_amounts'] = array_map('floatval', array_values($custom['push3_amounts'])); + } + if (isset($custom['maker_bonus']) && is_array($custom['maker_bonus'])) { + $mb = []; + foreach ($custom['maker_bonus'] as $lvl => $val) { + $mb[intval($lvl)] = floatval($val); + } + $rules['maker_bonus'] = $mb; + } + if (isset($custom['maker_peer_rate']) && is_numeric($custom['maker_peer_rate'])) { + $rules['maker_peer_rate'] = floatval($custom['maker_peer_rate']); + } + if (isset($custom['maker_overtaken_rate']) && is_numeric($custom['maker_overtaken_rate'])) { + $rules['maker_overtaken_rate'] = floatval($custom['maker_overtaken_rate']); + } + if (isset($custom['venue_split']) && is_numeric($custom['venue_split'])) { + $rules['venue_split'] = floatval($custom['venue_split']); + } + if (isset($custom['county_full']) && is_numeric($custom['county_full'])) { + $rules['county_full'] = floatval($custom['county_full']); + } + if (isset($custom['venue_peer_rate']) && is_numeric($custom['venue_peer_rate'])) { + $rules['venue_peer_rate'] = floatval($custom['venue_peer_rate']); + } + if (isset($custom['county_peer_rate']) && is_numeric($custom['county_peer_rate'])) { + $rules['county_peer_rate'] = floatval($custom['county_peer_rate']); + } + if (isset($custom['voucher_times']) && is_numeric($custom['voucher_times'])) { + $rules['voucher_times'] = floatval($custom['voucher_times']); + } + if (isset($custom['voucher_days']) && is_numeric($custom['voucher_days'])) { + $rules['voucher_days'] = intval($custom['voucher_days']); + } + if (isset($custom['venue_share']) && is_numeric($custom['venue_share'])) { + $rules['venue_share'] = floatval($custom['venue_share']); + } + if (isset($custom['county_share']) && is_numeric($custom['county_share'])) { + $rules['county_share'] = floatval($custom['county_share']); + } + + $rules['is_custom'] = true; + return $rules; + } catch (\Exception $e) { + return $rules; + } + } + + /** + * 解析订单中主计奖商品的生效分成规则 + */ + public static function getOrderEffectiveRules($order) + { + $default = self::defaultRules(); + $items = $order['items'] ?? []; + if (empty($items) && !empty($order['id'])) { + $fresh = \app\admin\model\shopro\order\Order::with(['items'])->where('id', $order['id'])->find(); + if ($fresh) { + $items = $fresh['items'] ?? []; + } + } + + // 优先寻找设置了独立分成规则的计奖商品 + foreach ($items as $item) { + $ext = is_array($item['ext'] ?? null) ? $item['ext'] : (json_decode($item['ext'] ?? '', true) ?: []); + if (isset($ext['is_commission']) && !$ext['is_commission']) { + continue; + } + $goodsId = intval($item['goods_id'] ?? 0); + $skuId = intval($item['goods_sku_price_id'] ?? 0); + $rules = self::getEffectiveRules($goodsId, $skuId); + if (!empty($rules['is_custom'])) { + return $rules; + } + } + + return $default; } } + diff --git a/addons/shopro/service/partner/PartnerRewardService.php b/addons/shopro/service/partner/PartnerRewardService.php index 54e2646..42aad3e 100644 --- a/addons/shopro/service/partner/PartnerRewardService.php +++ b/addons/shopro/service/partner/PartnerRewardService.php @@ -40,16 +40,19 @@ class PartnerRewardService $parentChain = $chainService->collect($buyerId, false); $selfChain = $chainService->collect($buyerId, true); + // 动态解析商品独立分成规则与全局默认配置 + $orderRules = PartnerConfig::getOrderEffectiveRules($order); + $parent = $parentChain[0] ?? null; - (new Push3Reward())->run($order, $buyerId, $parent, $active); + (new Push3Reward())->run($order, $buyerId, $parent, $active, $orderRules); - $diff = (new MakerDiffReward())->run($order, $buyerId, $parentChain, $active); - (new MakerPeerReward())->run($order, $buyerId, $parentChain, $diff, $active); + $diff = (new MakerDiffReward())->run($order, $buyerId, $parentChain, $active, $orderRules); + (new MakerPeerReward())->run($order, $buyerId, $parentChain, $diff, $active, $orderRules); - $team = (new TeamSplitReward())->run($order, $buyerId, $selfChain, $active); - (new PeerShareReward())->run($order, $buyerId, $selfChain, $team, $active); + $team = (new TeamSplitReward())->run($order, $buyerId, $selfChain, $active, $orderRules); + (new PeerShareReward())->run($order, $buyerId, $selfChain, $team, $active, $orderRules); - (new VoucherService())->grant($order, $buyerId); + (new VoucherService())->grant($order, $buyerId, $orderRules); return true; }); diff --git a/addons/shopro/service/partner/PeerShareReward.php b/addons/shopro/service/partner/PeerShareReward.php index cee6880..e4fe64b 100644 --- a/addons/shopro/service/partner/PeerShareReward.php +++ b/addons/shopro/service/partner/PeerShareReward.php @@ -7,10 +7,14 @@ namespace addons\shopro\service\partner; */ class PeerShareReward { - public function run($order, $buyerId, $chain, $teamResult, ActiveService $active) + public function run($order, $buyerId, $chain, $teamResult, ActiveService $active, $rules = null) { - $venueRate = (float) PartnerConfig::get('partner_venue_peer_rate', 0.10); - $countyRate = (float) PartnerConfig::get('partner_county_peer_rate', 0.10); + $venueRate = (isset($rules['venue_peer_rate']) && is_numeric($rules['venue_peer_rate'])) + ? (float) $rules['venue_peer_rate'] + : (float) PartnerConfig::get('partner_venue_peer_rate', 0.10); + $countyRate = (isset($rules['county_peer_rate']) && is_numeric($rules['county_peer_rate'])) + ? (float) $rules['county_peer_rate'] + : (float) PartnerConfig::get('partner_county_peer_rate', 0.10); if (!empty($teamResult['venue'])) { $this->peer($order, $buyerId, $chain, $teamResult['venue'], 'is_venue', 'venue_peer', 'venue', $venueRate, $active, '酒道馆'); diff --git a/addons/shopro/service/partner/Push3Reward.php b/addons/shopro/service/partner/Push3Reward.php index b672104..d2f0d3c 100644 --- a/addons/shopro/service/partner/Push3Reward.php +++ b/addons/shopro/service/partner/Push3Reward.php @@ -9,7 +9,7 @@ use app\admin\model\shopro\commission\Agent as AgentModel; */ class Push3Reward { - public function run($order, $buyerId, $parent, ActiveService $active) + public function run($order, $buyerId, $parent, ActiveService $active, $rules = null) { if (!$parent) { PartnerRewardService::writeSkip($order, $buyerId, 0, 'push3', 'taster', 'no_parent', '暂无推荐人,不计提推荐开拓奖'); @@ -35,7 +35,9 @@ class Push3Reward $agent->direct_order_seq = intval($agent->direct_order_seq) + 1; $agent->save(); - $amounts = PartnerConfig::push3Amounts(); + $amounts = (!empty($rules['push3_amounts']) && is_array($rules['push3_amounts'])) + ? $rules['push3_amounts'] + : PartnerConfig::push3Amounts(); $seq = intval($agent->direct_order_seq); $amount = $amounts[($seq - 1) % 3]; diff --git a/addons/shopro/service/partner/TeamSplitReward.php b/addons/shopro/service/partner/TeamSplitReward.php index 735bbd2..368a059 100644 --- a/addons/shopro/service/partner/TeamSplitReward.php +++ b/addons/shopro/service/partner/TeamSplitReward.php @@ -7,10 +7,14 @@ namespace addons\shopro\service\partner; */ class TeamSplitReward { - public function run($order, $buyerId, $chain, ActiveService $active) + public function run($order, $buyerId, $chain, ActiveService $active, $rules = null) { - $venueSplit = (float) PartnerConfig::get('partner_venue_split', 50); - $countyFull = (float) PartnerConfig::get('partner_county_full', 100); + $venueSplit = (isset($rules['venue_split']) && is_numeric($rules['venue_split'])) + ? (float) $rules['venue_split'] + : (float) PartnerConfig::get('partner_venue_split', 50); + $countyFull = (isset($rules['county_full']) && is_numeric($rules['county_full'])) + ? (float) $rules['county_full'] + : (float) PartnerConfig::get('partner_county_full', 100); $result = ['venue' => null, 'county' => null]; $venue = $this->nearest($chain, 'is_venue', $active); diff --git a/addons/shopro/service/partner/VoucherService.php b/addons/shopro/service/partner/VoucherService.php index 254295c..104ff7b 100644 --- a/addons/shopro/service/partner/VoucherService.php +++ b/addons/shopro/service/partner/VoucherService.php @@ -123,15 +123,19 @@ class VoucherService } } - public function grant($order, $buyerId) + public function grant($order, $buyerId, $rules = null) { $exists = VoucherPlan::where('order_id', $order['id'])->find(); if ($exists) { return $exists; } - $times = (float) PartnerConfig::get('partner_voucher_times', 1.5); - $days = intval(PartnerConfig::get('partner_voucher_days', 365)); + $times = (isset($rules['voucher_times']) && is_numeric($rules['voucher_times'])) + ? (float) $rules['voucher_times'] + : (float) PartnerConfig::get('partner_voucher_times', 1.5); + $days = (isset($rules['voucher_days']) && is_numeric($rules['voucher_days'])) + ? intval($rules['voucher_days']) + : intval(PartnerConfig::get('partner_voucher_days', 365)); $days = $days > 0 ? $days : 365; $rewardService = new PartnerRewardService(); $qualifiedPayFee = $rewardService->getQualifiedPayFee($order); diff --git a/addons/shopro/service/pay/PayOper.php b/addons/shopro/service/pay/PayOper.php index 3d8e03d..076c882 100755 --- a/addons/shopro/service/pay/PayOper.php +++ b/addons/shopro/service/pay/PayOper.php @@ -249,6 +249,19 @@ class PayOper // 订单支付完成 $user = User::where('id', $order->user_id)->find(); if ($order_type == 'order') { + // 兑换券同步核销(利用行锁即时扣减并标记,彻底消除异步队列延迟导致的并发双花漏洞) + $ext = $order->ext; + if (is_string($ext)) { + $ext = json_decode($ext, true) ?: []; + } + if (!empty($ext['voucher_fee']) && empty($ext['voucher_consumed'])) { + try { + (new \addons\shopro\service\partner\VoucherService())->consumeByOrder($order); + } catch (\Exception $e) { + \think\Log::record('[PayOper.consumeByOrder] 兑换券同步核销异常: ' . $e->getMessage(), 'error'); + } + } + if ($pay_mode == 'offline') { Action::add($order, null, auth_admin(), 'admin', '管理员操作自动货到付款支付成功'); // 在控制器执行后续内容,这里不再处理 diff --git a/addons/shopro/service/third/wechat/MiniProgram.php b/addons/shopro/service/third/wechat/MiniProgram.php index e6ab1e7..d75364e 100755 --- a/addons/shopro/service/third/wechat/MiniProgram.php +++ b/addons/shopro/service/third/wechat/MiniProgram.php @@ -34,10 +34,16 @@ class MiniProgram error_stop('登陆态已过期, 请重试', -1); } + // 如果前端传了手机号凭证(新版 code 或传统 iv+encryptedData),解析手机号 + $mobile = ''; + if (!empty($this->payload['code']) || (!empty($this->payload['iv']) && !empty($this->payload['encryptedData']))) { + $mobile = $this->getUserPhoneNumber(); + } + $wechatUser = [ 'openid' => $sessionData['openid'], 'unionid' => $sessionData['unionid'] ?? '', - 'mobile' => '', + 'mobile' => $mobile, 'avatar' => '', 'nickname' => '', ]; @@ -65,9 +71,32 @@ class MiniProgram return $wechatUser; } - // 解密微信小程序手机号 + // 解密微信小程序手机号(支持新版 code 与传统 iv + encryptedData) public function getUserPhoneNumber() { + // 1. 优先使用新版微信小程序通过 code 获取手机号 API + if (!empty($this->payload['code'])) { + try { + $response = $this->wechat->auth->httpPostJson('wxa/business/getuserphonenumber', [ + 'code' => $this->payload['code'] + ]); + if (isset($response['errcode']) && (int)$response['errcode'] === 0 && !empty($response['phone_info']['purePhoneNumber'])) { + if (!empty($response['phone_info']['countryCode']) && $response['phone_info']['countryCode'] !== '86') { + error_stop('仅支持大陆地区手机号'); + } + return $response['phone_info']['purePhoneNumber']; + } + if (!empty($response['errmsg']) && empty($this->payload['encryptedData'])) { + error_stop('获取手机号失败: ' . $response['errmsg']); + } + } catch (\Exception $e) { + if (empty($this->payload['encryptedData'])) { + error_stop('获取手机号异常: ' . $e->getMessage()); + } + } + } + + // 2. 兼容传统 iv + encryptedData 模式 if (empty($this->payload['sessionId'])) { error_stop('未获取到登陆态, 请重试', -1); } @@ -78,6 +107,10 @@ class MiniProgram error_stop('登陆态已过期, 请重试', -1); } + if (empty($this->payload['iv']) || empty($this->payload['encryptedData'])) { + error_stop('缺少手机号授权凭证'); + } + $phoneInfo = $this->wechat->encryptor->decryptData($sessionData['session_key'], $this->payload['iv'], $this->payload['encryptedData']); if (empty($phoneInfo['purePhoneNumber'])) { diff --git a/addons/shopro/service/third/wechat/Wechat.php b/addons/shopro/service/third/wechat/Wechat.php index adb6a79..fdbff11 100755 --- a/addons/shopro/service/third/wechat/Wechat.php +++ b/addons/shopro/service/third/wechat/Wechat.php @@ -146,6 +146,23 @@ class Wechat // 用户被删除,则重置第三方信息所绑定的用户id if (!$user) { $oauthInfo->user_id = 0; + } else { + // 如果本次快捷登录提供了手机号,且用户原本没有手机号,则为用户补充手机号 + $isChanged = false; + if (!empty($extend['mobile']) && empty($user->mobile)) { + $user->mobile = $extend['mobile']; + $isChanged = true; + } + $verification = $user->verification; + $verificationObj = is_object($verification) ? $verification : (object)$verification; + if ((!empty($user->mobile) || !empty($extend['mobile'])) && empty($verificationObj->mobile)) { + $verificationObj->mobile = 1; + $user->verification = $verificationObj; + $isChanged = true; + } + if ($isChanged) { + $user->save(); + } } } @@ -161,8 +178,18 @@ class Wechat return false; } - // 注册新用户 + // 注册新用户或匹配已有手机号用户 if ($oauthInfo->user_id === 0 && $user_id === 0) { + // 如果获取到了手机号,检查系统是否已有该手机号用户 + if (!empty($extend['mobile'])) { + $existingUser = UserModel::where('mobile', $extend['mobile'])->find(); + if ($existingUser) { + // 直接将此第三方账号关联到该已有手机号用户 + $oauthInfo->user_id = $existingUser->id; + return $oauthInfo->save(); + } + } + $auth = (new UserAuth())->register([ 'nickname' => $oauthInfo->nickname ?? '', 'avatar' => $oauthInfo->avatar ?? '', diff --git a/application/admin/controller/shopro/app/ScoreShop.php b/application/admin/controller/shopro/app/ScoreShop.php index fe56a7e..38a93eb 100644 --- a/application/admin/controller/shopro/app/ScoreShop.php +++ b/application/admin/controller/shopro/app/ScoreShop.php @@ -317,6 +317,10 @@ class ScoreShop extends Common unset($skuPrice['createtime'], $skuPrice['updatetime'], $skuPrice['deletetime']); // 不手动更新时间 $skuPrice['goods_id'] = $goods_id; + $skuPrice['score'] = isset($skuPrice['score']) && $skuPrice['score'] !== '' ? intval($skuPrice['score']) : 0; + $skuPrice['price'] = isset($skuPrice['price']) && $skuPrice['price'] !== '' ? round(floatval($skuPrice['price']), 2) : 0.00; + $skuPrice['stock'] = isset($skuPrice['stock']) && $skuPrice['stock'] !== '' ? intval($skuPrice['stock']) : 0; + $model = new ScoreSkuPrice; if (isset($skuPrice['id'])) { $model = $this->model->find($skuPrice['id']); diff --git a/application/admin/controller/shopro/commission/Goods.php b/application/admin/controller/shopro/commission/Goods.php index 0e9e3e0..7d12375 100644 --- a/application/admin/controller/shopro/commission/Goods.php +++ b/application/admin/controller/shopro/commission/Goods.php @@ -42,14 +42,21 @@ class Goods extends Common }); $config = sheep_config('shop.commission'); + $defaultPartnerRules = \addons\shopro\service\partner\PartnerConfig::defaultRules(); + $roleNames = \addons\shopro\service\partner\PartnerConfig::roleNames(); + $levels = \app\admin\model\shopro\commission\Level::order('level asc')->select(); + $this->success('分销商品详情', null, [ 'goods' => $goodsList, - 'config' => $config + 'config' => $config, + 'default_partner_rules' => $defaultPartnerRules, + 'role_names' => $roleNames, + 'levels' => $levels, ]); } /** - * 设置佣金(支持批量) + * 设置计奖规则/佣金(支持批量) * * @param $id */ @@ -61,18 +68,50 @@ class Goods extends Common // 接受全部参数 $params = $this->request->only(['status', 'self_rules', 'commission_order_status', 'commission_config', 'commission_rules']); + if (empty($id)) { + $id = $this->request->param('id', ''); + } + if (empty($id)) { + $this->error('缺少必要参数: id'); + } + + // 状态规范化 + if (isset($params['status'])) { + $params['status'] = intval($params['status']) === 1 ? 1 : 0; + } + if (isset($params['commission_order_status'])) { + $params['commission_order_status'] = intval($params['commission_order_status']) === 0 ? 0 : 1; + } + if (isset($params['self_rules'])) { + $params['self_rules'] = intval($params['self_rules']); + } + + // 针对 commission_rules 防御性处理 + if (isset($params['commission_rules'])) { + if (is_string($params['commission_rules'])) { + $decoded = json_decode($params['commission_rules'], true); + if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { + $params['commission_rules'] = $decoded; + } + } + } $result = Db::transaction(function () use ($id, $params) { $count = 0; - $ids = explode(',', $id); + $ids = explode(',', (string)$id); foreach ($ids as $goods_id) { + $goods_id = intval($goods_id); + if ($goods_id <= 0) { + continue; + } if ($row = $this->model->get($goods_id)) { $row->save($params); } else { $model = new CommissionGoodsModel(); - $params['goods_id'] = $goods_id; - $model->save($params); + $saveData = $params; + $saveData['goods_id'] = $goods_id; + $model->save($saveData); } $count++; } diff --git a/application/admin/model/shopro/app/ScoreSkuPrice.php b/application/admin/model/shopro/app/ScoreSkuPrice.php index e6a8628..5ab1697 100644 --- a/application/admin/model/shopro/app/ScoreSkuPrice.php +++ b/application/admin/model/shopro/app/ScoreSkuPrice.php @@ -42,7 +42,7 @@ class ScoreSkuPrice extends Common /** - * 积分加现金 + * 兑换门槛(兑换券 + 积分) * * @param string $value * @param array $data @@ -50,10 +50,19 @@ class ScoreSkuPrice extends Common */ public function getScorePriceAttr($value, $data) { - $score = $data['score'] ?? 0; + $score = intval($data['score'] ?? 0); $price = $data['price'] ?? 0; + $priceNum = floatval($price); - return $score . '积分' . ($price ? '+¥' . $price : ''); + if ($priceNum > 0 && $score > 0) { + return '¥' . $price . '兑换券 + ' . $score . '积分'; + } elseif ($priceNum > 0) { + return '¥' . $price . ' 兑换券'; + } elseif ($score > 0) { + return $score . '积分'; + } + + return '免费兑换'; } diff --git a/application/admin/model/shopro/user/User.php b/application/admin/model/shopro/user/User.php index 9f703ec..6164021 100644 --- a/application/admin/model/shopro/user/User.php +++ b/application/admin/model/shopro/user/User.php @@ -113,9 +113,24 @@ class User extends Common { $value = array_filter((array)json_decode($value, true)); $value = array_merge(['username' => 0, 'password' => 0, 'mobile' => 0], $value); + // 若数据库中已有手机号,则自动判定为已绑定/已验证手机号 + if (!empty($data['mobile'])) { + $value['mobile'] = 1; + } return (object)$value; } + /** + * 设置验证字段 + * @param mixed $value + * @return string + */ + public function setVerificationAttr($value) + { + $value = is_object($value) || is_array($value) ? json_encode($value) : $value; + return $value; + } + public function parentUser() { return $this->hasOne(User::class, 'id', 'parent_user_id')->field(['id', 'avatar', 'nickname', 'gender']); diff --git a/application/admin/view/shopro/app/score_shop/add.html b/application/admin/view/shopro/app/score_shop/add.html index 3680622..96457c8 100644 --- a/application/admin/view/shopro/app/score_shop/add.html +++ b/application/admin/view/shopro/app/score_shop/add.html @@ -56,17 +56,25 @@ -
商品名称:{{ state.title }}
+
商品名称:{{ state.title }}
+ +
- + - - + + @@ -79,15 +87,18 @@ diff --git a/application/admin/view/shopro/app/score_shop/index.html b/application/admin/view/shopro/app/score_shop/index.html index 87aca4f..72d1e7b 100644 --- a/application/admin/view/shopro/app/score_shop/index.html +++ b/application/admin/view/shopro/app/score_shop/index.html @@ -56,7 +56,7 @@
-
积分商城
+
兑换商城 (兑换券/积分)
@@ -121,12 +121,19 @@
- + diff --git a/application/admin/view/shopro/commission/goods/edit.html b/application/admin/view/shopro/commission/goods/edit.html index 14a3fc1..4f7729e 100644 --- a/application/admin/view/shopro/commission/goods/edit.html +++ b/application/admin/view/shopro/commission/goods/edit.html @@ -3,94 +3,165 @@ @@ -98,256 +169,311 @@ -
商品信息
-
-
- + +
+
+
-
- {{ goods.title }} -
-
- {{ goods.is_sku ? '多规格' : '' }} +
{{ goods.title }}
+
+ 指导价:¥{{ Array.isArray(goods.price) ? goods.price.join(' ~ ') : goods.price }}
+
多规格商品(共 {{ goods.sku_prices?.length || 0 }} 种规格)
+
单规格基础商品
-
佣金设置
- - - - 不参与 - 参与 - - - - - 不计入 - - - 计入 - - - - - - - - - - 默认规则 - 独立规则 - 批量规则 - - - - - - {{ state.commission_config_temp.status ? '自定义' : '默认' }} - - - - - 一级 - 二级 - 三级 - - - -
- - 关闭 - 开启 - -
分销自购开启后,分销商自己购买时,下单可以给自己返佣
+ + + +
+
+
计奖资格与业绩设置
- - -
- - 商品价 - 实际支付价 + + + 参与计奖 + 不参与计奖 -
- 商品价: 商品实际售价/规格价,实际支付价:实际支付的费用(不含运费) +
+ 开启后,购买该商品的新订单将参与收益中心推三返一、创客极差、团队拆分、兑换券及全网日结奖池;关闭后整单或单品排除计奖。
-
-
- - - 支付后结算 - 确认收货结算 - 订单完成结算 - 手动打款 - - -
- - - + @@ -83,15 +84,15 @@ - + @@ -104,7 +105,7 @@ 已选择 {{batchHandle.data.length}} 项
{if $auth->check('shopro/commission/goods/edit')} - 设置佣金 + 批量设置分成 {/if}
diff --git a/public/assets/js/backend/shopro/app/score_shop.js b/public/assets/js/backend/shopro/app/score_shop.js index 92bb0d6..1fc7011 100644 --- a/public/assets/js/backend/shopro/app/score_shop.js +++ b/public/assets/js/backend/shopro/app/score_shop.js @@ -189,8 +189,17 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin } function onConfirm() { + let cleanedPrices = (goods.score_sku_prices || []).map(item => { + let clone = { ...item }; + if (clone.status === 'up') { + clone.score = (clone.score === '' || clone.score === undefined || clone.score === null) ? 0 : Number(clone.score); + clone.price = (clone.price === '' || clone.price === undefined || clone.price === null) ? 0 : Number(clone.price); + clone.stock = (clone.stock === '' || clone.stock === undefined || clone.stock === null) ? 0 : Number(clone.stock); + } + return clone; + }); let submitForm = { - sku_prices: goods.score_sku_prices, + sku_prices: cleanedPrices, } if (state.type == 'add') { submitForm = { diff --git a/public/assets/js/backend/shopro/commission/agent.js b/public/assets/js/backend/shopro/commission/agent.js index ead9c0a..368ece4 100644 --- a/public/assets/js/backend/shopro/commission/agent.js +++ b/public/assets/js/backend/shopro/commission/agent.js @@ -29,6 +29,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin identity: '', is_venue: '', is_county: '', + is_partner: '', active_30d: '', county_code: '', }, @@ -110,6 +111,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin { value: 'maker', label: '创客' }, { value: 'venue', label: '酒道馆' }, { value: 'county', label: '区县' }, + { value: 'partner', label: '合伙股东' }, ] }, }, @@ -125,6 +127,12 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin value: '', options: { data: [{ value: '1', label: '是' }, { value: '0', label: '否' }] }, }, + is_partner: { + type: 'tselect', + label: '合伙股东', + value: '', + options: { data: [{ value: '1', label: '是' }, { value: '0', label: '否' }] }, + }, active_30d: { type: 'tselect', label: '30日活跃', @@ -207,6 +215,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin { value: 'maker', label: roleNames.maker || '创客' }, { value: 'venue', label: roleNames.venue || '酒道馆' }, { value: 'county', label: roleNames.county || '区县' }, + { value: 'partner', label: roleNames.partner || '合伙股东' }, ]; } if (state.filter.tools.is_venue) { @@ -215,6 +224,9 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin if (state.filter.tools.is_county) { state.filter.tools.is_county.label = roleNames.county || '区县代理'; } + if (state.filter.tools.is_partner) { + state.filter.tools.is_partner.label = roleNames.partner || '合伙股东'; + } } } return false @@ -405,8 +417,53 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin } onMounted(() => { - getLevelSelect() - getData() + // 解析 URL 查询参数并自动回显至筛选器(支持从仪表盘、其他模块跳转带参) + const params = new URLSearchParams(location.search); + const levelParam = params.get('level'); + if (levelParam !== null && levelParam !== '') { + const parsedLevel = !isNaN(Number(levelParam)) ? Number(levelParam) : levelParam; + state.filter.data.level = parsedLevel; + if (state.filter.tools && state.filter.tools.level) { + state.filter.tools.level.value = parsedLevel; + } + } + const isVenueParam = params.get('is_venue'); + if (isVenueParam !== null && isVenueParam !== '') { + state.filter.data.is_venue = isVenueParam; + if (state.filter.tools && state.filter.tools.is_venue) { + state.filter.tools.is_venue.value = isVenueParam; + } + } + const isCountyParam = params.get('is_county'); + if (isCountyParam !== null && isCountyParam !== '') { + state.filter.data.is_county = isCountyParam; + if (state.filter.tools && state.filter.tools.is_county) { + state.filter.tools.is_county.value = isCountyParam; + } + } + const isPartnerParam = params.get('is_partner'); + if (isPartnerParam !== null && isPartnerParam !== '') { + state.filter.data.is_partner = isPartnerParam; + if (state.filter.tools && state.filter.tools.is_partner) { + state.filter.tools.is_partner.value = isPartnerParam; + } + } + const identityParam = params.get('identity'); + if (identityParam !== null && identityParam !== '') { + state.filter.data.identity = identityParam; + if (state.filter.tools && state.filter.tools.identity) { + state.filter.tools.identity.value = identityParam; + } + } + const userIdParam = params.get('user_id'); + if (userIdParam !== null && userIdParam !== '') { + state.filter.data.user = { field: 'user_id', value: userIdParam }; + if (state.filter.tools && state.filter.tools.user) { + state.filter.tools.user.value = { field: 'user_id', value: userIdParam }; + } + } + getLevelSelect(); + getData(); }) return { diff --git a/public/assets/js/backend/shopro/commission/goods.js b/public/assets/js/backend/shopro/commission/goods.js index b892b09..b2b7c56 100644 --- a/public/assets/js/backend/shopro/commission/goods.js +++ b/public/assets/js/backend/shopro/commission/goods.js @@ -2,7 +2,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin var Controller = { index: () => { - const { reactive, onMounted } = Vue + const { reactive, onMounted } = Vue; const index = { setup() { const state = reactive({ @@ -12,6 +12,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin data: { keyword: { field: 'id', value: '' }, 'commission_goods.status': '', + 'commission_goods.self_rules': '', }, tools: { keyword: { @@ -39,11 +40,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin }, 'commission_goods.status': { type: 'tselect', - label: '分销状态', + label: '计奖状态', value: '', options: { data: [{ - label: '不参与', + label: '未参与', value: '0', }, { @@ -52,6 +53,25 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin }], }, }, + 'commission_goods.self_rules': { + type: 'tselect', + label: '分成规则', + value: '', + options: { + data: [{ + label: '默认规则', + value: '0', + }, + { + label: '独立规则', + value: '1', + }, + { + label: '批量规则', + value: '2', + }], + }, + }, }, condition: {}, }, @@ -64,7 +84,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin down: 'danger', hidden: 'info', }, - }) + }); function getData() { let tempSearch = JSON.parse(JSON.stringify(state.filter.data)); @@ -81,56 +101,56 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin ...search, }, }, function (ret, res) { - state.data = res.data.data - pagination.total = res.data.total - return false - }, function (ret, res) { }) + state.data = res.data.data; + pagination.total = res.data.total; + return false; + }, function (ret, res) { }); } function onOpenFilter() { - state.filter.drawer = true + state.filter.drawer = true; } function onChangeFilter() { - pagination.page = 1 - getData() - state.filter.drawer && (state.filter.drawer = false) + pagination.page = 1; + getData(); + state.filter.drawer && (state.filter.drawer = false); } const pagination = reactive({ page: 1, list_rows: 10, total: 0, - }) + }); const batchHandle = reactive({ data: [], - }) + }); function onChangeSelection(val) { - batchHandle.data = val + batchHandle.data = val; } function onBatchHandle(type) { - let ids = [] + let ids = []; batchHandle.data.forEach((item) => { - ids.push(item.id) - }) + ids.push(item.id); + }); switch (type) { case 'edit': - onEdit(ids.join(','), 'batch') + onEdit(ids.join(','), 'batch'); break; } } function onEdit(ids, rulesType = '') { - Fast.api.open(`shopro/commission/goods/edit?type=edit&id=${ids}&rulesType=${rulesType}`, "设置佣金", { + Fast.api.open(`shopro/commission/goods/edit?type=edit&id=${ids}&rulesType=${rulesType}`, "设置分成规则", { callback() { - getData() + getData(); } - }) + }); } onMounted(() => { - getData() - }) + getData(); + }); return { state, @@ -142,42 +162,158 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin onChangeSelection, onBatchHandle, onEdit, - } + }; } - } + }; createApp('index', index); }, edit: () => { Controller.form(); }, form: () => { - const { reactive, onMounted } = Vue - const { ElMessage } = ElementPlus + const { reactive, onMounted } = Vue; + const { ElMessage } = ElementPlus; const addEdit = { setup() { const state = reactive({ type: new URLSearchParams(location.search).get('type'), id: new URLSearchParams(location.search).get('id'), rulesType: new URLSearchParams(location.search).get('rulesType'), + data: {}, detailData: {}, commission_goods: { status: 1, - self_rules: new URLSearchParams(location.search).get('rulesType') == 'batch' ? 2 : 0, commission_order_status: 1, - commission_config: {}, + self_rules: new URLSearchParams(location.search).get('rulesType') == 'batch' ? 2 : 0, commission_rules: {}, }, - defaultCommissionConfig: { - status: 0, - level: 2, - self_buy: 0, - reward_type: 'goods_price', - reward_event: 'paid', + defaultPartnerRules: { + push3_amounts: [398, 400, 600], + maker_bonus: { "2": 20, "3": 40, "4": 60, "5": 80, "6": 100, "7": 120 }, + maker_peer_rate: 0.10, + maker_overtaken_rate: 0.05, + venue_split: 50, + county_full: 100, + venue_peer_rate: 0.10, + county_peer_rate: 0.10, + voucher_times: 1.5, + voucher_days: 365, + venue_share: 100, + county_share: 50, }, - commission_config_temp: {}, - commission_rules: {}, - levelData: [], - }) + roleNames: { + taster: '品酒师', + maker: '创客', + venue: '酒道馆', + county: '区县代理', + partner: '合伙股东' + }, + levels: [], + // 统一设置模式下的表单数据 + ruleForm: { + push3_1: 398, + push3_2: 400, + push3_3: 600, + maker_v1: 20, + maker_v2: 40, + maker_v3: 60, + maker_v4: 80, + maker_v5: 100, + maker_v6: 120, + maker_peer_rate: 10, + maker_overtaken_rate: 5, + venue_split: 50, + county_full: 100, + venue_peer_rate: 10, + county_peer_rate: 10, + voucher_times: 1.5, + voucher_days: 365, + venue_share: 100, + county_share: 50, + }, + skuMode: 'unified', // 'unified': 统一设置, 'sku': 按规格分别设置 + activeSkuId: '', + skuRules: {}, + }); + + function makeFormFromDefaults(defaults) { + const d = defaults || state.defaultPartnerRules; + return { + push3_1: d.push3_amounts?.[0] !== undefined ? d.push3_amounts[0] : 398, + push3_2: d.push3_amounts?.[1] !== undefined ? d.push3_amounts[1] : 400, + push3_3: d.push3_amounts?.[2] !== undefined ? d.push3_amounts[2] : 600, + maker_v1: d.maker_bonus?.['2'] !== undefined ? d.maker_bonus['2'] : 20, + maker_v2: d.maker_bonus?.['3'] !== undefined ? d.maker_bonus['3'] : 40, + maker_v3: d.maker_bonus?.['4'] !== undefined ? d.maker_bonus['4'] : 60, + maker_v4: d.maker_bonus?.['5'] !== undefined ? d.maker_bonus['5'] : 80, + maker_v5: d.maker_bonus?.['6'] !== undefined ? d.maker_bonus['6'] : 100, + maker_v6: d.maker_bonus?.['7'] !== undefined ? d.maker_bonus['7'] : 120, + maker_peer_rate: Number(((d.maker_peer_rate !== undefined ? d.maker_peer_rate : 0.10) * 100).toFixed(2)), + maker_overtaken_rate: Number(((d.maker_overtaken_rate !== undefined ? d.maker_overtaken_rate : 0.05) * 100).toFixed(2)), + venue_split: d.venue_split !== undefined ? d.venue_split : 50, + county_full: d.county_full !== undefined ? d.county_full : 100, + venue_peer_rate: Number(((d.venue_peer_rate !== undefined ? d.venue_peer_rate : 0.10) * 100).toFixed(2)), + county_peer_rate: Number(((d.county_peer_rate !== undefined ? d.county_peer_rate : 0.10) * 100).toFixed(2)), + voucher_times: d.voucher_times !== undefined ? d.voucher_times : 1.5, + voucher_days: d.voucher_days !== undefined ? d.voucher_days : 365, + venue_share: d.venue_share !== undefined ? d.venue_share : 100, + county_share: d.county_share !== undefined ? d.county_share : 50, + }; + } + + function rulesToForm(rules, defaults) { + const d = defaults || state.defaultPartnerRules; + const r = rules || {}; + return { + push3_1: r.push3_amounts?.[0] !== undefined ? r.push3_amounts[0] : (d.push3_amounts?.[0] ?? 398), + push3_2: r.push3_amounts?.[1] !== undefined ? r.push3_amounts[1] : (d.push3_amounts?.[1] ?? 400), + push3_3: r.push3_amounts?.[2] !== undefined ? r.push3_amounts[2] : (d.push3_amounts?.[2] ?? 600), + maker_v1: r.maker_bonus?.['2'] !== undefined ? r.maker_bonus['2'] : (d.maker_bonus?.['2'] ?? 20), + maker_v2: r.maker_bonus?.['3'] !== undefined ? r.maker_bonus['3'] : (d.maker_bonus?.['3'] ?? 40), + maker_v3: r.maker_bonus?.['4'] !== undefined ? r.maker_bonus['4'] : (d.maker_bonus?.['4'] ?? 60), + maker_v4: r.maker_bonus?.['5'] !== undefined ? r.maker_bonus['5'] : (d.maker_bonus?.['5'] ?? 80), + maker_v5: r.maker_bonus?.['6'] !== undefined ? r.maker_bonus['6'] : (d.maker_bonus?.['6'] ?? 100), + maker_v6: r.maker_bonus?.['7'] !== undefined ? r.maker_bonus['7'] : (d.maker_bonus?.['7'] ?? 120), + maker_peer_rate: r.maker_peer_rate !== undefined ? Number((r.maker_peer_rate * 100).toFixed(2)) : Number(((d.maker_peer_rate ?? 0.10) * 100).toFixed(2)), + maker_overtaken_rate: r.maker_overtaken_rate !== undefined ? Number((r.maker_overtaken_rate * 100).toFixed(2)) : Number(((d.maker_overtaken_rate ?? 0.05) * 100).toFixed(2)), + venue_split: r.venue_split !== undefined ? r.venue_split : (d.venue_split ?? 50), + county_full: r.county_full !== undefined ? r.county_full : (d.county_full ?? 100), + venue_peer_rate: r.venue_peer_rate !== undefined ? Number((r.venue_peer_rate * 100).toFixed(2)) : Number(((d.venue_peer_rate ?? 0.10) * 100).toFixed(2)), + county_peer_rate: r.county_peer_rate !== undefined ? Number((r.county_peer_rate * 100).toFixed(2)) : Number(((d.county_peer_rate ?? 0.10) * 100).toFixed(2)), + voucher_times: r.voucher_times !== undefined ? r.voucher_times : (d.voucher_times ?? 1.5), + voucher_days: r.voucher_days !== undefined ? r.voucher_days : (d.voucher_days ?? 365), + venue_share: r.venue_share !== undefined ? r.venue_share : (d.venue_share ?? 100), + county_share: r.county_share !== undefined ? r.county_share : (d.county_share ?? 50), + }; + } + + function formToRules(form) { + return { + push3_amounts: [ + Number(form.push3_1 || 0), + Number(form.push3_2 || 0), + Number(form.push3_3 || 0) + ], + maker_bonus: { + "2": Number(form.maker_v1 || 0), + "3": Number(form.maker_v2 || 0), + "4": Number(form.maker_v3 || 0), + "5": Number(form.maker_v4 || 0), + "6": Number(form.maker_v5 || 0), + "7": Number(form.maker_v6 || 0) + }, + maker_peer_rate: Number((Number(form.maker_peer_rate || 0) / 100).toFixed(4)), + maker_overtaken_rate: Number((Number(form.maker_overtaken_rate || 0) / 100).toFixed(4)), + venue_split: Number(form.venue_split || 0), + county_full: Number(form.county_full || 0), + venue_peer_rate: Number((Number(form.venue_peer_rate || 0) / 100).toFixed(4)), + county_peer_rate: Number((Number(form.county_peer_rate || 0) / 100).toFixed(4)), + voucher_times: Number(form.voucher_times || 0), + voucher_days: parseInt(form.voucher_days || 365), + venue_share: Number(form.venue_share || 0), + county_share: Number(form.county_share || 0) + }; + } function getDetail() { Fast.api.ajax({ @@ -185,249 +321,170 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin type: 'GET', }, function (ret, res) { state.data = res.data; + if (res.data.default_partner_rules) { + state.defaultPartnerRules = res.data.default_partner_rules; + } + if (res.data.role_names) { + state.roleNames = res.data.role_names; + } + if (res.data.levels) { + state.levels = res.data.levels; + } if (state.rulesType != 'batch') { - state.detailData = state.data.goods[0]; + state.detailData = (state.data.goods && state.data.goods[0]) ? state.data.goods[0] : {}; } - Object.keys(state.defaultCommissionConfig).forEach((key) => { - if (key != 'status') state.defaultCommissionConfig[key] = state.data.config[key]; - }); + const cg = state.detailData.commission_goods; + if (cg) { + state.commission_goods.status = cg.status !== undefined ? Number(cg.status) : 1; + state.commission_goods.commission_order_status = cg.commission_order_status !== undefined ? Number(cg.commission_order_status) : 1; + state.commission_goods.self_rules = cg.self_rules !== undefined ? Number(cg.self_rules) : 0; - state.commission_config_temp = JSON.parse(JSON.stringify(state.defaultCommissionConfig)); - - if (isEmpty(state.detailData.commission_goods)) { - initCommission(state.commission_goods.self_rules); - } else { - state.commission_goods = state.detailData.commission_goods; - - if ( - state.detailData.commission_goods.commission_config && - state.detailData.commission_goods.commission_config.status - ) { - state.commission_config_temp = state.detailData.commission_goods.commission_config; + let rawRules = cg.commission_rules; + if (typeof rawRules === 'string') { + try { + rawRules = JSON.parse(rawRules); + } catch (e) { + rawRules = {}; + } } - initCommission(state.detailData.commission_goods.self_rules); - } - return false - }, function (ret, res) { }) - } - - function getLevelSelect() { - Fast.api.ajax({ - url: `shopro/commission/level/select`, - type: 'GET', - data: { - sort: 'level', - order: 'asc', - } - }, function (ret, res) { - state.levelData = res.data; - state.type == 'edit' && getDetail() - return false - }, function (ret, res) { }) - } - - function initCommission(type) { - let rate_money = { - 1: { - rate: '', - money: '', - }, - 2: { - rate: '', - money: '', - }, - 3: { - rate: '', - money: '', - }, - }; - if (!isObject(state.commission_goods.commission_rules)) { - state.commission_goods.commission_rules = {}; - } - if (type == 0) { - state.commission_goods.commission_rules = null; - } else if (type == 1) { - state.detailData.sku_prices.forEach((i) => { - if (state.commission_goods.commission_rules[i.id]) { - state.levelData.forEach((j) => { - if (state.commission_goods.commission_rules[i.id][j.level]) { - for (let key in state.commission_goods.commission_rules[i.id][j.level]) { - if (!state.commission_goods.commission_rules[i.id][j.level][key].rate) { - state.commission_goods.commission_rules[i.id][j.level][key].rate = ''; - } else if (!state.commission_goods.commission_rules[i.id][j.level][key].money) { - state.commission_goods.commission_rules[i.id][j.level][key].money = ''; - } - } - } else { - state.commission_goods.commission_rules[i.id][j.level] = JSON.parse(JSON.stringify(rate_money)); - } - }); - - for (var level in state.commission_goods.commission_rules[i.id]) { - let index = state.levelData.findIndex((l) => Number(l.level) == Number(level)); - if (index == -1) { - delete state.commission_goods.commission_rules[i.id][level]; + // 检查是否包含规格级规则 + const skuPrices = state.detailData.sku_prices || []; + let hasSkuSpecific = false; + if (skuPrices.length > 0 && rawRules) { + for (let sku of skuPrices) { + if (rawRules[sku.id] || rawRules['sku_' + sku.id]) { + hasSkuSpecific = true; + break; } } - } else { - state.commission_goods.commission_rules[i.id] = {}; - state.levelData.forEach((l) => { - state.commission_goods.commission_rules[i.id][l.level] = JSON.parse(JSON.stringify(rate_money)); + } + + if (hasSkuSpecific) { + state.skuMode = 'sku'; + skuPrices.forEach((sku) => { + const skuConfig = rawRules[sku.id] || rawRules['sku_' + sku.id] || rawRules; + state.skuRules[sku.id] = rulesToForm(skuConfig, state.defaultPartnerRules); }); - } - }); - - for (var sku in state.commission_goods.commission_rules) { - let index = state.detailData.sku_prices.findIndex((s) => Number(s.id) == Number(sku)); - if (index == -1) { - delete state.commission_goods.commission_rules[sku]; - } - } - } else if (type == 2) { - state.levelData.forEach((l) => { - if (state.commission_goods.commission_rules[l.level]) { + if (skuPrices.length > 0) { + state.activeSkuId = String(skuPrices[0].id); + } } else { - state.commission_goods.commission_rules[l.level] = JSON.parse(JSON.stringify(rate_money)); + state.skuMode = 'unified'; + state.ruleForm = rulesToForm(rawRules, state.defaultPartnerRules); + // 预填充各 skuRules 备用 + skuPrices.forEach((sku) => { + state.skuRules[sku.id] = JSON.parse(JSON.stringify(state.ruleForm)); + }); + if (skuPrices.length > 0) { + state.activeSkuId = String(skuPrices[0].id); + } } + } else { + state.ruleForm = makeFormFromDefaults(state.defaultPartnerRules); + const skuPrices = state.detailData.sku_prices || []; + skuPrices.forEach((sku) => { + state.skuRules[sku.id] = makeFormFromDefaults(state.defaultPartnerRules); + }); + if (skuPrices.length > 0) { + state.activeSkuId = String(skuPrices[0].id); + } + } + return false; + }, function (ret, res) { }); + } + + function onFillDefaults(target) { + const defaults = makeFormFromDefaults(state.defaultPartnerRules); + if (target === 'current_sku' && state.activeSkuId) { + state.skuRules[state.activeSkuId] = defaults; + ElMessage({ message: '当前规格已填入系统默认规则', type: 'success' }); + } else if (target === 'all_skus') { + const skuPrices = state.detailData.sku_prices || []; + skuPrices.forEach((sku) => { + state.skuRules[sku.id] = JSON.parse(JSON.stringify(defaults)); }); - - for (var level in state.commission_goods.commission_rules) { - let index = state.levelData.findIndex((l) => Number(l.level) == Number(level)); - if (index == -1) { - delete state.commission_goods.commission_rules[level]; - } - } - } - } - - function onChangeSelfRules(self_rules) { - if (self_rules == 1 || self_rules == 2) { - state.commission_goods.commission_rules = {}; - if (state.commission_config_temp.status == 0) { - state.commission_config_temp = JSON.parse(JSON.stringify(state.defaultCommissionConfig)); - } - } else if (self_rules == 0) { - state.commission_goods.commission_rules = null; - state.commission_config_temp = JSON.parse(JSON.stringify(state.defaultCommissionConfig)); - } - initCommission(self_rules); - } - - function onChangeCommissionConfigStatus(val) { - if (val == 0) { - // 默认 - state.commission_config_temp = JSON.parse(JSON.stringify(state.defaultCommissionConfig)); + state.ruleForm = defaults; + ElMessage({ message: '所有规格已同步系统默认规则', type: 'success' }); } else { - // 自定义 - if ( - state.detailData.commission_goods && - state.detailData.commission_goods.commission_config - ) { - state.commission_config_temp = state.detailData.commission_goods.commission_config; - } - - state.commission_config_temp.status = 1; + state.ruleForm = defaults; + ElMessage({ message: '已填入系统默认规则', type: 'success' }); } } - const commissionPopover = reactive({ - flag: {}, - form: { - money: '', - rate: '', - }, - }); - function onConfirmCommissionPopover(cl) { - if (state.commission_goods.self_rules == 1) { - for (let sku in state.commission_goods.commission_rules) { - for (let level in state.commission_goods.commission_rules[sku]) { - state.commission_goods.commission_rules[sku][level][cl] = JSON.parse(JSON.stringify(commissionPopover.form)) - } - } - } else if (state.commission_goods.self_rules == 2) { - for (let level in state.commission_goods.commission_rules) { - state.commission_goods.commission_rules[level][cl] = JSON.parse(JSON.stringify(commissionPopover.form)) + function onSyncToAllSkus() { + const currentSource = (state.skuMode === 'sku' && state.activeSkuId) + ? state.skuRules[state.activeSkuId] + : state.ruleForm; + const skuPrices = state.detailData.sku_prices || []; + skuPrices.forEach((sku) => { + state.skuRules[sku.id] = JSON.parse(JSON.stringify(currentSource)); + }); + state.ruleForm = JSON.parse(JSON.stringify(currentSource)); + ElMessage({ message: '已将当前规则同步至全部规格', type: 'success' }); + } + + function onChangeSelfRules(val) { + if (val == 1 || val == 2) { + if (!state.ruleForm.push3_1) { + state.ruleForm = makeFormFromDefaults(state.defaultPartnerRules); } } - onCancelCommissionPopover(cl); - } - function onCancelCommissionPopover(cl) { - commissionPopover.flag[cl] = false; - commissionPopover.form = { - money: '', - rate: '', - }; } function onConfirm() { - const commission_goods = { - ...state.commission_goods, + const payload = { + status: Number(state.commission_goods.status), + commission_order_status: Number(state.commission_goods.commission_order_status), + self_rules: Number(state.commission_goods.self_rules), + commission_rules: null, }; - commission_goods.commission_config = JSON.stringify({ - ...state.commission_config_temp, - }); - let flag = true - if (commission_goods.self_rules == 1) { - for (var key1 in commission_goods.commission_rules) { - for (var key2 in commission_goods.commission_rules[key1]) { - for (var key3 in commission_goods.commission_rules[key1][key2]) { - if (key3 <= state.commission_config_temp.level) { - if ((commission_goods.commission_rules[key1][key2][key3].rate === '' || commission_goods.commission_rules[key1][key2][key3].rate < 0) - && (commission_goods.commission_rules[key1][key2][key3].money === '' || commission_goods.commission_rules[key1][key2][key3].money < 0)) { - flag = false - } - } - } + if (payload.status === 1) { + if (payload.self_rules === 0) { + payload.commission_rules = null; + } else { + // 独立规则或批量规则 + const skuPrices = state.detailData?.sku_prices || []; + if (state.skuMode === 'sku' && skuPrices.length > 0 && state.rulesType !== 'batch') { + const rulesMap = {}; + skuPrices.forEach((sku) => { + const form = state.skuRules[sku.id] || state.ruleForm; + rulesMap[sku.id] = formToRules(form); + }); + payload.commission_rules = rulesMap; + } else { + payload.commission_rules = formToRules(state.ruleForm); } } } - if (commission_goods.self_rules == 2) { - for (var key1 in commission_goods.commission_rules) { - for (var key2 in commission_goods.commission_rules[key1]) { - if (key2 <= state.commission_config_temp.level) { - if (commission_goods.commission_rules[key1][key2].rate === '' || commission_goods.commission_rules[key1][key2].rate < 0) { - flag = false - } - } - } - } - } - if (!flag) { - ElMessage({ - message: '请将数据填写完整', - type: 'warning', - }) - return false - } Fast.api.ajax({ url: `shopro/commission/goods/edit/id/${state.id}`, type: 'POST', - data: JSON.parse(JSON.stringify(commission_goods)) + data: payload }, function (ret, res) { - Fast.api.close() - }, function (ret, res) { }) + ElMessage({ message: '设置保存成功', type: 'success' }); + Fast.api.close(); + return false; + }, function (ret, res) { }); } onMounted(() => { - getLevelSelect() - }) + getDetail(); + }); return { state, + onFillDefaults, + onSyncToAllSkus, onChangeSelfRules, - onChangeCommissionConfigStatus, - commissionPopover, - onConfirmCommissionPopover, - onCancelCommissionPopover, onConfirm, - } + }; } - } + }; createApp('addEdit', addEdit); } }; diff --git a/public/assets/js/backend/shopro/dashboard.js b/public/assets/js/backend/shopro/dashboard.js index 85df25a..cc8e665 100644 --- a/public/assets/js/backend/shopro/dashboard.js +++ b/public/assets/js/backend/shopro/dashboard.js @@ -790,9 +790,14 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function ( onOpenTab(url, "收益流水"); } + /** + * 打开分销商/成员档案选项卡 + * @param {number|string} level 分销商等级(如 1 代表品酒师会员) + */ function onOpenAgent(level) { - let url = `shopro/commission/agent/index` + (level ? `&level=${level}` : ''); - onOpenTab(url, "成员档案"); + let url = `shopro/commission/agent/index` + (level !== undefined && level !== null && level !== '' ? `?level=${level}` : ''); + let title = (level == 1) ? "品酒师会员" : "成员档案"; + onOpenTab(url, title); } function onOpenDaily() { diff --git a/tests/verify_agent_url_routing_spec.js b/tests/verify_agent_url_routing_spec.js new file mode 100644 index 0000000..bb27e72 --- /dev/null +++ b/tests/verify_agent_url_routing_spec.js @@ -0,0 +1,164 @@ +const fs = require('fs'); +const path = require('path'); + +let passed = 0; +let failed = 0; + +function assert(condition, testName, detail = '') { + if (condition) { + console.log(` [PASS] ${testName}`); + passed++; + } else { + console.error(` [FAIL] ${testName} - ${detail}`); + failed++; + } +} + +console.log("========================================================"); +console.log(" 分销商成员档案 URL 路由 & 参数回显自动化规范测试"); +console.log("========================================================\n"); + +// 1. 验证 dashboard.js 中 onOpenAgent 修复 +const dashboardJsPath = path.resolve(__dirname, '../public/assets/js/backend/shopro/dashboard.js'); +assert(fs.existsSync(dashboardJsPath), 'dashboard.js 文件存在'); +const dashboardContent = fs.readFileSync(dashboardJsPath, 'utf-8'); + +assert(!dashboardContent.includes('`shopro/commission/agent/index` + (level ? `&level=${level}` : \'\')'), + 'dashboard.js 中已彻底移除错误的 &level= 拼接语法'); + +assert(dashboardContent.includes('`shopro/commission/agent/index` + (level !== undefined && level !== null && level !== \'\' ? `?level=${level}` : \'\')'), + 'dashboard.js 中使用健壮的 ?level= 拼接参数'); + +// 模拟测试 onOpenAgent 各种参数行为 +function mockOnOpenAgent(level) { + let captured = {}; + function onOpenTab(url, title) { + captured = { url, title }; + } + let url = `shopro/commission/agent/index` + (level !== undefined && level !== null && level !== '' ? `?level=${level}` : ''); + let title = (level == 1) ? "品酒师会员" : "成员档案"; + onOpenTab(url, title); + return captured; +} + +const res1 = mockOnOpenAgent(1); +assert(res1.url === 'shopro/commission/agent/index?level=1' && res1.title === '品酒师会员', + '正常场景: level=1 生成正确的 query string ?level=1 且标题为品酒师会员'); + +const resEmpty = mockOnOpenAgent(''); +assert(resEmpty.url === 'shopro/commission/agent/index' && resEmpty.title === '成员档案', + '边界场景: level="" 生成基础无参数 URL'); + +const resNull = mockOnOpenAgent(null); +assert(resNull.url === 'shopro/commission/agent/index' && resNull.title === '成员档案', + '边界场景: level=null 生成基础无参数 URL'); + +const resUndef = mockOnOpenAgent(undefined); +assert(resUndef.url === 'shopro/commission/agent/index' && resUndef.title === '成员档案', + '边界场景: level=undefined 生成基础无参数 URL'); + +// 2. 验证 agent.js URL 参数解析与初始化 +const agentJsPath = path.resolve(__dirname, '../public/assets/js/backend/shopro/commission/agent.js'); +assert(fs.existsSync(agentJsPath), 'agent.js 文件存在'); +const agentContent = fs.readFileSync(agentJsPath, 'utf-8'); + +assert(agentContent.includes('is_partner: \'\''), 'agent.js filter.data 包含 is_partner'); +assert(agentContent.includes('is_partner: {') && agentContent.includes('label: \'合伙股东\''), 'agent.js filter.tools 包含 is_partner 筛选工具'); +assert(agentContent.includes('{ value: \'partner\', label: \'合伙股东\' }'), 'agent.js filter.tools.identity 选项中包含合伙股东'); +assert(agentContent.includes('new URLSearchParams(location.search)'), 'agent.js onMounted 中使用 URLSearchParams 解析参数'); +assert(agentContent.includes('params.get(\'level\')'), 'agent.js onMounted 读取 level 参数'); +assert(agentContent.includes('params.get(\'is_venue\')'), 'agent.js onMounted 读取 is_venue 参数'); +assert(agentContent.includes('params.get(\'is_county\')'), 'agent.js onMounted 读取 is_county 参数'); +assert(agentContent.includes('params.get(\'is_partner\')'), 'agent.js onMounted 读取 is_partner 参数'); +assert(agentContent.includes('params.get(\'identity\')'), 'agent.js onMounted 读取 identity 参数'); +assert(agentContent.includes('params.get(\'user_id\')'), 'agent.js onMounted 读取 user_id 参数'); + +// 模拟测试 agent.js onMounted 参数解析与赋值 +function mockAgentMount(searchString) { + const params = new URLSearchParams(searchString); + let state = { + filter: { + data: { level: '', is_venue: '', is_county: '', is_partner: '', identity: '', user: { field: 'user_id', value: '' } }, + tools: { + level: { value: '' }, + is_venue: { value: '' }, + is_county: { value: '' }, + is_partner: { value: '' }, + identity: { value: '' }, + user: { value: { field: 'user_id', value: '' } } + } + } + }; + + const levelParam = params.get('level'); + if (levelParam !== null && levelParam !== '') { + const parsedLevel = !isNaN(Number(levelParam)) ? Number(levelParam) : levelParam; + state.filter.data.level = parsedLevel; + if (state.filter.tools && state.filter.tools.level) { + state.filter.tools.level.value = parsedLevel; + } + } + const isVenueParam = params.get('is_venue'); + if (isVenueParam !== null && isVenueParam !== '') { + state.filter.data.is_venue = isVenueParam; + if (state.filter.tools && state.filter.tools.is_venue) { + state.filter.tools.is_venue.value = isVenueParam; + } + } + const isCountyParam = params.get('is_county'); + if (isCountyParam !== null && isCountyParam !== '') { + state.filter.data.is_county = isCountyParam; + if (state.filter.tools && state.filter.tools.is_county) { + state.filter.tools.is_county.value = isCountyParam; + } + } + const isPartnerParam = params.get('is_partner'); + if (isPartnerParam !== null && isPartnerParam !== '') { + state.filter.data.is_partner = isPartnerParam; + if (state.filter.tools && state.filter.tools.is_partner) { + state.filter.tools.is_partner.value = isPartnerParam; + } + } + const identityParam = params.get('identity'); + if (identityParam !== null && identityParam !== '') { + state.filter.data.identity = identityParam; + if (state.filter.tools && state.filter.tools.identity) { + state.filter.tools.identity.value = identityParam; + } + } + const userIdParam = params.get('user_id'); + if (userIdParam !== null && userIdParam !== '') { + state.filter.data.user = { field: 'user_id', value: userIdParam }; + if (state.filter.tools && state.filter.tools.user) { + state.filter.tools.user.value = { field: 'user_id', value: userIdParam }; + } + } + + return state; +} + +const mockState = mockAgentMount('?level=1&ref=addtabs'); +assert(mockState.filter.data.level === 1 && mockState.filter.tools.level.value === 1, + 'agent.js 正确从 ?level=1 解析数字等级 1 并同步至 filter.data 与 filter.tools'); + +const mockPartnerState = mockAgentMount('?is_partner=1'); +assert(mockPartnerState.filter.data.is_partner === '1' && mockPartnerState.filter.tools.is_partner.value === '1', + 'agent.js 正确从 ?is_partner=1 解析合伙股东标记'); + +// 3. 验证 AgentFilter.php 过滤能力 +const filterPhpPath = path.resolve(__dirname, '../addons/shopro/filter/commission/AgentFilter.php'); +assert(fs.existsSync(filterPhpPath), 'AgentFilter.php 文件存在'); +const filterPhpContent = fs.readFileSync(filterPhpPath, 'utf-8'); + +assert(filterPhpContent.includes('public function level($query, $data)'), 'AgentFilter 显式定义 level 过滤方法'); +assert(filterPhpContent.includes('public function isPartner($query, $data)'), 'AgentFilter 显式定义 isPartner 过滤方法'); +assert(filterPhpContent.includes('public function isVenue($query, $data)'), 'AgentFilter 显式定义 isVenue 过滤方法'); +assert(filterPhpContent.includes('public function isCounty($query, $data)'), 'AgentFilter 显式定义 isCounty 过滤方法'); +assert(filterPhpContent.includes('if ($value === \'partner\') {') && filterPhpContent.includes('where(\'is_partner\', 1)'), + 'AgentFilter identity 方法支持合伙股东 (partner)'); + +console.log("\n--------------------------------------------------------"); +console.log(`测试执行结果: 通过 ${passed} 项, 失败 ${failed} 项`); +console.log("========================================================"); + +process.exit(failed === 0 ? 0 : 1); diff --git a/tests/verify_commission_goods_rules_spec.js b/tests/verify_commission_goods_rules_spec.js new file mode 100644 index 0000000..065aa29 --- /dev/null +++ b/tests/verify_commission_goods_rules_spec.js @@ -0,0 +1,376 @@ +/** + * 计奖商品独立分成与多规格规则专项验证测试 + * 覆盖范围: + * 1. PartnerConfig (defaultRules, getEffectiveRules, getOrderEffectiveRules) + * 2. PartnerRewardService (settleOrder 动态规则提取与分发) + * 3. 奖项子服务 (Push3Reward, MakerDiffReward, MakerPeerReward, TeamSplitReward, PeerShareReward, VoucherService) + * 4. 后台控制器 Goods.php (detail, edit) + * 5. 后台模板 index.html & edit.html + * 6. 后台前端脚本 goods.js (Vue 表单交互、多规格规则映射、默认值同步) + * 7. 算法行为模拟:默认规则、单规格独立规则、多规格独立规则、混合订单提取、金额与倍数计算 + */ + +const fs = require('fs'); +const path = require('path'); + +let passCount = 0; +let failCount = 0; + +function assert(condition, message) { + if (condition) { + console.log(` \x1b[32m[PASS]\x1b[0m ${message}`); + passCount++; + } else { + console.error(` \x1b[31m[FAIL]\x1b[0m ${message}`); + failCount++; + } +} + +console.log('========================================================'); +console.log(' 计奖商品独立分成与多规格规则专项测试'); +console.log('========================================================\n'); + +// ---------------------------------------------------- +// 1. PartnerConfig.php 验证 +// ---------------------------------------------------- +console.log('--- 1. PartnerConfig.php 规范验证 ---'); +const partnerConfigPath = path.resolve(__dirname, '../addons/shopro/service/partner/PartnerConfig.php'); +assert(fs.existsSync(partnerConfigPath), 'PartnerConfig.php 文件存在'); +if (fs.existsSync(partnerConfigPath)) { + const content = fs.readFileSync(partnerConfigPath, 'utf-8'); + assert(content.includes('public static function defaultRules()'), '包含 defaultRules() 矩阵方法'); + assert(content.includes("'push3_amounts' => self::push3Amounts()"), 'defaultRules 包含 push3_amounts'); + assert(content.includes("'maker_bonus' => self::makerBonus()"), 'defaultRules 包含 maker_bonus'); + assert(content.includes("'maker_peer_rate' => (float) self::get('partner_maker_peer_rate'"), 'defaultRules 包含 maker_peer_rate'); + assert(content.includes("'maker_overtaken_rate' => (float) self::get('partner_maker_overtaken_rate'"), 'defaultRules 包含 maker_overtaken_rate'); + assert(content.includes("'venue_split' => (float) self::get('partner_venue_split'"), 'defaultRules 包含 venue_split'); + assert(content.includes("'county_full' => (float) self::get('partner_county_full'"), 'defaultRules 包含 county_full'); + assert(content.includes("'voucher_times' => (float) self::get('partner_voucher_times'"), 'defaultRules 包含 voucher_times'); + assert(content.includes('public static function getEffectiveRules($goodsId, $skuPriceId = 0)'), '包含 getEffectiveRules($goodsId, $skuPriceId) 方法'); + assert(content.includes('$selfRules !== 1 && $selfRules !== 2'), 'getEffectiveRules 识别 self_rules 规则类型 (0默认/1独立/2批量)'); + assert(content.includes("isset($custom[$skuKey])"), 'getEffectiveRules 支持根据 skuPriceId 匹配特定规格规则'); + assert(content.includes("isset($custom['sku_' . $skuKey])"), 'getEffectiveRules 兼容 sku_ 前缀格式'); + assert(content.includes('public static function getOrderEffectiveRules($order)'), '包含 getOrderEffectiveRules($order) 订单规则解析方法'); + assert(content.includes("!empty($rules['is_custom'])"), 'getOrderEffectiveRules 优先提取商品专属独立规则'); +} + +// ---------------------------------------------------- +// 2. PartnerRewardService.php 验证 +// ---------------------------------------------------- +console.log('\n--- 2. PartnerRewardService.php 调度验证 ---'); +const partnerRewardServicePath = path.resolve(__dirname, '../addons/shopro/service/partner/PartnerRewardService.php'); +assert(fs.existsSync(partnerRewardServicePath), 'PartnerRewardService.php 文件存在'); +if (fs.existsSync(partnerRewardServicePath)) { + const content = fs.readFileSync(partnerRewardServicePath, 'utf-8'); + assert(content.includes('$orderRules = PartnerConfig::getOrderEffectiveRules($order)'), 'settleOrder 调用 getOrderEffectiveRules 提取订单专属规则'); + assert(content.includes('(new Push3Reward())->run($order, $buyerId, $parent, $active, $orderRules)'), 'Push3Reward 透传 $orderRules'); + assert(content.includes('(new MakerDiffReward())->run($order, $buyerId, $parentChain, $active, $orderRules)'), 'MakerDiffReward 透传 $orderRules'); + assert(content.includes('(new MakerPeerReward())->run($order, $buyerId, $parentChain, $diff, $active, $orderRules)'), 'MakerPeerReward 透传 $orderRules'); + assert(content.includes('(new TeamSplitReward())->run($order, $buyerId, $selfChain, $active, $orderRules)'), 'TeamSplitReward 透传 $orderRules'); + assert(content.includes('(new PeerShareReward())->run($order, $buyerId, $selfChain, $team, $active, $orderRules)'), 'PeerShareReward 透传 $orderRules'); + assert(content.includes('(new VoucherService())->grant($order, $buyerId, $orderRules)'), 'VoucherService grant 透传 $orderRules'); +} + +// ---------------------------------------------------- +// 3. 各奖励子服务入参验证 +// ---------------------------------------------------- +console.log('\n--- 3. 奖项子服务参数消费验证 ---'); +const push3Path = path.resolve(__dirname, '../addons/shopro/service/partner/Push3Reward.php'); +if (fs.existsSync(push3Path)) { + const c = fs.readFileSync(push3Path, 'utf-8'); + assert(c.includes('$rules = null') || c.includes('$rules = []'), 'Push3Reward::run 支持接收 $rules'); + assert(c.includes("$rules['push3_amounts']"), 'Push3Reward 优先使用 $rules 中的推三返一循环金额'); +} + +const makerDiffPath = path.resolve(__dirname, '../addons/shopro/service/partner/MakerDiffReward.php'); +if (fs.existsSync(makerDiffPath)) { + const c = fs.readFileSync(makerDiffPath, 'utf-8'); + assert(c.includes('$rules = null') || c.includes('$rules = []'), 'MakerDiffReward::run 支持接收 $rules'); + assert(c.includes("$rules['maker_bonus']"), 'MakerDiffReward 优先使用 $rules 中的各级极差基数'); +} + +const makerPeerPath = path.resolve(__dirname, '../addons/shopro/service/partner/MakerPeerReward.php'); +if (fs.existsSync(makerPeerPath)) { + const c = fs.readFileSync(makerPeerPath, 'utf-8'); + assert(c.includes('$rules = null') || c.includes('$rules = []'), 'MakerPeerReward::run 支持接收 $rules'); + assert(c.includes("$rules['maker_peer_rate']"), 'MakerPeerReward 优先使用 $rules 中的创客平级协同比例'); + assert(c.includes("$rules['maker_overtaken_rate']"), 'MakerPeerReward 优先使用 $rules 中的创客超越卓越比例'); +} + +const teamSplitPath = path.resolve(__dirname, '../addons/shopro/service/partner/TeamSplitReward.php'); +if (fs.existsSync(teamSplitPath)) { + const c = fs.readFileSync(teamSplitPath, 'utf-8'); + assert(c.includes('$rules = null') || c.includes('$rules = []'), 'TeamSplitReward::run 支持接收 $rules'); + assert(c.includes("$rules['venue_split']"), 'TeamSplitReward 优先使用 $rules 中的酒道馆运营分成金额'); + assert(c.includes("$rules['county_full']"), 'TeamSplitReward 优先使用 $rules 中的区县统筹分成金额'); +} + +const peerSharePath = path.resolve(__dirname, '../addons/shopro/service/partner/PeerShareReward.php'); +if (fs.existsSync(peerSharePath)) { + const c = fs.readFileSync(peerSharePath, 'utf-8'); + assert(c.includes('$rules = null') || c.includes('$rules = []'), 'PeerShareReward::run 支持接收 $rules'); + assert(c.includes("$rules['venue_peer_rate']"), 'PeerShareReward 优先使用 $rules 中的酒道馆协同比例'); + assert(c.includes("$rules['county_peer_rate']"), 'PeerShareReward 优先使用 $rules 中的区县协同比例'); +} + +const voucherPath = path.resolve(__dirname, '../addons/shopro/service/partner/VoucherService.php'); +if (fs.existsSync(voucherPath)) { + const c = fs.readFileSync(voucherPath, 'utf-8'); + assert(c.includes('$rules = null') || c.includes('$rules = []'), 'VoucherService::grant 支持接收 $rules'); + assert(c.includes("$rules['voucher_times']"), 'VoucherService 优先使用 $rules 中的消费券倍数'); + assert(c.includes("$rules['voucher_days']"), 'VoucherService 优先使用 $rules 中的消费券释放周期天数'); +} + +// ---------------------------------------------------- +// 4. 后台控制器 Goods.php 验证 +// ---------------------------------------------------- +console.log('\n--- 4. 后台控制器 Goods.php 验证 ---'); +const goodsControllerPath = path.resolve(__dirname, '../application/admin/controller/shopro/commission/Goods.php'); +assert(fs.existsSync(goodsControllerPath), 'Goods.php 控制器存在'); +if (fs.existsSync(goodsControllerPath)) { + const c = fs.readFileSync(goodsControllerPath, 'utf-8'); + assert(c.includes('PartnerConfig::defaultRules()'), 'detail() 返回系统全局 default_partner_rules'); + assert(c.includes('PartnerConfig::roleNames()'), 'detail() 返回定制化 role_names'); + assert(c.includes('Level::order'), 'detail() 返回创客等级 levels 列表'); + assert(c.includes("$params = $this->request->only(['status', 'self_rules', 'commission_order_status'"), 'edit() 严格获取指定参数'); + assert(c.includes("intval($params['status']) === 1 ? 1 : 0"), 'edit() 规范化 status 为 0/1'); + assert(c.includes("intval($params['commission_order_status']) === 0 ? 0 : 1"), 'edit() 规范化 commission_order_status 为 0/1'); + assert(c.includes("json_decode($params['commission_rules'], true)"), 'edit() 对 commission_rules 进行 JSON 防御校验'); + assert(c.includes('Db::transaction'), 'edit() 使用数据库事务保证批量或单条保存原子性'); +} + +// ---------------------------------------------------- +// 5. 后台视图与前端脚本验证 +// ---------------------------------------------------- +console.log('\n--- 5. 后台视图与前端脚本验证 ---'); +const indexPath = path.resolve(__dirname, '../application/admin/view/shopro/commission/goods/index.html'); +assert(fs.existsSync(indexPath), 'index.html 存在'); +if (fs.existsSync(indexPath)) { + const c = fs.readFileSync(indexPath, 'utf-8'); + assert(c.includes('label="分成规则"'), '列表包含“分成规则”列'); + assert(c.includes('默认规则'), '列表包含“默认规则” Tag 展现'); + assert(c.includes('独立规则'), '列表包含“独立规则” Tag 展现'); + assert(c.includes('不参与计奖'), '列表包含“不参与计奖” Tag 展现'); + assert(c.includes('设置分成'), '操作区包含“设置分成”按钮'); +} + +const editPath = path.resolve(__dirname, '../application/admin/view/shopro/commission/goods/edit.html'); +assert(fs.existsSync(editPath), 'edit.html 存在'); +if (fs.existsSync(editPath)) { + const c = fs.readFileSync(editPath, 'utf-8'); + assert(c.includes('v-model="state.commission_goods.status"'), '包含参与计奖开关状态'); + assert(c.includes('v-model="state.commission_goods.self_rules"'), '包含分成规则单选 (0默认/1独立/2批量)'); + assert(c.includes('推三返一循环返现'), '表单包含“推三返一循环返现”卡片'); + assert(c.includes('极差基数'), '表单包含“各级极差基数”卡片'); + assert(c.includes('创客下级反超卓越比例') || c.includes('协同奖'), '表单包含“协同与超越卓越奖”卡片'); + assert(c.includes('酒道馆运营奖基准') || c.includes('实体渠道与区域运营奖'), '表单包含“实体渠道与区域运营分成”卡片'); + assert(c.includes('消费补贴兑换券计划'), '表单包含“消费补贴兑换券计划”卡片'); + assert(c.includes('全平台日结均分奖池注入'), '表单包含“全平台日结均分奖池注入”卡片'); + assert(c.includes('@click="onFillDefaults'), '包含快速填入系统默认规则按钮'); + assert(c.includes('@click="onSyncToAllSkus"'), '包含同步至全部规格按钮'); +} + +const goodsJsPath = path.resolve(__dirname, '../public/assets/js/backend/shopro/commission/goods.js'); +assert(fs.existsSync(goodsJsPath), 'goods.js 存在'); +if (fs.existsSync(goodsJsPath)) { + const c = fs.readFileSync(goodsJsPath, 'utf-8'); + assert(c.includes("'commission_goods.self_rules'"), '包含分成规则筛选器'); + assert(c.includes('function makeFormFromDefaults'), '包含 makeFormFromDefaults 工具函数'); + assert(c.includes('function rulesToForm'), '包含 rulesToForm 工具函数'); + assert(c.includes('function formToRules'), '包含 formToRules 工具函数'); + assert(c.includes('state.skuMode === \'sku\''), '支持按规格分别设置与统一设置判断'); + assert(c.includes('state.skuRules[sku.id]'), '保存时正确提取各规格独立规则映射'); +} + +// ---------------------------------------------------- +// 6. 算法行为与业务逻辑模拟 +// ---------------------------------------------------- +console.log('\n--- 6. 算法行为与业务逻辑模拟断言 ---'); + +const mockSystemDefaults = { + push3_amounts: [398, 400, 600], + maker_bonus: { 2: 20, 3: 40, 4: 60, 5: 80, 6: 100, 7: 120 }, + maker_peer_rate: 0.10, + maker_overtaken_rate: 0.05, + venue_split: 50, + county_full: 100, + venue_peer_rate: 0.10, + county_peer_rate: 0.10, + voucher_times: 1.5, + voucher_days: 365, + venue_share: 100, + county_share: 50, + is_custom: false, +}; + +function mockGetEffectiveRules(goodsRecord, skuPriceId = 0) { + const rules = JSON.parse(JSON.stringify(mockSystemDefaults)); + if (!goodsRecord || Number(goodsRecord.status) !== 1) { + rules.status = 0; + return rules; + } + rules.status = 1; + rules.commission_order_status = Number(goodsRecord.commission_order_status ?? 1); + const selfRules = Number(goodsRecord.self_rules ?? 0); + rules.self_rules = selfRules; + + if (selfRules !== 1 && selfRules !== 2) { + return rules; // 跟随默认规则 + } + + let custom = goodsRecord.commission_rules; + if (typeof custom === 'string') { + try { + custom = JSON.parse(custom); + } catch (e) { + custom = {}; + } + } + if (!custom || Object.keys(custom).length === 0) { + return rules; + } + + const skuKey = Number(skuPriceId); + if (skuKey > 0 && custom[skuKey] && typeof custom[skuKey] === 'object') { + custom = custom[skuKey]; + } else if (skuKey > 0 && custom['sku_' + skuKey] && typeof custom['sku_' + skuKey] === 'object') { + custom = custom['sku_' + skuKey]; + } + + if (Array.isArray(custom.push3_amounts) && custom.push3_amounts.length >= 3) { + rules.push3_amounts = custom.push3_amounts.map(Number); + } + if (custom.maker_bonus && typeof custom.maker_bonus === 'object') { + const mb = {}; + for (const [k, v] of Object.entries(custom.maker_bonus)) { + mb[parseInt(k)] = Number(v); + } + rules.maker_bonus = mb; + } + if (custom.maker_peer_rate !== undefined) rules.maker_peer_rate = Number(custom.maker_peer_rate); + if (custom.maker_overtaken_rate !== undefined) rules.maker_overtaken_rate = Number(custom.maker_overtaken_rate); + if (custom.venue_split !== undefined) rules.venue_split = Number(custom.venue_split); + if (custom.county_full !== undefined) rules.county_full = Number(custom.county_full); + if (custom.voucher_times !== undefined) rules.voucher_times = Number(custom.voucher_times); + if (custom.voucher_days !== undefined) rules.voucher_days = parseInt(custom.voucher_days); + if (custom.venue_share !== undefined) rules.venue_share = Number(custom.venue_share); + if (custom.county_share !== undefined) rules.county_share = Number(custom.county_share); + + rules.is_custom = true; + return rules; +} + +// 模拟场景 1: 商品未开启计奖 (status = 0) +const uncommissionedGoods = { goods_id: 201, status: 0, self_rules: 1 }; +const res1 = mockGetEffectiveRules(uncommissionedGoods); +assert(res1.status === 0, '模拟场景 1: status=0 商品被标记为不参与计奖'); +assert(res1.is_custom === false, '模拟场景 1: status=0 商品不激活独立规则'); + +// 模拟场景 2: 开启计奖但使用默认规则 (self_rules = 0) +const defaultGoods = { goods_id: 202, status: 1, self_rules: 0 }; +const res2 = mockGetEffectiveRules(defaultGoods); +assert(res2.status === 1 && res2.self_rules === 0, '模拟场景 2: 开启计奖且 self_rules=0'); +assert(res2.push3_amounts[0] === 398 && res2.push3_amounts[1] === 400 && res2.push3_amounts[2] === 600, '模拟场景 2: 采用系统默认推三返一金额 (398/400/600)'); +assert(res2.voucher_times === 1.5, '模拟场景 2: 采用系统默认兑换券倍数 (1.5)'); +assert(res2.is_custom === false, '模拟场景 2: is_custom 为 false'); + +// 模拟场景 3: 独立规则设置单品分成 (self_rules = 1, 单规格) +const customGoods = { + goods_id: 203, + status: 1, + self_rules: 1, + commission_rules: JSON.stringify({ + push3_amounts: [500, 500, 800], + maker_bonus: { 2: 50, 3: 100, 4: 150, 5: 200, 6: 250, 7: 300 }, + venue_split: 80, + county_full: 150, + voucher_times: 2.0, + voucher_days: 180 + }) +}; +const res3 = mockGetEffectiveRules(customGoods); +assert(res3.status === 1 && res3.is_custom === true, '模拟场景 3: 成功识别单品独立规则 (is_custom = true)'); +assert(res3.push3_amounts[0] === 500 && res3.push3_amounts[2] === 800, '模拟场景 3: 推三返一正确覆盖为独立金额 [500, 500, 800]'); +assert(res3.maker_bonus[2] === 50 && res3.maker_bonus[3] === 100, '模拟场景 3: 创客极差正确覆盖为定制梯度'); +assert(res3.venue_split === 80 && res3.county_full === 150, '模拟场景 3: 渠道运营奖覆盖为 80 / 150'); +assert(res3.voucher_times === 2.0 && res3.voucher_days === 180, '模拟场景 3: 兑换券计划覆盖为 2.0 倍及 180 天'); +assert(res3.maker_peer_rate === 0.10, '模拟场景 3: 未设置项 (maker_peer_rate) 自动平滑回退至全局默认值 (0.10)'); + +// 模拟场景 4: 多规格分别设置独立分成 (多 SKU) +const multiSkuGoods = { + goods_id: 204, + status: 1, + self_rules: 1, + commission_rules: JSON.stringify({ + 1001: { // 500ml 规格 + push3_amounts: [200, 200, 300], + venue_split: 30, + voucher_times: 1.2 + }, + 1002: { // 1000ml 礼盒装规格 + push3_amounts: [600, 600, 1000], + venue_split: 120, + voucher_times: 2.5 + } + }) +}; +const resSku1001 = mockGetEffectiveRules(multiSkuGoods, 1001); +const resSku1002 = mockGetEffectiveRules(multiSkuGoods, 1002); +assert(resSku1001.push3_amounts[0] === 200 && resSku1001.venue_split === 30 && resSku1001.voucher_times === 1.2, '模拟场景 4: 规格 1001 准确读取小规格独立规则 (推三返一:200, 酒道馆:30, 券倍数:1.2)'); +assert(resSku1002.push3_amounts[0] === 600 && resSku1002.venue_split === 120 && resSku1002.voucher_times === 2.5, '模拟场景 4: 规格 1002 准确读取大礼盒独立规则 (推三返一:600, 酒道馆:120, 券倍数:2.5)'); + +// 模拟场景 5: 订单级规则提取优先级 +function mockGetOrderEffectiveRules(order, goodsMap) { + for (const item of (order.items || [])) { + if (item.ext && item.ext.is_commission === false) continue; + const g = goodsMap[item.goods_id]; + const r = mockGetEffectiveRules(g, item.goods_sku_price_id); + if (r.is_custom) { + return r; + } + } + return JSON.parse(JSON.stringify(mockSystemDefaults)); +} + +const mixedOrder = { + id: 9901, + items: [ + { goods_id: 201, goods_sku_price_id: 0, pay_fee: 300, ext: { is_commission: false } }, // 不计奖 + { goods_id: 202, goods_sku_price_id: 0, pay_fee: 500, ext: { is_commission: true } }, // 默认规则 + { goods_id: 204, goods_sku_price_id: 1002, pay_fee: 1000, ext: { is_commission: true } } // 规格独立规则 + ] +}; +const orderEffective = mockGetOrderEffectiveRules(mixedOrder, { + 201: uncommissionedGoods, + 202: defaultGoods, + 204: multiSkuGoods +}); +assert(orderEffective.is_custom === true, '模拟场景 5: 混合订单成功命中并优先提取独立规则商品配置'); +assert(orderEffective.push3_amounts[0] === 600 && orderEffective.voucher_times === 2.5, '模拟场景 5: 订单实际结算采用独立规则 (push3:600, 券倍数:2.5)'); + +// 模拟场景 6: 奖项结算金额计算断言 +// (1) 推三返一:第 1 单、第 2 单、第 3 单按商品定制发放 +const cycleRound = [1, 2, 3]; +const push3Pay = cycleRound.map((round) => { + const idx = (round - 1) % 3; + return orderEffective.push3_amounts[idx]; +}); +assert(push3Pay[0] === 600 && push3Pay[1] === 600 && push3Pay[2] === 1000, '模拟场景 6.1: 推三返一按定制金额循环 (600 -> 600 -> 1000)'); + +// (2) 消费补贴券计算 +const qualifiedPayFee = 1000; // 假设计奖实付 1000 元 +const voucherAmount = qualifiedPayFee * orderEffective.voucher_times; +assert(voucherAmount === 2500, `模拟场景 6.2: 消费券根据定制 2.5 倍计算实发 2500 券(默认1.5倍为1500)`); + +// ---------------------------------------------------- +// 测试汇总输出 +// ---------------------------------------------------- +console.log('\n--------------------------------------------------------'); +console.log(`测试执行结果: 通过 ${passCount} 项, 失败 ${failCount} 项`); +console.log('========================================================\n'); + +if (failCount > 0) { + process.exit(1); +} else { + process.exit(0); +} diff --git a/tests/verify_score_shop_voucher_integration_spec.js b/tests/verify_score_shop_voucher_integration_spec.js index 9e16400..e65fb0f 100644 --- a/tests/verify_score_shop_voucher_integration_spec.js +++ b/tests/verify_score_shop_voucher_integration_spec.js @@ -56,6 +56,15 @@ if (fs.existsSync(voucherServicePath)) { assert(content.includes('$remainVoucher'), 'refundConsume 严格限制退回上限不超过剩余未退额'); } +// 2.2 PayOper.php 同步核销与防双花验证 +const payOperPath = path.resolve(__dirname, '../addons/shopro/service/pay/PayOper.php'); +assert(fs.existsSync(payOperPath), 'PayOper.php 文件存在'); +if (fs.existsSync(payOperPath)) { + const content = fs.readFileSync(payOperPath, 'utf-8'); + assert(content.includes('consumeByOrder($order)'), 'PayOper.php checkAndPaid 包含兑换券即时同步核销调用'); + assert(content.includes('voucher_consumed'), 'PayOper.php 具备 voucher_consumed 幂等检查'); +} + // 3. confirm.vue 验证 const confirmVuePath = path.resolve(__dirname, '../uniapp/pages/order/confirm.vue'); assert(fs.existsSync(confirmVuePath), 'confirm.vue 文件存在'); @@ -83,7 +92,7 @@ if (fs.existsSync(scoreShopVuePath)) { assert(content.includes('userVoucherAvailable'), '积分商城首页引入可用兑换券计算属性'); assert(content.includes('当前积分'), '展示当前积分余额看板'); assert(content.includes('可用兑换券'), '展示可用兑换券余额看板'); - assert(content.includes('💡 兑换商品时支持使用兑换券 1:1 抵扣现金部分'), '包含兑换券抵扣说明提示'); + assert(content.includes('全额抵扣现金'), '包含兑换券全额抵扣说明提示'); assert(content.includes('/pages/commission/voucher'), '提供快捷跳转兑换券明细入口'); } @@ -93,7 +102,30 @@ assert(fs.existsSync(scoreDetailVuePath), 'score.vue 文件存在'); if (fs.existsSync(scoreDetailVuePath)) { const content = fs.readFileSync(scoreDetailVuePath, 'utf-8'); assert(content.includes('voucher-tip-tag'), '积分商品详情包含兑换券抵扣标签组件'); - assert(content.includes('支持兑换券抵扣'), '积分商品详情标明支持兑换券抵扣现金'); + assert(content.includes('支持兑换券全额抵扣'), '积分商品详情标明支持兑换券全额抵扣'); +} + +// 6.1 后台页面及模型验证 +const scoreSkuPricePath = path.resolve(__dirname, '../application/admin/model/shopro/app/ScoreSkuPrice.php'); +assert(fs.existsSync(scoreSkuPricePath), 'ScoreSkuPrice.php 文件存在'); +if (fs.existsSync(scoreSkuPricePath)) { + const content = fs.readFileSync(scoreSkuPricePath, 'utf-8'); + assert(content.includes('兑换券'), 'ScoreSkuPrice 具备兑换券输出属性'); +} + +const addHtmlPath = path.resolve(__dirname, '../application/admin/view/shopro/app/score_shop/add.html'); +assert(fs.existsSync(addHtmlPath), 'add.html 文件存在'); +if (fs.existsSync(addHtmlPath)) { + const content = fs.readFileSync(addHtmlPath, 'utf-8'); + assert(content.includes('所需兑换券(元)'), 'add.html 包含所需兑换券表头'); + assert(content.includes('所需积分'), 'add.html 包含所需积分表头'); +} + +const indexHtmlPath = path.resolve(__dirname, '../application/admin/view/shopro/app/score_shop/index.html'); +assert(fs.existsSync(indexHtmlPath), 'index.html 文件存在'); +if (fs.existsSync(indexHtmlPath)) { + const content = fs.readFileSync(indexHtmlPath, 'utf-8'); + assert(content.includes('兑换门槛 (兑换券/积分)'), 'index.html 包含兑换门槛表头'); } // 7. 结算抵扣算法模拟 diff --git a/tests/verify_team_center_full_spec.js b/tests/verify_team_center_full_spec.js new file mode 100644 index 0000000..13802dd --- /dev/null +++ b/tests/verify_team_center_full_spec.js @@ -0,0 +1,115 @@ +/** + * 团队作战中心全功能补齐与手机号脱敏专项验证测试 + * 覆盖范围: + * 1. 服务端:Agent.php 手机号脱敏与收益业绩统计 + * 2. 服务端:Index.php 收益明细 buyer_id 穿透过滤与 buyer_info + * 3. 移动端:team.vue 专属邀请码、团队收益看板、卡片脱敏、一键拨打/复制/查收益 + * 4. 移动端:rewards.vue 穿透筛选提示、参数捕获与 API 传参 + * 5. 移动端:index.vue 收益中心首页团队快捷入口看板 + */ + +const fs = require('fs'); +const path = require('path'); + +let passCount = 0; +let failCount = 0; + +function assert(condition, testName, detail = '') { + if (condition) { + console.log(` \x1b[32m[PASS]\x1b[0m ${testName}`); + passCount++; + } else { + console.error(` \x1b[31m[FAIL]\x1b[0m ${testName} - ${detail}`); + failCount++; + } +} + +console.log('========================================================'); +console.log(' 团队作战中心全功能闭环与手机号脱敏专项验证测试'); +console.log('========================================================\n'); + +// 1. 服务端 Agent.php 校验 +const agentPhpPath = path.resolve(__dirname, '../addons/shopro/controller/commission/Agent.php'); +assert(fs.existsSync(agentPhpPath), 'Agent.php 控制器存在'); +if (fs.existsSync(agentPhpPath)) { + const content = fs.readFileSync(agentPhpPath, 'utf-8'); + assert(content.includes("use app\\admin\\model\\shopro\\partner\\Reward as RewardModel;"), 'Agent.php 引入了 RewardModel'); + assert(content.includes('$user->mobile = $maskedMobile;'), 'Agent.php 覆写了 $user->mobile 为脱敏号码'); + assert(content.includes('$user->mobile_hide = $maskedMobile;'), 'Agent.php 提供了 $user->mobile_hide 字段'); + assert(content.includes('$user->mobile_raw = $rawMobile;'), 'Agent.php 保留了 $user->mobile_raw 供客户端外呼'); + assert(content.includes('$user->parentUser->mobile = $pMasked;'), 'Agent.php 对所属间推推荐人手机号执行了脱敏'); + assert(content.includes("'team_total_consume'"), 'getTeamHealthSummary 返回团队自购总业绩'); + assert(content.includes("'team_total_reward'"), 'getTeamHealthSummary 返回团队贡献总收益'); + assert(content.includes("'team_month_reward'"), 'getTeamHealthSummary 返回本月团队贡献收益'); + assert(content.includes("'team_order_count'"), 'getTeamHealthSummary 返回团队总订单数'); +} + +// 2. 服务端 partner/Index.php 校验 +const partnerIndexPhpPath = path.resolve(__dirname, '../addons/shopro/controller/partner/Index.php'); +assert(fs.existsSync(partnerIndexPhpPath), 'partner/Index.php 控制器存在'); +if (fs.existsSync(partnerIndexPhpPath)) { + const content = fs.readFileSync(partnerIndexPhpPath, 'utf-8'); + assert(content.includes("intval($this->request->param('buyer_id', 0))"), 'rewards 接口接收 buyer_id 参数'); + assert(content.includes("$query->where('buyer_id', $buyerId);"), 'rewards 接口支持按 buyer_id 过滤明细'); + assert(content.includes("'buyer_info' => $buyerInfo"), 'rewards 接口返回穿透成员信息 buyer_info'); +} + +// 3. 移动端 team.vue 校验 +const teamVuePath = path.resolve(__dirname, '../uniapp/pages/commission/team.vue'); +assert(fs.existsSync(teamVuePath), 'team.vue 页面文件存在'); +if (fs.existsSync(teamVuePath)) { + const content = fs.readFileSync(teamVuePath, 'utf-8'); + // 专属邀请码 + assert(content.includes('专属邀请码:'), 'team.vue 包含专属邀请码标签'); + assert(content.includes('HL{{ userInfo.id ||'), 'team.vue 包含 HL + 用户ID 的邀请码渲染'); + assert(content.includes('onCopyInviteCode'), 'team.vue 包含 onCopyInviteCode 复制方法'); + assert(content.includes('onCopyShareLink'), 'team.vue 包含 onCopyShareLink 复制链接方法'); + // 团队收益看板 + assert(content.includes('team-wealth-box'), 'team.vue 包含 team-wealth-box 团队收益看板'); + assert(content.includes('state.summary.team_total_reward'), 'team.vue 绑定团队贡献总收益'); + assert(content.includes('state.summary.team_total_consume'), 'team.vue 绑定团队自购总业绩'); + assert(content.includes('state.summary.team_month_reward'), 'team.vue 绑定本月贡献收益'); + assert(content.includes('state.summary.team_order_count'), 'team.vue 绑定团队订单数'); + // 手机号脱敏展示与操作 + assert(content.includes('item.mobile_hide'), 'team.vue 列表卡片展示脱敏手机号'); + assert(content.includes('state.currentMember.mobile_hide'), 'team.vue 抽屉详情展示脱敏手机号'); + assert(content.includes('btn-reward'), 'team.vue 列表卡片提供直达收益流水按钮'); + assert(content.includes('onJumpRewards(item.id, item.nickname)'), 'team.vue 卡片支持一键穿透查收益流水'); + assert(content.includes('onJumpRewards(state.currentMember.id, state.currentMember.nickname)'), 'team.vue 抽屉支持一键穿透查收益流水'); +} + +// 4. 移动端 rewards.vue 校验 +const rewardsVuePath = path.resolve(__dirname, '../uniapp/pages/commission/rewards.vue'); +assert(fs.existsSync(rewardsVuePath), 'rewards.vue 页面文件存在'); +if (fs.existsSync(rewardsVuePath)) { + const content = fs.readFileSync(rewardsVuePath, 'utf-8'); + assert(content.includes('buyer-filter-bar'), 'rewards.vue 包含 buyer-filter-bar 穿透筛选提示条'); + assert(content.includes('onClearBuyerFilter'), 'rewards.vue 包含 onClearBuyerFilter 清除筛选方法'); + assert(content.includes('options?.buyer_id'), 'rewards.vue onLoad 中捕获 buyer_id 参数'); + assert(content.includes('params.buyer_id = state.buyerId;'), 'rewards.vue 请求 API 时传递 buyer_id'); +} + +// 5. 移动端 index.vue 校验 +const indexVuePath = path.resolve(__dirname, '../uniapp/pages/commission/index.vue'); +assert(fs.existsSync(indexVuePath), 'index.vue 页面文件存在'); +if (fs.existsSync(indexVuePath)) { + const content = fs.readFileSync(indexVuePath, 'utf-8'); + assert(content.includes('team-quick-card'), 'index.vue 包含 team-quick-card 快捷看板'); + assert(content.includes('团队作战中心'), 'index.vue 明确展示团队作战中心标题'); + assert(content.includes('/pages/commission/team'), 'index.vue 点击直达 /pages/commission/team'); +} + +// 6. 算法测试:脱敏与邀请码逻辑 +const testMobile = "13912345678"; +const maskedMobile = testMobile.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2'); +assert(maskedMobile === "139****5678", `手机号脱敏算法验证正常: ${maskedMobile}`); + +const testUserId = 2048; +const inviteCode = `HL${testUserId}`; +assert(inviteCode === "HL2048", `邀请码生成格式符合规范: ${inviteCode}`); + +console.log('\n--------------------------------------------------------'); +console.log(`测试结果统计: 通过 ${passCount} 项, 失败 ${failCount} 项`); +console.log('========================================================'); + +process.exit(failCount === 0 ? 0 : 1); diff --git a/tests/verify_wechat_quick_login_spec.js b/tests/verify_wechat_quick_login_spec.js new file mode 100644 index 0000000..170c672 --- /dev/null +++ b/tests/verify_wechat_quick_login_spec.js @@ -0,0 +1,237 @@ +const fs = require('fs'); +const path = require('path'); + +let passed = 0; +let failed = 0; + +function assert(condition, testName, detail = '') { + if (condition) { + console.log(` [PASS] ${testName}`); + passed++; + } else { + console.error(` [FAIL] ${testName} - ${detail}`); + failed++; + } +} + +console.log("========================================================"); +console.log(" 微信小程序默认登录采用手机号一键快捷登录规格测试"); +console.log("========================================================\n"); + +// 1. 后端服务层: MiniProgram.php +const miniProgramPhpPath = path.resolve(__dirname, '../addons/shopro/service/third/wechat/MiniProgram.php'); +assert(fs.existsSync(miniProgramPhpPath), 'MiniProgram.php 文件存在'); +const miniProgramPhpContent = fs.readFileSync(miniProgramPhpPath, 'utf-8'); + +assert( + miniProgramPhpContent.includes('wxa/business/getuserphonenumber') && miniProgramPhpContent.includes("['code']"), + 'MiniProgram.php 支持微信新版 code 获取手机号接口', + '未包含 wxa/business/getuserphonenumber 逻辑' +); + +assert( + miniProgramPhpContent.includes('encryptor->decryptData') && miniProgramPhpContent.includes('purePhoneNumber'), + 'MiniProgram.php 保留传统 iv + encryptedData 解密降级兼容', + '未包含 decryptData 解密兼容' +); + +assert( + miniProgramPhpContent.includes('$mobile = $this->getUserPhoneNumber()') && miniProgramPhpContent.includes("'mobile' => $mobile"), + 'MiniProgram.php 在 login() 方法中当传入手机号凭证时自动提取 mobile', + 'login() 中未提取手机号' +); + +// 2. 后端服务层: Wechat.php +const wechatServicePath = path.resolve(__dirname, '../addons/shopro/service/third/wechat/Wechat.php'); +assert(fs.existsSync(wechatServicePath), 'Wechat.php (Service) 文件存在'); +const wechatServiceContent = fs.readFileSync(wechatServicePath, 'utf-8'); + +assert( + wechatServiceContent.includes("UserModel::where('mobile', $extend['mobile'])->find()"), + 'Wechat.php 在 registerOrBindUser 中检查已有手机号并直接绑定关联', + '未检测到已有手机号用户直接关联逻辑' +); + +assert( + wechatServiceContent.includes('$user->mobile = $extend['), + 'Wechat.php 为已绑定微信但尚无手机号的用户自动补充手机号', + '未检测到已绑定账号补充手机号逻辑' +); + +// 3. 后端控制器层: Wechat.php +const wechatControllerPath = path.resolve(__dirname, '../addons/shopro/controller/third/Wechat.php'); +assert(fs.existsSync(wechatControllerPath), 'Wechat.php (Controller) 文件存在'); +const wechatControllerContent = fs.readFileSync(wechatControllerPath, 'utf-8'); + +assert( + wechatControllerContent.includes("'token' => $auth->isLogin() ? $auth->getToken() : ''"), + 'Wechat.php (Controller) 在 login 成功响应数据中返回 token', + '响应未返回 token' +); + +// 4. 前端平台驱动层: miniProgram.js +const miniProgramJsPath = path.resolve(__dirname, '../uniapp/sheep/platform/provider/wechat/miniProgram.js'); +assert(fs.existsSync(miniProgramJsPath), 'miniProgram.js 文件存在'); +const miniProgramJsContent = fs.readFileSync(miniProgramJsPath, 'utf-8'); + +assert( + miniProgramJsContent.includes('const mobileLogin = async (e) => {') && miniProgramJsContent.includes('third.wechat.login'), + 'miniProgram.js 包含封装完善的 mobileLogin 方法', + '未包含 mobileLogin 方法' +); + +assert( + miniProgramJsContent.includes('mobileLogin,') && miniProgramJsContent.includes('export default {'), + 'miniProgram.js 在 export default 中导出了 mobileLogin', + '未导出 mobileLogin' +); + +// 5. 前端弹窗状态控制: useModal.js +const useModalPath = path.resolve(__dirname, '../uniapp/sheep/hooks/useModal.js'); +assert(fs.existsSync(useModalPath), 'useModal.js 文件存在'); +const useModalContent = fs.readFileSync(useModalPath, 'utf-8'); + +assert( + useModalContent.includes('MP-WEIXIN') && useModalContent.includes("targetType = 'mpMobileLogin'"), + 'useModal.js 微信小程序环境下默认弹窗类型为 mpMobileLogin', + '未设置微信小程序默认弹窗类型为 mpMobileLogin' +); + +// 6. 新增组件: mp-mobile-login.vue +const mpMobileLoginPath = path.resolve(__dirname, '../uniapp/sheep/components/s-auth-modal/components/mp-mobile-login.vue'); +assert(fs.existsSync(mpMobileLoginPath), 'mp-mobile-login.vue 文件存在'); +const mpMobileLoginContent = fs.readFileSync(mpMobileLoginPath, 'utf-8'); + +assert( + mpMobileLoginContent.includes('open-type="getPhoneNumber"') && mpMobileLoginContent.includes('@getphonenumber="onGetPhoneNumber"'), + 'mp-mobile-login.vue 包含 open-type="getPhoneNumber" 授权按钮', + '缺少 getPhoneNumber 授权按钮' +); + +assert( + mpMobileLoginContent.includes("showAuthModal('smsLogin')") && mpMobileLoginContent.includes("showAuthModal('accountLogin')"), + 'mp-mobile-login.vue 提供切换短信登录和账号登录的快速入口', + '缺少其他登录方式入口' +); + +assert( + mpMobileLoginContent.includes('agreeStatus') && mpMobileLoginContent.includes('mobileLogin'), + 'mp-mobile-login.vue 校验用户协议并调用 mobileLogin 登录', + '缺少协议校验或登录调用' +); + +// 7. 弹窗容器: s-auth-modal.vue +const sAuthModalPath = path.resolve(__dirname, '../uniapp/sheep/components/s-auth-modal/s-auth-modal.vue'); +assert(fs.existsSync(sAuthModalPath), 's-auth-modal.vue 文件存在'); +const sAuthModalContent = fs.readFileSync(sAuthModalPath, 'utf-8'); + +assert( + sAuthModalContent.includes("import mpMobileLogin from './components/mp-mobile-login.vue'") && sAuthModalContent.includes("authType === 'mpMobileLogin'"), + 's-auth-modal.vue 成功挂载 mp-mobile-login 组件', + '未挂载 mp-mobile-login' +); + +assert( + sAuthModalContent.includes("'mpMobileLogin'") && sAuthModalContent.includes('agreement-box'), + 's-auth-modal.vue 协议栏支持 mpMobileLogin 显示与勾选状态关联', + '协议栏未支持 mpMobileLogin' +); + +// 8. 个人中心卡片组件: s-user-card.vue +const sUserCardPath = path.resolve(__dirname, '../uniapp/sheep/components/s-user-card/s-user-card.vue'); +assert(fs.existsSync(sUserCardPath), 's-user-card.vue 文件存在'); +const sUserCardContent = fs.readFileSync(sUserCardPath, 'utf-8'); + +assert( + sUserCardContent.includes('#ifdef MP-WEIXIN') && sUserCardContent.includes('open-type="getPhoneNumber"') && sUserCardContent.includes('@getphonenumber="onQuickLogin"'), + 's-user-card.vue 在微信小程序端未登录时渲染 open-type="getPhoneNumber" 快捷登录按钮', + '未检测到微信小程序快捷登录按钮' +); + +assert( + sUserCardContent.includes('async function onQuickLogin(e)') && sUserCardContent.includes("mobileLogin(e.detail)"), + 's-user-card.vue 包含 onQuickLogin 处理函数调用 mobileLogin 完成快捷登录', + '未检测到 onQuickLogin 处理逻辑' +); + +assert( + sUserCardContent.includes('hasBoundMobile') && sUserCardContent.includes('userInfo.value?.mobile || userInfo.value?.verification?.mobile'), + 's-user-card.vue 使用 hasBoundMobile 判定:只要存在手机号则不展示绑定提示条', + '缺少 hasBoundMobile 判定逻辑' +); + +assert( + sUserCardContent.includes('onBindPhoneNumber') && sUserCardContent.includes('bindUserPhoneNumber(e.detail)'), + 's-user-card.vue 在微信小程序端去绑定支持 open-type="getPhoneNumber" 一键绑定', + '缺少一键去绑定逻辑' +); + +assert( + sUserCardContent.includes('#ifndef MP-WEIXIN') && sUserCardContent.includes('showAuthModal()'), + 's-user-card.vue 保留非微信小程序端正常点击弹窗唤醒逻辑', + '未保留非微信小程序端唤醒逻辑' +); + +// 9. 用户模型与用户仓库:User.php & user.js +const userModelPath = path.resolve(__dirname, '../application/admin/model/shopro/user/User.php'); +assert(fs.existsSync(userModelPath), 'User.php 模型存在'); +const userModelContent = fs.readFileSync(userModelPath, 'utf-8'); + +assert( + userModelContent.includes("if (!empty($data['mobile']))") && userModelContent.includes("$value['mobile'] = 1"), + 'User.php 模型 getVerificationAttr 当会员已有手机号时自动认定 mobile = 1', + '缺少 mobile 存在时自动认定 mobile = 1 逻辑' +); + +assert( + userModelContent.includes('function setVerificationAttr'), + 'User.php 模型定义了 setVerificationAttr 支持对象/数组自动 json 序列化', + '缺少 setVerificationAttr 方法' +); + +const userStorePath = path.resolve(__dirname, '../uniapp/sheep/store/user.js'); +assert(fs.existsSync(userStorePath), 'user.js Store 存在'); +const userStoreContent = fs.readFileSync(userStorePath, 'utf-8'); + +assert( + userStoreContent.includes('if (merged.mobile)') && userStoreContent.includes('mobile: 1'), + 'user.js getInfo() 当存在 mobile 时自动认定 verification.mobile = 1', + '缺少 getInfo() 补充 verification.mobile 逻辑' +); + +// 10. 售后申请页编译修复: apply.vue +const applyVuePath = path.resolve(__dirname, '../uniapp/pages/order/aftersale/apply.vue'); +assert(fs.existsSync(applyVuePath), 'apply.vue 存在'); +const applyVueContent = fs.readFileSync(applyVuePath, 'utf-8'); + +assert( + applyVueContent.includes(':modelValue="formData"') && !applyVueContent.includes('v-model="formData"'), + 'apply.vue 使用 :modelValue="formData" 消除 const 重赋值编译错误', + '仍包含 v-model="formData"' +); + +assert( + applyVueContent.includes('let formData = reactive({'), + 'apply.vue 中 formData 使用 let 声明提供双重防护', + 'formData 不是 let 声明' +); + +// 11. bindUserPhoneNumber 弹性支持与自动更新 +assert( + miniProgramJsContent.includes('bindUserPhoneNumber') && miniProgramJsContent.includes("sheep.$store('user').getInfo()"), + 'miniProgram.js bindUserPhoneNumber 成功后自动调用 getInfo() 刷新用户信息', + 'bindUserPhoneNumber 未自动刷新用户信息' +); + +// 12. 个人中心绑定横条受 showBindMobile 精确控制(微信小程序环境不冗余展示) +assert( + sUserCardContent.includes('v-if="showBindMobile"') && sUserCardContent.includes('#ifdef MP-WEIXIN'), + 's-user-card.vue 使用 showBindMobile 精确控制,微信小程序端不显示冗余绑定提示条', + '未配置 showBindMobile 精确控制' +); + +console.log("\n--------------------------------------------------------"); +console.log(`测试执行结果: 通过 ${passed} 项, 失败 ${failed} 项`); +console.log("========================================================"); + +process.exit(failed === 0 ? 0 : 1); diff --git a/uniapp/manifest.json b/uniapp/manifest.json index 747e3a2..28756f6 100644 --- a/uniapp/manifest.json +++ b/uniapp/manifest.json @@ -1,5 +1,5 @@ { - "name": "Shopro", + "name": "恒链数通", "appid": "__UNI__082C0BA", "description": "Shopro是由SheepJS团队开发,使用Uniapp+Vue3技术驱动的在线商城系统,内含诸多功能与丰富的活动,期待您的使用和反馈。", "versionName": "3.2.0", diff --git a/uniapp/package-lock.json b/uniapp/package-lock.json new file mode 100644 index 0000000..0991dcd --- /dev/null +++ b/uniapp/package-lock.json @@ -0,0 +1,465 @@ +{ + "name": "shopro", + "version": "3.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "shopro", + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "@hyoga/uni-socket.io": "^1.0.1", + "dayjs": "^1.11.9", + "lodash": "^4.17.21", + "luch-request": "^3.1.0", + "pinia": "2.1.4", + "pinia-plugin-persist-uni": "1.2.0", + "qs-canvas": "^1.0.11", + "weixin-js-sdk": "^1.6.0" + }, + "devDependencies": { + "prettier": "^3.0.0", + "vconsole": "^3.15.1" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@dcloudio/types": { + "version": "2.6.12", + "resolved": "https://registry.npmmirror.com/@dcloudio/types/-/types-2.6.12.tgz", + "integrity": "sha512-mrCMwcINy1IFjU9VUqLeWBkj404yWs5paLDttBcA+eqUjanuUQbBcTVPqlrGgkyzLXDcV2oDDZRSNxNpXi4kMQ==", + "license": "Apache-2.0" + }, + "node_modules/@hyoga/uni-socket.io": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@hyoga/uni-socket.io/-/uni-socket.io-1.0.1.tgz", + "integrity": "sha512-+a3PvweyFHx39IJfJWoO9wYsvv50XCIslJ23OiOs8/jMdCZud7c4uWoZF5gH2iskcX72JcDm1Gk5g5o8X7dxEg==", + "license": "MIT", + "dependencies": { + "socket.io-client": "^2.1.1" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/after": { + "version": "0.8.2", + "resolved": "https://registry.npmmirror.com/after/-/after-0.8.2.tgz", + "integrity": "sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA==", + "license": "MIT" + }, + "node_modules/arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmmirror.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==", + "license": "MIT" + }, + "node_modules/backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==", + "license": "MIT" + }, + "node_modules/base64-arraybuffer": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", + "integrity": "sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/blob": { + "version": "0.0.5", + "resolved": "https://registry.npmmirror.com/blob/-/blob-0.0.5.tgz", + "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==", + "license": "MIT" + }, + "node_modules/component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha512-WZveuKPeKAG9qY+FkYDeADzdHyTYdIboXS59ixDeRJL5ZhxpqUnxSOwop4FQjMsiYm3/Or8cegVbpAHNA7pHxw==" + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmmirror.com/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha512-w+LhYREhatpVqTESyGFg3NlP6Iu0kEKUHETY9GoZP/pQyW4mHFZuFWRUCIqVPZ36ueVLtoOEZaAqbCF2RDndaA==" + }, + "node_modules/copy-text-to-clipboard": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.2.tgz", + "integrity": "sha512-T6SqyLd1iLuqPA90J5N4cTalrtovCySh58iiZDGJ6FGznbclKh4UI+FGacQSgFzwKG77W7XT5gwbVEbd9cIH1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/core-js": { + "version": "3.50.0", + "resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.50.0.tgz", + "integrity": "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/dayjs": { + "version": "1.11.23", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.23.tgz", + "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/engine.io-client": { + "version": "3.5.6", + "resolved": "https://registry.npmmirror.com/engine.io-client/-/engine.io-client-3.5.6.tgz", + "integrity": "sha512-2fDMKiXSU7bGRDCWEw9cHEdRNfoU8cpP6lt+nwJhv72tSJpO7YBsqMqYZ63eVvwX3l9prPl2k/mxhfVhY+SDWg==", + "license": "MIT", + "dependencies": { + "component-emitter": "~1.3.0", + "component-inherit": "0.0.3", + "debug": "~3.1.0", + "engine.io-parser": "~2.2.0", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.6", + "parseuri": "0.0.6", + "ws": "~7.5.10", + "xmlhttprequest-ssl": "~1.6.2", + "yeast": "0.1.2" + } + }, + "node_modules/engine.io-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/engine.io-parser/-/engine.io-parser-2.2.1.tgz", + "integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==", + "license": "MIT", + "dependencies": { + "after": "0.8.2", + "arraybuffer.slice": "~0.0.7", + "base64-arraybuffer": "0.1.4", + "blob": "0.0.5", + "has-binary2": "~1.0.2" + } + }, + "node_modules/has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "license": "MIT", + "dependencies": { + "isarray": "2.0.1" + } + }, + "node_modules/has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA==", + "license": "MIT" + }, + "node_modules/indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==" + }, + "node_modules/isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha512-c2cu3UxbI+b6kR3fy0nRnAhodsvR9dx7U5+znCOzdj6IfP3upFURTr0Xl5BlQZNKZjEtxrmVyfSdeE3O57smoQ==", + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/luch-request": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/luch-request/-/luch-request-3.1.1.tgz", + "integrity": "sha512-p7+mlcEtgRcd0OfXC4XZbyiwSr1XgCeqNT7LlVUjnk7InYl/8d5Rk7BUqAYNA2WRafI1wRIUQWRWZRpeUwWR0w==", + "license": "MIT", + "dependencies": { + "@dcloudio/types": "^2.0.16" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/mutation-observer": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/mutation-observer/-/mutation-observer-1.0.3.tgz", + "integrity": "sha512-M/O/4rF2h776hV7qGMZUH3utZLO/jK7p8rnNgGkjKUw8zCGjRQPxB8z6+5l8+VjRUQ3dNYu4vjqXYLr+U8ZVNA==", + "dev": true + }, + "node_modules/parseqs": { + "version": "0.0.6", + "resolved": "https://registry.npmmirror.com/parseqs/-/parseqs-0.0.6.tgz", + "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==", + "license": "MIT" + }, + "node_modules/parseuri": { + "version": "0.0.6", + "resolved": "https://registry.npmmirror.com/parseuri/-/parseuri-0.0.6.tgz", + "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==", + "license": "MIT" + }, + "node_modules/pinia": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.1.4.tgz", + "integrity": "sha512-vYlnDu+Y/FXxv1ABo1vhjC+IbqvzUdiUC3sfDRrRyY2CQSrqqaa+iiHmqtARFxJVqWQMCJfXx1PBvFs9aJVLXQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/devtools-api": "^6.5.0", + "vue-demi": ">=0.14.5" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@vue/composition-api": "^1.4.0", + "typescript": ">=4.4.4", + "vue": "^2.6.14 || ^3.3.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/pinia-plugin-persist-uni": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/pinia-plugin-persist-uni/-/pinia-plugin-persist-uni-1.2.0.tgz", + "integrity": "sha512-9ZLdeB+nfeu0WD7WEv19E1u2cWbnK/J8fDR5w56JPGoQtbU4Uy8VLKSvfHlbuVzGxBaE1DhG4/ulWVm3oAArpg==", + "dependencies": { + "vue-demi": "^0.12.1" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0", + "pinia": "^2.0.0", + "vue": "^2.0.0 || >=3.0.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/pinia-plugin-persist-uni/node_modules/vue-demi": { + "version": "0.12.5", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.12.5.tgz", + "integrity": "sha512-BREuTgTYlUr0zw0EZn3hnhC3I6gPWv+Kwh4MCih6QcAeaTlaIX0DwOVN0wHej7hSvDPecz4jygy/idsgKfW58Q==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/prettier": { + "version": "3.9.7", + "resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.9.7.tgz", + "integrity": "sha512-T3mF5P7HFzZVLDQuCUNtJj6WK1pcpLMweMNUVwGb01bLNkH9AkKxtZvmInw8K6bnn2O3d6/5RHl6zSHiBAD0Og==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/qs-canvas": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/qs-canvas/-/qs-canvas-1.0.11.tgz", + "integrity": "sha512-WJwLwrAA1++ZqT6eR2F4Qa42yowybDUtQ2TDPtDOwhD1i4DzEOotug5N20cDkHat9l69ThOWoxV4qJbLKAjkNg==", + "license": "BSD 3-Clause" + }, + "node_modules/socket.io-client": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/socket.io-client/-/socket.io-client-2.5.0.tgz", + "integrity": "sha512-lOO9clmdgssDykiOmVQQitwBAF3I6mYcQAo7hQ7AM6Ny5X7fp8hIJ3HcQs3Rjz4SoggoxA1OgrQyY8EgTbcPYw==", + "license": "MIT", + "dependencies": { + "backo2": "1.0.2", + "component-bind": "1.0.0", + "component-emitter": "~1.3.0", + "debug": "~3.1.0", + "engine.io-client": "~3.5.0", + "has-binary2": "~1.0.2", + "indexof": "0.0.1", + "parseqs": "0.0.6", + "parseuri": "0.0.6", + "socket.io-parser": "~3.3.0", + "to-array": "0.1.4" + } + }, + "node_modules/socket.io-parser": { + "version": "3.3.6", + "resolved": "https://registry.npmmirror.com/socket.io-parser/-/socket.io-parser-3.3.6.tgz", + "integrity": "sha512-+VwteZF0qtYVkcO1nhKf6ZUVz5wq6Ya+Lx10y3Y81Sp5+iyGGY2GTcd81W36C/r1tjP+GhXjiXaLKlCMyE+yHQ==", + "license": "MIT", + "dependencies": { + "component-emitter": "~1.3.0", + "debug": "~3.1.0", + "isarray": "2.0.1" + } + }, + "node_modules/to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha512-LhVdShQD/4Mk4zXNroIQZJC+Ap3zgLcDuwEdcmLv9CCO73NWockQDwyUnW/m8VX/EElfL6FcYx7EeutN4HJA6A==" + }, + "node_modules/vconsole": { + "version": "3.15.1", + "resolved": "https://registry.npmmirror.com/vconsole/-/vconsole-3.15.1.tgz", + "integrity": "sha512-KH8XLdrq9T5YHJO/ixrjivHfmF2PC2CdVoK6RWZB4yftMykYIaXY1mxZYAic70vADM54kpMQF+dYmvl5NRNy1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.17.2", + "copy-text-to-clipboard": "^3.0.1", + "core-js": "^3.11.0", + "mutation-observer": "^1.0.3" + } + }, + "node_modules/vue": { + "version": "2.6.14", + "resolved": "https://registry.npmmirror.com/vue/-/vue-2.6.14.tgz", + "integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==", + "deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.", + "license": "MIT", + "peer": true + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/weixin-js-sdk": { + "version": "1.6.5", + "resolved": "https://registry.npmmirror.com/weixin-js-sdk/-/weixin-js-sdk-1.6.5.tgz", + "integrity": "sha512-Gph1WAWB2YN/lMOFB/ymb+hbU/wYazzJgu6PMMktCy9cSCeW5wA6Zwt0dpahJbJ+RJEwtTv2x9iIu0U4enuVSQ==", + "license": "MIT" + }, + "node_modules/ws": { + "version": "7.5.13", + "resolved": "https://registry.npmmirror.com/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "1.6.3", + "resolved": "https://registry.npmmirror.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz", + "integrity": "sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg==", + "license": "MIT" + } + } +} diff --git a/uniapp/pages/app/score-shop.vue b/uniapp/pages/app/score-shop.vue index 345d570..3e05b83 100644 --- a/uniapp/pages/app/score-shop.vue +++ b/uniapp/pages/app/score-shop.vue @@ -1,5 +1,5 @@
商品规格 是否参与商品价格商品原价 可兑换数量兑换积分兑换价格所需兑换券(元)所需积分
{{ sp.price }} + placeholder="如 100" + v-model="goods.score_sku_prices[spindex].price"> + placeholder="无积分填0" + v-model="goods.score_sku_prices[spindex].score">