feat(commission): 支持计奖商品参与设置、独立分成与多规格规则,完善团队中心与微信快捷登录

这个提交包含在:
chen
2026-09-17 19:27:17 +08:00
父节点 3c7df9688b
当前提交 829e88cfa9
共有 54 个文件被更改,包括 3536 次插入 和 666 次删除

查看文件

@@ -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,
];
}

查看文件

@@ -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,
]);
}

查看文件

@@ -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('登陆失败');
}

查看文件

@@ -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);

查看文件

@@ -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;

查看文件

@@ -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 = [];

查看文件

@@ -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;

查看文件

@@ -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;
}
}

查看文件

@@ -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;
});

查看文件

@@ -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, '酒道馆');

查看文件

@@ -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];

查看文件

@@ -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);

查看文件

@@ -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);

查看文件

@@ -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', '管理员操作自动货到付款支付成功');
// 在控制器执行后续内容,这里不再处理

查看文件

@@ -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'])) {

查看文件

@@ -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 ?? '',

查看文件

@@ -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']);

查看文件

@@ -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++;
}

查看文件

@@ -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 '免费兑换';
}

查看文件

@@ -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']);

查看文件

@@ -56,17 +56,25 @@
<el-container class="panel-block">
<el-main>
<el-scrollbar height="100%">
<div class="title mb-4">商品名称:{{ state.title }}</div>
<div class="title mb-3">商品名称:{{ state.title }}</div>
<el-alert
title="💡 兑换规则配置说明"
type="info"
:closable="false"
class="mb-4"
description="1. 纯兑换券兑换:【所需兑换券】填入所需面额(如 100),【所需积分】填 0。用户在小程序下单时将自动使用可用兑换券 1:1 全额抵扣(实付 0 元)。&#10;2. 兑换券+积分组合:同时填写对应所需兑换券额度与所需积分。&#10;3. 若用户可用兑换券不足,系统自动支持抵扣部分可用券额,剩余差额可使用微信现金支付补齐。"
show-icon>
</el-alert>
<div class="sku-table-wrap">
<table class="sku-table" rules="none">
<thead>
<tr>
<th class="sku-item sku-col">商品规格</th>
<th class="sku-item">是否参与</th>
<th class="sku-item">商品价格</th>
<th class="sku-item">商品原价</th>
<th class="sku-item">可兑换数量</th>
<th class="sku-item">兑换积分</th>
<th class="sku-item">兑换价格</th>
<th class="sku-item">所需兑换券(元)</th>
<th class="sku-item">所需积分</th>
</tr>
</thead>
<tbody>
@@ -79,15 +87,18 @@
<td class="sku-item">{{ sp.price }}</td>
<th class="sku-item">
<el-input v-if="goods.score_sku_prices[spindex].status == 'up'" type="number"
placeholder="库存数量"
v-model="goods.score_sku_prices[spindex].stock"></el-input>
</th>
<th class="sku-item">
<el-input v-if="goods.score_sku_prices[spindex].status == 'up'" type="number"
v-model="goods.score_sku_prices[spindex].score"></el-input>
placeholder="如 100"
v-model="goods.score_sku_prices[spindex].price"></el-input>
</th>
<th class="sku-item">
<el-input v-if="goods.score_sku_prices[spindex].status == 'up'" type="number"
v-model="goods.score_sku_prices[spindex].price"></el-input>
placeholder="无积分填0"
v-model="goods.score_sku_prices[spindex].score"></el-input>
</th>
</tr>
</tbody>

查看文件

@@ -56,7 +56,7 @@
<el-header class="sa-header">
<div class="sa-title sa-flex sa-row-between">
<div class="sa-title-left">
<div class="left-name">积分商城</div>
<div class="left-name">兑换商城 (兑换券/积分)</div>
<sa-filter-condition v-model="state.filter" @filter-delete="onChangeFilter">
</sa-filter-condition>
</div>
@@ -121,12 +121,19 @@
</div>
</template>
</el-table-column>
<el-table-column label="积分现金" min-width="200">
<el-table-column label="兑换门槛 (兑换券/积分)" min-width="220">
<template #default="scope">
<div v-if="scope.row.score_price" class="sa-flex">
{{ scope.row.score_price.score }}积分
<div v-if="Number(scope.row.score_price.price)">+¥{{ scope.row.score_price.price }}
</div>
<span v-if="Number(scope.row.score_price.price)" style="color: #e6a23c; font-weight: 600;">
¥{{ scope.row.score_price.price }} 兑换券
</span>
<span v-if="Number(scope.row.score_price.price) && Number(scope.row.score_price.score)" class="mx-1">+</span>
<span v-if="Number(scope.row.score_price.score)" style="color: #409eff; font-weight: 600;">
{{ scope.row.score_price.score }}积分
</span>
<span v-if="!Number(scope.row.score_price.price) && !Number(scope.row.score_price.score)" style="color: #67c23a;">
免费兑换
</span>
</div>
<div v-else>-</div>
</template>

查看文件

@@ -3,94 +3,165 @@
<style>
.goods-form .sa-title.is-line {
margin-bottom: 16px;
font-weight: 600;
font-size: 15px;
}
.goods-form .goods-item {
margin-bottom: 16px;
.goods-form .goods-card {
background: #f8f9fa;
border-radius: 8px;
padding: 14px 16px;
margin-bottom: 20px;
border: 1px solid var(--sa-border);
}
.goods-form .goods-item .goods-title {
height: 16px;
line-height: 16px;
font-size: 14px;
font-weight: 500;
height: 20px;
line-height: 20px;
font-size: 15px;
font-weight: 600;
color: var(--sa-font);
margin-bottom: 8px;
margin-bottom: 6px;
}
.goods-form .goods-item .goods-price {
font-size: 14px;
color: #f56c6c;
font-weight: 600;
margin-bottom: 6px;
}
.goods-form .goods-item .goods-sku {
width: fit-content;
height: 18px;
line-height: 18px;
height: 20px;
line-height: 20px;
background: var(--el-color-primary);
border-radius: 10px;
padding: 0 8px;
padding: 0 10px;
font-size: 12px;
color: var(--sa-background-assist);
}
.goods-form .commission-table {
overflow: hidden;
overflow-x: auto;
.goods-form .section-box {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 16px;
margin-bottom: 18px;
}
.goods-form .commission-table .commission-header {
border-left: 1px solid var(--sa-border);
}
.goods-form .commission-table .commission-header .col-item {
padding: 0 12px;
height: 40px;
line-height: 40px;
background: var(--sa-table-header-bg);
border-top: 1px solid var(--sa-border);
}
.goods-form .commission-table .commission-content {
border-left: 1px solid var(--sa-border);
}
.goods-form .commission-table .col-item {
width: 150px;
min-width: 150px;
padding: 0 12px;
height: 48px;
line-height: 48px;
.goods-form .section-header {
display: flex;
align-items: center;
justify-content: center;
border-right: 1px solid var(--sa-border);
border-bottom: 1px solid var(--sa-border);
justify-content: space-between;
margin-bottom: 14px;
padding-bottom: 8px;
border-bottom: 1px dashed #ebeef5;
}
.goods-form .commission-table .col-item .el-input {
margin-right: 12px;
.goods-form .section-title {
font-size: 14px;
font-weight: 600;
color: #303133;
display: flex;
align-items: center;
}
.goods-form .commission-table .col-item .el-input:last-of-type {
margin-right: 0;
.goods-form .section-title::before {
content: '';
display: inline-block;
width: 4px;
height: 14px;
background: var(--el-color-primary);
border-radius: 2px;
margin-right: 8px;
}
.goods-form .commission-table .col-item.commission-item, .goods-form .commission-table .col-item.sku-col {
width: 240px;
min-width: 240px;
.goods-form .section-tip {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
.goods-form .batch-edit {
color: var(--el-color-primary);
margin-left: 8px;
.goods-form .rules-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
}
.commission-popover .commission-item {
margin-bottom: 12px;
.goods-form .rules-grid-2 {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 14px;
}
.commission-popover .commission-item .el-input {
margin-right: 12px;
.goods-form .rules-grid-6 {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.commission-popover .commission-item .el-input:last-of-type {
margin-right: 0;
.goods-form .field-item {
background: #fafafa;
padding: 10px 12px;
border-radius: 6px;
border: 1px solid #f0f0f0;
}
.goods-form .field-label {
font-size: 12px;
color: #606266;
margin-bottom: 6px;
font-weight: 500;
}
.goods-form .preview-summary {
background: #f0f9eb;
border: 1px solid #e1f3d8;
border-radius: 8px;
padding: 16px;
margin-top: 10px;
}
.goods-form .preview-summary .summary-title {
font-weight: 600;
font-size: 14px;
color: #67c23a;
margin-bottom: 10px;
}
.goods-form .preview-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
font-size: 13px;
color: #555;
}
.goods-form .preview-item {
background: #fff;
padding: 8px 12px;
border-radius: 4px;
border: 1px solid #e1f3d8;
}
.goods-form .sku-toolbar {
background: #f4f6fa;
padding: 10px 14px;
border-radius: 6px;
margin-bottom: 16px;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
}
.goods-form .sku-tab-wrap {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
</style>
@@ -98,256 +169,311 @@
<el-container class="panel-block">
<el-main>
<el-scrollbar height="100%">
<div class="sa-title is-line">商品信息</div>
<div v-for="goods in state.data?.goods" :key="goods">
<div class="goods-item sa-flex sa-col-top">
<sa-image class="mr-2" :url="goods.image" size="48"></sa-image>
<!-- 商品基本信息卡片 -->
<div class="goods-card">
<div v-for="goods in state.data?.goods" :key="goods.id" class="goods-item sa-flex sa-col-top">
<sa-image class="mr-3" :url="goods.image" size="64"></sa-image>
<div>
<div class="goods-title sa-table-line-1">
{{ goods.title }}
</div>
<div v-if="goods.is_sku" class="goods-sku">
{{ goods.is_sku ? '多规格' : '' }}
<div class="goods-title sa-table-line-1">{{ goods.title }}</div>
<div class="goods-price" v-if="goods.price">
指导价:¥{{ Array.isArray(goods.price) ? goods.price.join(' ~ ') : goods.price }}
</div>
<div v-if="goods.is_sku" class="goods-sku">多规格商品(共 {{ goods.sku_prices?.length || 0 }} 种规格)</div>
<div v-else class="goods-sku" style="background:#67c23a;">单规格基础商品</div>
</div>
</div>
</div>
<div class="sa-title is-line">佣金设置</div>
<el-form label-width="120px">
<el-form-item label="是否参与">
<el-radio-group v-model="state.commission_goods.status">
<el-radio :label="0">不参与</el-radio>
<el-radio :label="1">参与</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="分销商业绩">
<el-radio-group v-model="state.commission_goods.commission_order_status">
<el-radio :label="0">不计入</el-radio>
<el-radio :label="1">
<span class="sa-flex">
计入
<el-popover placement="top" :width="310" content="关闭则只分佣,不计入分销订单金额和订单数">
<template #reference>
<el-icon class="warning sa-m-l-8">
<warning />
</el-icon>
</template>
</el-popover>
</span>
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="佣金规则">
<el-radio-group v-model="state.commission_goods.self_rules" @change="onChangeSelfRules">
<el-radio :label="0">默认规则</el-radio>
<el-radio :label="1" v-if="state.rulesType != 'batch'">独立规则</el-radio>
<el-radio :label="2">批量规则</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="state.commission_goods.self_rules == 1 || state.commission_goods.self_rules == 2"
label="分销设置">
<el-switch v-model="state.commission_config_temp.status"
@change="onChangeCommissionConfigStatus" :active-value="1" :inactive-value="0" />
<span class="sa-m-l-8">
{{ state.commission_config_temp.status ? '自定义' : '默认' }}
</span>
</el-form-item>
<el-form-item label="分销层级">
<el-radio-group v-model="state.commission_config_temp.level"
:disabled="state.commission_config_temp.status == 0">
<el-radio :label="1">一级</el-radio>
<el-radio :label="2">二级</el-radio>
<el-radio :label="3">三级</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="分销自购">
<div>
<el-radio-group v-model="state.commission_config_temp.self_buy"
:disabled="state.commission_config_temp.status == 0">
<el-radio :label="0">关闭</el-radio>
<el-radio :label="1">开启</el-radio>
</el-radio-group>
<div class="tip"> 分销自购开启后,分销商自己购买时,下单可以给自己返佣 </div>
<!-- 计奖与分成核心设置 -->
<el-form label-width="120px" label-position="left">
<div class="section-box">
<div class="section-header">
<div class="section-title">计奖资格与业绩设置</div>
</div>
</el-form-item>
<el-form-item label="商品结算方式">
<div>
<el-radio-group v-model="state.commission_config_temp.reward_type"
:disabled="state.commission_config_temp.status == 0">
<el-radio label="goods_price">商品价</el-radio>
<el-radio label="pay_price">实际支付价</el-radio>
<el-form-item label="是否参与计奖">
<el-radio-group v-model="state.commission_goods.status">
<el-radio :label="1"><el-tag type="success" size="small">参与计奖</el-tag></el-radio>
<el-radio :label="0"><el-tag type="danger" size="small">不参与计奖</el-tag></el-radio>
</el-radio-group>
<div class="tip">
商品价: 商品实际售价/规格价,实际支付价:实际支付的费用(不含运费)
<div class="section-tip">
开启后,购买该商品的新订单将参与收益中心推三返一、创客极差、团队拆分、兑换券及全网日结奖池;关闭后整单或单品排除计奖。
</div>
</div>
</el-form-item>
<el-form-item label="佣金结算方式">
<el-radio-group v-model="state.commission_config_temp.reward_event"
:disabled="state.commission_config_temp.status == 0">
<el-radio label="paid">支付后结算</el-radio>
<el-radio label="confirm">确认收货结算</el-radio>
<el-radio label="finish">订单完成结算</el-radio>
<el-radio label="admin">手动打款</el-radio>
</el-radio-group>
</el-form-item>
<div class="commission-table">
<template v-if="state.commission_goods.self_rules == 0">
<div class="commission-header sa-flex">
<div class="col-item">分销等级名称</div>
<template v-for="commission in state.commission_config_temp.level" :key="commission">
<div v-if="commission == 1" class="col-item commission-item">
一级(自购)佣金比例
</div>
<div v-if="commission == 2" class="col-item commission-item"> 二级佣金比例 </div>
<div v-if="commission == 3" class="col-item commission-item"> 三级佣金比例 </div>
</template>
</div>
<div class="commission-content sa-flex" v-for="level in state.levelData" :key="level">
<div class="col-item">{{ level.name }}</div>
<template v-for="commission in state.commission_config_temp.level" :key="commission">
<div class="col-item commission-item">
{{ level.commission_rules[`commission_${commission}`] }}%
</div>
</template>
</div>
</template>
<template v-if="state.commission_goods.self_rules == 1">
<div class="commission-header sa-flex">
<div class="col-item sku-col">商品规格</div>
<div class="col-item">价格</div>
<div class="col-item">分销等级名称</div>
<template v-for="commission in state.commission_config_temp.level" :key="commission">
<div class="col-item commission-item">
<template v-if="commission == 1">一级(自购)佣金比例</template>
<template v-if="commission == 2">二级佣金比例</template>
<template v-if="commission == 3">三级佣金比例</template>
<el-popover popper-class="commission-popover"
v-model:visible="commissionPopover.flag[commission]" placement="top"
:width="220" trigger="click">
<div class="commission-item sa-flex">
<el-input v-model="commissionPopover.form.rate" type="number"
:disabled="commissionPopover.form.money != ''">
<template #append>%</template>
</el-input>
<el-input v-model="commissionPopover.form.money" type="number"
:disabled="commissionPopover.form.rate != ''">
<template #append>元</template>
</el-input>
</div>
<div class="sa-flex sa-row-right">
<el-button class="is-link" type="primary" size="small"
@click="onCancelCommissionPopover(commission)">取消</el-button>
<el-button type="primary" size="small"
@click="onConfirmCommissionPopover(commission)">确定</el-button>
</div>
<template #reference>
<el-icon class="batch-edit">
<edit />
</el-icon>
</template>
</el-popover>
</div>
</template>
</div>
<div class="commission-content sa-flex">
<div class="sa-flex sa-flex-col">
<div class="sa-flex" v-for="sku in state.detailData.sku_prices" :key="sku">
<div class="col-item sku-col" :style="{ height: 48 * state.levelData.length + 'px' }">
{{ state.detailData?.is_sku ? sku.goods_sku_text.join(',') : '默认规格' }}
</div>
<div class="col-item" :style="{ height: 48 * state.levelData.length + 'px' }">
{{ sku.price }}
</div>
<div>
<div class="col-item" v-for="level in state.levelData" :key="level">
{{ level.name }}
</div>
</div>
</div>
</el-form-item>
<template v-if="state.commission_goods.status == 1">
<el-form-item label="分销商业绩">
<el-radio-group v-model="state.commission_goods.commission_order_status">
<el-radio :label="1">计入业绩</el-radio>
<el-radio :label="0">不计入业绩</el-radio>
</el-radio-group>
<div class="section-tip">
计入后,该订单将累加分销商团队订单金额和单数,用于创客升级考核。
</div>
<div>
<div v-for="sku in state.commission_goods.commission_rules" :key="sku">
<div class="sa-flex" v-for="level in sku" :key="level">
<template v-for="commission in state.commission_config_temp.level"
:key="commission">
<div v-if="level[commission]" class="col-item commission-item">
<el-input v-model="level[commission].rate" type="number"
:disabled="level[commission].money != ''">
<template #append>%</template>
</el-input>
<el-input v-model="level[commission].money" type="number"
:disabled="level[commission].rate != ''">
<template #append>元</template>
</el-input>
</div>
</template>
</div>
</div>
</el-form-item>
<el-form-item label="分成规则模式">
<el-radio-group v-model="state.commission_goods.self_rules" @change="onChangeSelfRules">
<el-radio :label="0">默认规则(跟随系统配置)</el-radio>
<el-radio :label="1" v-if="state.rulesType != 'batch'">独立规则(单独设置分成)</el-radio>
<el-radio :label="2">批量规则(统一设置分成)</el-radio>
</el-radio-group>
<div class="section-tip">
选择「默认规则」时直接继承系统收益中心全局配置;选择「独立规则」可为当前商品单独定制分成比例与金额。
</div>
</div>
</template>
<template v-if="state.commission_goods.self_rules == 2">
<div class="commission-header sa-flex">
<div class="col-item">分销等级名称</div>
<template v-for="commission in state.commission_config_temp.level" :key="commission">
<div class="col-item commission-item">
<template v-if="commission == 1">一级(自购)佣金比例</template>
<template v-if="commission == 2">二级佣金比例</template>
<template v-if="commission == 3">三级佣金比例</template>
<el-popover popper-class="commission-popover"
v-model:visible="commissionPopover.flag[commission]" placement="top"
:width="220" trigger="click">
<div class="commission-item sa-flex">
<el-input v-model="commissionPopover.form.rate" type="number"
:disabled="commissionPopover.form.money != ''">
<template #append>%</template>
</el-input>
</div>
<div class="sa-flex sa-row-right">
<el-button class="is-link" type="primary" size="small"
@click="onCancelCommissionPopover(commission)">取消</el-button>
<el-button size="small" type="primary"
@click="onConfirmCommissionPopover(commission)">确定</el-button>
</div>
<template #reference>
<el-icon class="batch-edit">
<edit />
</el-icon>
</template>
</el-popover>
</div>
</template>
</div>
<div class="commission-content sa-flex">
<div>
<div class="col-item" v-for="level in state.levelData" :key="level">
{{ level.name }}
</div>
</div>
<div>
<div class="sa-flex" v-for="level in state.commission_goods.commission_rules"
:key="level">
<template v-for="commission in state.commission_config_temp.level"
:key="commission">
<div v-if="level[commission]" class="col-item commission-item">
<el-input v-model="level[commission].rate" type="number"
:disabled="level[commission].money != ''">
<template #append>%</template>
</el-input>
</div>
</template>
</div>
</div>
</div>
</el-form-item>
</template>
</div>
<!-- 状态为不参与时的警示提示 -->
<el-alert v-if="state.commission_goods.status == 0" type="warning" show-icon :closable="false"
title="商品未参与计奖"
description="当前商品已关闭计奖,会员或下级购买该商品时将不产生推荐开拓奖、创客极差、酒道馆/区县团队奖及兑换券,亦不进入全网日结均分奖池。"
style="margin-bottom: 20px;">
</el-alert>
<!-- 模式 0:跟随系统默认规则预览卡片 -->
<div v-if="state.commission_goods.status == 1 && state.commission_goods.self_rules == 0" class="preview-summary">
<div class="summary-title sa-flex sa-row-between">
<span>系统收益中心全局默认分成规则概览</span>
<el-tag size="small" type="success">当前生效</el-tag>
</div>
<div class="preview-list">
<div class="preview-item">
<strong>推荐开拓奖(推三返一):</strong>
循环返现 ¥{{ state.defaultPartnerRules.push3_amounts?.[0] || 398 }} / ¥{{ state.defaultPartnerRules.push3_amounts?.[1] || 400 }} / ¥{{ state.defaultPartnerRules.push3_amounts?.[2] || 600 }} 元
</div>
<div class="preview-item">
<strong>创客极差基数(V1~V6):</strong>
¥{{ state.defaultPartnerRules.maker_bonus?.['2'] || 20 }} ~ ¥{{ state.defaultPartnerRules.maker_bonus?.['7'] || 120 }} 元
</div>
<div class="preview-item">
<strong>创客协同与超越:</strong>
同级协同 {{ ((state.defaultPartnerRules.maker_peer_rate || 0.10) * 100).toFixed(0) }}%,超越关怀 {{ ((state.defaultPartnerRules.maker_overtaken_rate || 0.05) * 100).toFixed(0) }}%
</div>
<div class="preview-item">
<strong>渠道运营统筹奖:</strong>
酒道馆 ¥{{ state.defaultPartnerRules.venue_split || 50 }},区县统筹 ¥{{ state.defaultPartnerRules.county_full || 100 }}(有馆时各50)
</div>
<div class="preview-item">
<strong>馆/县平级协同奖:</strong>
酒道馆 {{ ((state.defaultPartnerRules.venue_peer_rate || 0.10) * 100).toFixed(0) }}%,区县代理 {{ ((state.defaultPartnerRules.county_peer_rate || 0.10) * 100).toFixed(0) }}%
</div>
<div class="preview-item">
<strong>消费补贴兑换券:</strong>
实付 {{ state.defaultPartnerRules.voucher_times || 1.5 }} 倍,分 {{ state.defaultPartnerRules.voucher_days || 365 }} 天线性释放
</div>
</div>
</div>
<!-- 模式 1 / 2:独立设置分成规则面板 -->
<template v-if="state.commission_goods.status == 1 && (state.commission_goods.self_rules == 1 || state.commission_goods.self_rules == 2)">
<!-- 多规格切换与快捷操作栏 -->
<div class="sku-toolbar">
<div class="sa-flex" v-if="state.detailData?.is_sku == 1 && state.rulesType != 'batch'">
<span class="mr-2" style="font-size: 13px; font-weight: 500;">规格配置模式:</span>
<el-radio-group v-model="state.skuMode" size="small">
<el-radio-button label="unified">统一设置所有规格</el-radio-button>
<el-radio-button label="sku">按规格分别设置</el-radio-button>
</el-radio-group>
</div>
<div class="sa-flex" v-else>
<el-tag size="small" type="info">商品独立分成模式</el-tag>
</div>
<div class="sa-flex">
<el-button size="small" type="primary" plain @click="onFillDefaults(state.skuMode === 'sku' ? 'current_sku' : 'all_skus')">
一键填入系统默认值
</el-button>
<el-button size="small" type="success" plain v-if="state.skuMode === 'sku' && state.detailData?.sku_prices?.length > 1" @click="onSyncToAllSkus">
同步当前规则至全部规格
</el-button>
</div>
</div>
<!-- 当按规格分别设置时的规格切换 Tabs -->
<div v-if="state.skuMode === 'sku' && state.detailData?.sku_prices?.length" style="margin-bottom: 16px;">
<el-radio-group v-model="state.activeSkuId" size="default">
<el-radio-button v-for="sku in state.detailData.sku_prices" :key="sku.id" :label="String(sku.id)">
{{ sku.goods_sku_text?.join(' / ') || '默认规格' }}(¥{{ sku.price }})
</el-radio-button>
</el-radio-group>
</div>
<!-- 当前激活的表单对象引用 -->
<template v-for="curForm in [(state.skuMode === 'sku' && state.activeSkuId) ? state.skuRules[state.activeSkuId] : state.ruleForm]">
<!-- 1. 推荐开拓奖(推三返一) -->
<div class="section-box">
<div class="section-header">
<div class="section-title">1. 推荐开拓奖(推三返一循环返现)</div>
<span class="section-tip">直推有效单奖励,每 3 笔订单形成一个循环返还货款</span>
</div>
<div class="rules-grid">
<div class="field-item">
<div class="field-label">第 1 单返现金额</div>
<el-input v-model="curForm.push3_1" type="number" :min="0">
<template #append>元</template>
</el-input>
</div>
<div class="field-item">
<div class="field-label">第 2 单返现金额</div>
<el-input v-model="curForm.push3_2" type="number" :min="0">
<template #append>元</template>
</el-input>
</div>
<div class="field-item">
<div class="field-label">第 3 单返现金额(波峰档)</div>
<el-input v-model="curForm.push3_3" type="number" :min="0">
<template #append>元</template>
</el-input>
</div>
</div>
</div>
<!-- 2. 创客进阶奖(极差基数)与协同奖 -->
<div class="section-box">
<div class="section-header">
<div class="section-title">2. 创客进阶奖(V1~V6极差基数)与协同奖</div>
<span class="section-tip">向上链条创客按自身基数减去已截留最高基数计提差额</span>
</div>
<div class="rules-grid-6" style="margin-bottom: 14px;">
<div class="field-item">
<div class="field-label">创客 V1 进阶基数</div>
<el-input v-model="curForm.maker_v1" type="number" :min="0">
<template #append>元</template>
</el-input>
</div>
<div class="field-item">
<div class="field-label">创客 V2 进阶基数</div>
<el-input v-model="curForm.maker_v2" type="number" :min="0">
<template #append>元</template>
</el-input>
</div>
<div class="field-item">
<div class="field-label">创客 V3 进阶基数</div>
<el-input v-model="curForm.maker_v3" type="number" :min="0">
<template #append>元</template>
</el-input>
</div>
<div class="field-item">
<div class="field-label">创客 V4 进阶基数</div>
<el-input v-model="curForm.maker_v4" type="number" :min="0">
<template #append>元</template>
</el-input>
</div>
<div class="field-item">
<div class="field-label">创客 V5 进阶基数</div>
<el-input v-model="curForm.maker_v5" type="number" :min="0">
<template #append>元</template>
</el-input>
</div>
<div class="field-item">
<div class="field-label">创客 V6 进阶基数</div>
<el-input v-model="curForm.maker_v6" type="number" :min="0">
<template #append>元</template>
</el-input>
</div>
</div>
<div class="rules-grid-2">
<div class="field-item">
<div class="field-label">创客同级平级协同比例</div>
<el-input v-model="curForm.maker_peer_rate" type="number" :min="0" :max="100">
<template #append>%</template>
</el-input>
</div>
<div class="field-item">
<div class="field-label">创客下级反超卓越比例</div>
<el-input v-model="curForm.maker_overtaken_rate" type="number" :min="0" :max="100">
<template #append>%</template>
</el-input>
</div>
</div>
</div>
<!-- 3. 实体渠道与区域运营奖(团队拆分) -->
<div class="section-box">
<div class="section-header">
<div class="section-title">3. 实体渠道与区域运营奖(团队拆分)</div>
<span class="section-tip">无馆有县独享,有馆有县各分其半</span>
</div>
<div class="rules-grid-2" style="margin-bottom: 14px;">
<div class="field-item">
<div class="field-label">酒道馆运营奖基准</div>
<el-input v-model="curForm.venue_split" type="number" :min="0">
<template #append>元</template>
</el-input>
</div>
<div class="field-item">
<div class="field-label">区县统筹全额(无馆时独享)</div>
<el-input v-model="curForm.county_full" type="number" :min="0">
<template #append>元</template>
</el-input>
</div>
</div>
<div class="rules-grid-2">
<div class="field-item">
<div class="field-label">酒道馆平级协同比例</div>
<el-input v-model="curForm.venue_peer_rate" type="number" :min="0" :max="100">
<template #append>%</template>
</el-input>
</div>
<div class="field-item">
<div class="field-label">区县代理平级协同比例</div>
<el-input v-model="curForm.county_peer_rate" type="number" :min="0" :max="100">
<template #append>%</template>
</el-input>
</div>
</div>
</div>
<!-- 4. 消费补贴兑换券计划 -->
<div class="section-box">
<div class="section-header">
<div class="section-title">4. 消费补贴兑换券计划</div>
<span class="section-tip">买家本人下单实付金额按倍数赠送并在商城按日释放抵扣</span>
</div>
<div class="rules-grid-2">
<div class="field-item">
<div class="field-label">赠送兑换券倍数(相对计奖实付)</div>
<el-input v-model="curForm.voucher_times" type="number" :min="0" step="0.1">
<template #append>倍</template>
</el-input>
</div>
<div class="field-item">
<div class="field-label">线性释放周期天数</div>
<el-input v-model="curForm.voucher_days" type="number" :min="1">
<template #append>天</template>
</el-input>
</div>
</div>
</div>
<!-- 5. 全平台日结奖池注入(可选定制) -->
<div class="section-box">
<div class="section-header">
<div class="section-title">5. 全平台日结均分奖池注入(可选定制)</div>
<span class="section-tip">每日凌晨 00:30 全网有效订单向池中注入并由活跃身份均分</span>
</div>
<div class="rules-grid-2">
<div class="field-item">
<div class="field-label">酒道馆日结奖池单笔注入</div>
<el-input v-model="curForm.venue_share" type="number" :min="0">
<template #append>元/单</template>
</el-input>
</div>
<div class="field-item">
<div class="field-label">区县代理日结奖池单笔注入</div>
<el-input v-model="curForm.county_share" type="number" :min="0">
<template #append>元/单</template>
</el-input>
</div>
</div>
</div>
</template>
</template>
</el-form>
</el-scrollbar>
</el-main>
<el-footer class="sa-footer--submit sa-flex sa-row-right">
<el-button type="primary" @click="onConfirm">确定</el-button>
<el-button type="primary" size="large" @click="onConfirm">保存分成设置</el-button>
</el-footer>
</el-container>
</div>

查看文件

@@ -66,14 +66,15 @@
<div>{{ scope.row.price?.join('~') || 0 }}</div>
</template>
</el-table-column>
<el-table-column label="分销规则" width="100">
<el-table-column label="分成规则" width="120">
<template #default="scope">
<template v-if="scope.row.commission_goods && scope.row.commission_goods.status == 1">
<template v-if="scope.row.commission_goods.self_rules == 0">默认规则</template>
<template v-if="scope.row.commission_goods.self_rules == 1">独立规则</template>
<template v-if="scope.row.commission_goods.self_rules == 2">批量规则</template>
<el-tag size="small" type="info" v-if="scope.row.commission_goods.self_rules == 0">默认规则</el-tag>
<el-tag size="small" type="success" v-else-if="scope.row.commission_goods.self_rules == 1">独立规则</el-tag>
<el-tag size="small" type="warning" v-else-if="scope.row.commission_goods.self_rules == 2">批量规则</el-tag>
<el-tag size="small" type="info" v-else>默认规则</el-tag>
</template>
<template v-else>-</template>
<el-tag size="small" type="danger" v-else>不参与计奖</el-tag>
</template>
</el-table-column>
<el-table-column label="商品状态" width="100">
@@ -83,15 +84,15 @@
</span>
</template>
</el-table-column>
<el-table-column label="操作" min-width="140" fixed="right">
<el-table-column label="操作" min-width="160" fixed="right">
<template #default="scope">
<div class="sa-flex">
<span class="mr-2"
:class="`sa-color--${state.goodsStatusStyle[scope.row.commission_goods?.status] || 'info'}`">
{{ scope.row.commission_goods?.status_text || '未参与' }}
</span>
<el-tag size="small" class="mr-2"
:type="scope.row.commission_goods?.status == 1 ? 'success' : 'danger'">
{{ scope.row.commission_goods?.status == 1 ? '参与中' : '未参与' }}
</el-tag>
{if $auth->check('shopro/commission/goods/edit')}
<el-button type="primary" link @click="onEdit(scope.row.id)">设置计奖</el-button>
<el-button type="primary" link @click="onEdit(scope.row.id)">设置分成</el-button>
{/if}
</div>
</template>
@@ -104,7 +105,7 @@
已选择 <span>{{batchHandle.data.length}}</span> 项</div>
<div class="sa-flex">
{if $auth->check('shopro/commission/goods/edit')}
<el-button type="primary" :disabled="!batchHandle.data.length" @click="onBatchHandle('edit')">设置佣金
<el-button type="primary" :disabled="!batchHandle.data.length" @click="onBatchHandle('edit')">批量设置分成
</el-button>
{/if}
</div>

查看文件

@@ -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 = {

查看文件

@@ -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 {

查看文件

@@ -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);
}
};

查看文件

@@ -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() {

查看文件

@@ -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);

查看文件

@@ -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);
}

查看文件

@@ -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. 结算抵扣算法模拟

查看文件

@@ -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);

查看文件

@@ -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);

查看文件

@@ -1,5 +1,5 @@
{
"name": "Shopro",
"name": "恒链数通",
"appid": "__UNI__082C0BA",
"description": "Shopro是由SheepJS团队开发,使用Uniapp+Vue3技术驱动的在线商城系统,内含诸多功能与丰富的活动,期待您的使用和反馈。",
"versionName": "3.2.0",

465
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"
}
}
}

查看文件

@@ -1,5 +1,5 @@
<template>
<s-layout title="积分商城">
<s-layout title="兑换商城">
<!-- 资产看板:当前积分与可用兑换券 -->
<view class="asset-banner ss-m-20 ss-p-20">
<view class="ss-flex ss-row-between ss-col-center">
@@ -17,7 +17,7 @@
</button>
</view>
<view class="asset-tip ss-m-t-16">
💡 兑换商品时支持使用兑换券 1:1 抵扣现金部分
💡 兑换商品时支持使用可用兑换券 1:1 全额抵扣现金
</view>
</view>
@@ -34,7 +34,7 @@
<s-empty
v-if="state.pagination.total === 0"
icon="/static/goods-empty.png"
text="暂无积分商品"
text="暂无兑换商品"
></s-empty>
<uni-load-more
v-if="state.pagination.total > 0"

查看文件

@@ -12,6 +12,28 @@
</view>
<!-- 账户信息 -->
<account-info />
<!-- 团队作战快捷看板 -->
<view class="team-quick-card ss-flex ss-row-between ss-col-center" @tap="sheep.$router.go('/pages/commission/team')">
<view class="team-quick-left ss-flex ss-col-center">
<view class="icon-wrap ss-flex ss-row-center ss-col-center">
<uni-icons type="staff" size="24" color="#ff6000"></uni-icons>
</view>
<view class="team-quick-info ss-m-l-16">
<view class="team-title ss-flex ss-col-center">
<text class="title-bold">团队作战中心</text>
<text class="active-badge ss-m-l-10" v-if="userInfo?.active_30d !== false && agentInfo?.active_30d !== false">正常活跃</text>
<text class="warn-badge ss-m-l-10" v-else>奖励暂停</text>
</view>
<view class="team-sub ss-m-t-6">
直推 {{ agentInfo?.child_user_count_1 || 0 }} 人 · 团队共 {{ agentInfo?.child_user_count_all || 0 }} 人
</view>
</view>
</view>
<view class="team-quick-right ss-flex ss-col-center">
<text class="manage-text">去管理</text>
<uni-icons type="right" size="14" color="#ff6000"></uni-icons>
</view>
</view>
<!-- 菜单栏 -->
<commission-menu />
<!-- 分销记录 -->
@@ -103,4 +125,62 @@
color: #ffffff;
}
}
.team-quick-card {
width: 694rpx;
margin: 20rpx auto 0;
padding: 22rpx 28rpx;
box-sizing: border-box;
background: linear-gradient(135deg, #ffffff 0%, #fffbf0 100%);
border-radius: 16rpx;
border: 1rpx solid #ffe8c2;
box-shadow: 0 4rpx 16rpx rgba(247, 213, 152, 0.25);
position: relative;
z-index: 3;
.icon-wrap {
width: 72rpx;
height: 72rpx;
border-radius: 36rpx;
background: #fff0e6;
}
.team-quick-info {
.title-bold {
font-size: 28rpx;
font-weight: bold;
color: #692e04;
}
.active-badge {
font-size: 20rpx;
padding: 2rpx 10rpx;
border-radius: 12rpx;
background: #ecfdf5;
color: #059669;
font-weight: 500;
}
.warn-badge {
font-size: 20rpx;
padding: 2rpx 10rpx;
border-radius: 12rpx;
background: #fef2f2;
color: #dc2626;
font-weight: 500;
}
.team-sub {
font-size: 22rpx;
color: #a17545;
font-family: OPPOSANS;
}
}
.team-quick-right {
.manage-text {
font-size: 24rpx;
font-weight: 500;
color: #ff6000;
margin-right: 4rpx;
}
}
}
</style>

查看文件

@@ -12,6 +12,19 @@
</view>
</view>
<!-- 穿透查看成员明细提示条 -->
<view v-if="state.buyerId > 0" class="buyer-filter-bar ss-flex ss-row-between ss-col-center">
<view class="ss-flex ss-col-center">
<uni-icons type="person-filled" size="16" color="#ff6000"></uni-icons>
<text class="filter-tip-text ss-m-l-10">
正在筛选成员:<text class="buyer-highlight">【{{ state.buyerName || ('ID:' + state.buyerId) }}】</text>的贡献
</text>
</view>
<button class="ss-reset-button clear-filter-btn" @tap="onClearBuyerFilter">
查看全部
</button>
</view>
<su-sticky>
<su-tabs
:list="tabMaps"
@@ -139,6 +152,8 @@
loadStatus: '',
showDetail: false,
current: null,
buyerId: 0,
buyerName: '',
overview: {
month_income: '0.00',
skip_count: 0,
@@ -179,11 +194,15 @@
async function getLogList(page = 1, list_rows = 10) {
state.loadStatus = 'loading';
try {
const res = await sheep.$api.partner.rewards({
tab: tabMaps[state.currentTab]?.value || 'all',
const params = {
tab: tabMaps.value[state.currentTab]?.value || 'all',
page,
list_rows,
});
};
if (state.buyerId > 0) {
params.buyer_id = state.buyerId;
}
const res = await sheep.$api.partner.rewards(params);
if (res?.code === 1) {
const pageData = pickList(res.data);
state.pagination = {
@@ -220,7 +239,20 @@
state.showDetail = true;
}
onLoad(() => {
function onClearBuyerFilter() {
state.buyerId = 0;
state.buyerName = '';
state.pagination = { ...emptyPagination };
getLogList(1);
}
onLoad((options) => {
if (options?.buyer_id) {
state.buyerId = Number(options.buyer_id) || 0;
}
if (options?.buyer_name) {
state.buyerName = decodeURIComponent(options.buyer_name);
}
getOverview();
getLogList();
});
@@ -239,6 +271,29 @@
</script>
<style lang="scss" scoped>
.buyer-filter-bar {
margin: 16rpx 24rpx;
padding: 16rpx 24rpx;
background: #fff4e5;
border-radius: 12rpx;
border: 1rpx solid #ffd8a8;
.filter-tip-text {
font-size: 24rpx;
color: #a17545;
}
.buyer-highlight {
font-weight: bold;
color: #ff6000;
}
.clear-filter-btn {
font-size: 22rpx;
padding: 6rpx 18rpx;
border-radius: 20rpx;
background: #ff6000;
color: #ffffff;
}
}
.header-box {
background-color: $white;
padding: 30rpx;

查看文件

@@ -22,6 +22,25 @@
<text class="referrer-name">{{ userInfo.parent_user.nickname }}</text>
</view>
<!-- 专属邀请码与拓展卡片 -->
<view class="invite-action-card ss-flex ss-row-between ss-col-center">
<view class="invite-code-wrap ss-flex ss-col-center">
<text class="invite-label">专属邀请码:</text>
<text class="invite-code">HL{{ userInfo.id || '---' }}</text>
<button class="ss-reset-button copy-code-btn ss-m-l-10" @tap="onCopyInviteCode">复制</button>
</view>
<view class="invite-btn-group ss-flex ss-col-center">
<button class="ss-reset-button invite-btn-poster ss-flex ss-col-center" @tap="onOpenShareModal">
<uni-icons type="image" size="14" color="#ffffff"></uni-icons>
<text class="ss-m-l-6">邀请海报</text>
</button>
<button class="ss-reset-button invite-btn-link ss-m-l-12 ss-flex ss-col-center" @tap="onCopyShareLink">
<uni-icons type="link" size="14" color="#ff6000"></uni-icons>
<text class="ss-m-l-6">复制链接</text>
</button>
</view>
</view>
<!-- 团队核心数据总览卡片 -->
<view class="team-data-box ss-flex ss-col-center ss-row-between">
<!-- 团队总人数卡片 -->
@@ -85,6 +104,20 @@
</view>
</view>
<!-- 团队收益与业绩统计大盘 -->
<view class="team-wealth-box ss-flex ss-col-center ss-row-between">
<view class="wealth-card ss-flex-1 ss-m-r-16">
<view class="wealth-title">团队贡献总收益(元)</view>
<view class="wealth-num text-orange">¥{{ state.summary.team_total_reward || '0.00' }}</view>
<view class="wealth-sub">本月贡献: ¥{{ state.summary.team_month_reward || '0.00' }}</view>
</view>
<view class="wealth-card ss-flex-1">
<view class="wealth-title">团队自购总业绩(元)</view>
<view class="wealth-num text-gold">¥{{ state.summary.team_total_consume || '0.00' }}</view>
<view class="wealth-sub">团队累计订单: {{ state.summary.team_order_count || 0 }} 笔</view>
</view>
</view>
<!-- 30天自购保活健康度胶囊条 -->
<view class="health-bar ss-flex ss-row-between ss-col-center">
<view class="health-title">保活健康度:</view>
@@ -206,6 +239,10 @@
{{ item.team_level === 1 ? '一级直推' : '二级间推' }}
</text>
</view>
<view class="member-sub-info ss-flex ss-col-center ss-m-t-4" v-if="item.mobile_hide || item.id">
<text class="member-phone" v-if="item.mobile_hide">{{ item.mobile_hide }}</text>
<text class="member-id-tag ss-m-l-8">ID: {{ item.id }}</text>
</view>
<view class="member-meta-row ss-flex ss-col-center ss-m-t-8">
<!-- 身份徽章 -->
<view v-if="memberLevelName(item)" class="tag-box ss-flex ss-col-center">
@@ -310,13 +347,20 @@
<text class="ss-m-l-6">拨打</text>
</button>
<button
v-if="item.mobile_raw"
v-if="item.mobile_raw || item.mobile_hide"
class="ss-reset-button action-btn btn-copy ss-m-l-12"
@tap.stop="onCopyText(item.mobile_raw, '手机号已复制')"
@tap.stop="onCopyText(item.mobile_raw || item.mobile_hide, '手机号已复制')"
>
<uni-icons type="paperclip" size="14" color="#666666"></uni-icons>
<text class="ss-m-l-6">复制手机</text>
</button>
<button
class="ss-reset-button action-btn btn-reward ss-m-l-12"
@tap.stop="onJumpRewards(item.id, item.nickname)"
>
<uni-icons type="wallet" size="14" color="#ff6000"></uni-icons>
<text class="ss-m-l-6">收益</text>
</button>
<button
class="ss-reset-button action-btn btn-detail ss-m-l-12"
@tap.stop="onOpenMemberDetail(item)"
@@ -387,6 +431,8 @@
<view class="drawer-sub ss-flex ss-col-center ss-m-t-8">
<text class="user-id">ID: {{ state.currentMember.id }}</text>
<text class="dot-split">·</text>
<text class="user-phone">手机: {{ state.currentMember.mobile_hide || '未绑定' }}</text>
<text class="dot-split">·</text>
<text class="join-time">{{ formatTime(state.currentMember.createtime) }} 加入</text>
</view>
</view>
@@ -496,16 +542,16 @@
<text class="ss-m-l-8">呼叫成员</text>
</button>
<button
v-if="state.currentMember.mobile_raw"
v-if="state.currentMember.mobile_raw || state.currentMember.mobile_hide"
class="ss-reset-button drawer-btn drawer-btn-sub ss-flex-1 ss-m-r-16"
@tap="onCopyText(state.currentMember.mobile_raw, '手机号已复制')"
@tap="onCopyText(state.currentMember.mobile_raw || state.currentMember.mobile_hide, '手机号已复制')"
>
<uni-icons type="paperclip" size="16" color="#ff6000"></uni-icons>
<text class="ss-m-l-8">复制电话</text>
</button>
<button
class="ss-reset-button drawer-btn drawer-btn-outline ss-flex-1"
@tap="onJumpRewards(state.currentMember.id)"
@tap="onJumpRewards(state.currentMember.id, state.currentMember.nickname)"
>
<uni-icons type="list" size="16" color="#666666"></uni-icons>
<text class="ss-m-l-8">查收益流水</text>
@@ -577,6 +623,10 @@
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,
},
pagination: {
data: [],
@@ -728,9 +778,32 @@
state.showDetail = true;
}
function onJumpRewards(buyerId) {
function onJumpRewards(buyerId, buyerName = '') {
state.showDetail = false;
sheep.$router.go(`/pages/commission/rewards?buyer_id=${buyerId}`);
const nameParam = buyerName ? `&buyer_name=${encodeURIComponent(buyerName)}` : '';
sheep.$router.go(`/pages/commission/rewards?buyer_id=${buyerId}${nameParam}`);
}
function onCopyInviteCode() {
if (!userInfo.value?.id) {
sheep.$helper.toast('请先登录');
return;
}
const code = `HL${userInfo.value.id}`;
sheep.$helper.copyText(code, '专属邀请码已复制');
}
function onCopyShareLink() {
if (!userInfo.value?.id) {
sheep.$helper.toast('请先登录');
return;
}
const shareQuery = sheep.$platform.share.buildSpmQuery({
page: '1',
query: '0',
});
const shareUrl = sheep.$platform.share.buildSpmPath(shareQuery);
sheep.$helper.copyText(shareUrl, '专属推广链接已复制');
}
function onOpenShareModal() {
@@ -825,6 +898,59 @@
}
}
// 专属邀请码卡片
.invite-action-card {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border-radius: 16rpx;
padding: 12rpx 20rpx;
margin-bottom: 20rpx;
.invite-code-wrap {
.invite-label {
font-size: 24rpx;
color: #ffffff;
font-weight: 500;
}
.invite-code {
font-size: 28rpx;
font-weight: bold;
color: #ffffff;
font-family: OPPOSANS;
letter-spacing: 1rpx;
}
.copy-code-btn {
font-size: 20rpx;
padding: 4rpx 14rpx;
border-radius: 16rpx;
background: #ffffff;
color: #ff6000;
font-weight: 500;
}
}
.invite-btn-group {
.invite-btn-poster {
height: 48rpx;
padding: 0 16rpx;
border-radius: 24rpx;
background: #ff6000;
color: #ffffff;
font-size: 22rpx;
font-weight: 500;
}
.invite-btn-link {
height: 48rpx;
padding: 0 16rpx;
border-radius: 24rpx;
background: #ffffff;
color: #ff6000;
font-size: 22rpx;
font-weight: 500;
}
}
}
// 团队信息总览
.team-data-box {
.data-card {
@@ -893,6 +1019,41 @@
color: #333333;
font-family: OPPOSANS;
}
// 团队收益与业绩看板
.team-wealth-box {
margin-top: 20rpx;
.wealth-card {
background: #ffffff;
border-radius: 20rpx;
padding: 18rpx 20rpx;
box-sizing: border-box;
.wealth-title {
font-size: 22rpx;
color: #999999;
line-height: 28rpx;
margin-bottom: 6rpx;
}
.wealth-num {
font-size: 32rpx;
font-weight: bold;
font-family: OPPOSANS;
line-height: 38rpx;
margin-bottom: 6rpx;
&.text-orange {
color: #ff6000;
}
&.text-gold {
color: #c48a3a;
}
}
.wealth-sub {
font-size: 20rpx;
color: #888888;
line-height: 24rpx;
}
}
}
@@ -1127,6 +1288,24 @@
}
}
.member-sub-info {
font-size: 20rpx;
color: #999999;
font-family: OPPOSANS;
.member-phone {
color: #666666;
font-size: 22rpx;
}
.member-id-tag {
background: #f3f4f6;
color: #9ca3af;
padding: 2rpx 8rpx;
border-radius: 6rpx;
font-size: 18rpx;
}
}
.member-meta-row {
gap: 8rpx;
}
@@ -1240,6 +1419,10 @@
background: #f3f4f6;
color: #4b5563;
}
&.btn-reward {
background: #fff7ed;
color: #ea580c;
}
&.btn-detail {
background: #ffffff;
color: #666666;

查看文件

@@ -33,13 +33,14 @@
<view v-if="goodsPrice.price > 0" class="price-text"> ¥{{ goodsPrice.price }} </view>
<text v-if="goodsPrice.price > 0 && goodsPrice.score > 0">+</text>
<image
v-if="goodsPrice.score > 0"
:src="sheep.$url.static('/assets/addons/shopro/uniapp/goods/score1.svg')"
class="score-img"
></image>
<view class="score-text ss-m-r-16">
<view v-if="goodsPrice.score > 0" class="score-text ss-m-r-16">
{{ goodsPrice.score }}
</view>
<view v-if="goodsPrice.price > 0" class="voucher-tip-tag">支持兑换券抵扣</view>
<view v-if="goodsPrice.price > 0" class="voucher-tip-tag">支持兑换券全额抵扣</view>
</view>
<view class="sales-text">
{{ formatExchange(state.goodsInfo.sales_show_type, state.goodsInfo.sales) }}

查看文件

@@ -5,7 +5,7 @@
navbar="custom"
tabbar="/pages/index/index"
:bgStyle="template.style?.background"
:navbarStyle="template.style?.navbar"
:navbarStyle="navbarStyle"
onShareAppMessage
:showFloatButton="true"
>
@@ -30,6 +30,43 @@
});
const template = computed(() => sheep.$store('app').template?.home);
// 代码直接接管首页顶部导航栏 Logo,不依赖后台装修配置
const navbarStyle = computed(() => {
const base = template.value?.style?.navbar || {};
return {
...base,
mode: 'normal',
type: 'color',
color: '#FFFFFF',
list: {
mp: [
{
width: 3,
height: 1,
top: 0,
left: 0,
type: 'image',
src: '/static/logo.png',
url: '/pages/index/index',
borderRadius: 0,
},
],
app: [
{
width: 4,
height: 1,
top: 0,
left: 2,
type: 'image',
src: '/static/logo.png',
url: '/pages/index/index',
borderRadius: 0,
},
],
},
};
});
onLoad((options) => {
// #ifdef MP
// 小程序识别二维码

查看文件

@@ -8,7 +8,7 @@
:num="state.goodsItem.goods_num"></s-goods-item>
</view>
<uni-forms ref="form" v-model="formData" :rules="rules" label-position="top">
<uni-forms ref="form" :modelValue="formData" :rules="rules" label-position="top">
<!-- 售后类型 -->
<view class="refund-item">
<view class="item-title ss-m-b-20">售后类型</view>
@@ -135,7 +135,7 @@ const state = reactive({
},
],
});
const formData = reactive({
let formData = reactive({
type: '',
reason: '',
mobile: '',

查看文件

@@ -100,7 +100,7 @@
>
<view class="item-title">兑换券</view>
<view class="ss-flex ss-col-center" @tap="openVoucher">
<text class="item-value text-red" v-if="state.orderPayload.voucher_fee > 0"
<text class="item-value text-red" v-if="Number(state.orderInfo.voucher_fee || state.orderPayload.voucher_fee) > 0"
>-¥{{ state.orderInfo.voucher_fee || state.orderPayload.voucher_fee }}</text
>
<text
@@ -197,7 +197,7 @@
<view class="footer-box border-top ss-flex ss-row-between ss-p-x-20 ss-col-center">
<view class="total-box-footer ss-flex ss-col-center">
<view class="total-num ss-font-30 text-red"> ¥{{ state.orderInfo.pay_fee }} </view>
<view v-if="state.orderPayload.order_type === 'score'" class="ss-flex">
<view v-if="state.orderPayload.order_type === 'score' && Number(state.orderInfo.score_amount) > 0" class="ss-flex">
<view class="total-num ss-font-30 text-red ss-m-l-4">+</view>
<image
:src="sheep.$url.static('/assets/addons/shopro/uniapp/goods/score1.svg')"
@@ -313,10 +313,22 @@
}
if (exchangeNow.value) {
const hasScore = Number(state.orderInfo.score_amount) > 0;
const voucherFee = Number(state.orderInfo.voucher_fee || state.orderPayload.voucher_fee || 0);
let confirmContent = '确定立即兑换此商品?';
if (hasScore && voucherFee > 0) {
confirmContent = `确定使用 ${voucherFee} 元兑换券与 ${state.orderInfo.score_amount} 积分立即兑换?`;
} else if (voucherFee > 0) {
confirmContent = `确定使用 ${voucherFee} 元兑换券立即兑换(现金实付 0 元)?`;
} else if (hasScore) {
confirmContent = `确定使用 ${state.orderInfo.score_amount} 积分立即兑换?`;
}
uni.showModal({
title: '提示',
content: '确定使用积分立即兑换?',
title: '兑换确认',
content: confirmContent,
cancelText: '再想想',
confirmText: '确定兑换',
success: async function (res) {
if (res.confirm) {
submitOrder();
@@ -359,6 +371,10 @@
state.orderInfo.goods_list.forEach((item) => {
state.totalNumber += item.goods_num;
});
// 积分/兑换商城订单:若后端自动计算了兑换券抵扣,同步至 orderPayload
if (state.orderPayload.order_type === 'score' && state.orderPayload.voucher_fee === undefined && data.voucher_fee !== undefined) {
state.orderPayload.voucher_fee = data.voucher_fee;
}
}
// 获取可用优惠券

查看文件

@@ -0,0 +1,138 @@
<!-- 微信小程序手机号一键快捷登录 - mpMobileLogin -->
<template>
<view class="mp-mobile-login-wrap">
<!-- 标题栏 -->
<view class="head-box ss-m-b-50">
<view class="ss-flex ss-m-b-16">
<view class="head-title head-title-animation">手机号快捷登录</view>
</view>
<view class="head-subtitle">授权微信手机号,享有一键快捷登录体验</view>
</view>
<!-- 微信手机号一键登录主按钮 -->
<view class="btn-box ss-m-b-40">
<button
class="ss-reset-button wechat-quick-btn ss-flex ss-row-center ss-col-center"
open-type="getPhoneNumber"
@getphonenumber="onGetPhoneNumber"
>
<text class="cicon-wechat ss-m-r-16 wechat-icon"></text>
<text class="btn-text">手机号一键快捷登录</text>
</button>
</view>
<!-- 备选切换登录方式 -->
<view class="switch-box ss-flex ss-row-center ss-col-center">
<view class="switch-item" @tap="showAuthModal('smsLogin')">验证码登录</view>
<view class="divider"></view>
<view class="switch-item" @tap="showAuthModal('accountLogin')">账号密码登录</view>
</view>
</view>
</template>
<script setup>
import sheep from '@/sheep';
import { showAuthModal, closeAuthModal } from '@/sheep/hooks/useModal';
const props = defineProps({
agreeStatus: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(['onConfirm']);
async function onGetPhoneNumber(e) {
if (!props.agreeStatus) {
emits('onConfirm', true);
sheep.$helper.toast('请阅读并勾选协议');
return;
}
if (e.detail?.errMsg !== 'getPhoneNumber:ok') {
return;
}
uni.showLoading({
title: '登录中...',
mask: true,
});
try {
const result = await sheep.$platform.useProvider('wechat').mobileLogin(e.detail);
if (result) {
closeAuthModal();
await sheep.$store('user').loginAfter();
sheep.$helper.toast('登录成功');
} else {
sheep.$helper.toast('登录失败,请重试');
}
} catch (err) {
console.error('mobileLogin error', err);
sheep.$helper.toast('登录异常,请稍后重试');
} finally {
uni.hideLoading();
}
}
</script>
<style lang="scss" scoped>
@import '../index.scss';
.mp-mobile-login-wrap {
padding-top: 10rpx;
}
.btn-box {
width: 100%;
margin-top: 50rpx;
}
.wechat-quick-btn {
width: 100%;
height: 88rpx;
background: #07c160;
border-radius: 44rpx;
color: #ffffff;
box-shadow: 0 8rpx 16rpx rgba(7, 193, 96, 0.25);
transition: all 0.2s;
&:active {
opacity: 0.9;
transform: scale(0.98);
}
.wechat-icon {
font-size: 40rpx;
line-height: 1;
}
.btn-text {
font-size: 32rpx;
font-weight: 500;
letter-spacing: 1rpx;
}
}
.switch-box {
margin-top: 36rpx;
font-size: 26rpx;
color: #888888;
.switch-item {
padding: 10rpx 20rpx;
font-size: 26rpx;
color: #666666;
&:active {
color: var(--ui-BG-Main);
}
}
.divider {
width: 2rpx;
height: 24rpx;
background-color: #dddddd;
}
}
</style>

查看文件

@@ -8,6 +8,13 @@
@click="closeAuthModal"
/> -->
<view class="login-wrap">
<!-- 0. 微信小程序手机号一键快捷登录 mpMobileLogin -->
<mp-mobile-login
v-if="authType === 'mpMobileLogin'"
:agreeStatus="state.protocol"
@onConfirm="onConfirm"
/>
<!-- 1. 账号密码登录 accountLogin -->
<account-login
v-if="authType === 'accountLogin'"
@@ -56,7 +63,7 @@
>立即注册</button
>
<view class="or-title">或</view>
<button class="ss-reset-button login-btn" @tap="thirdLogin('wechat')">快捷登录</button>
<button class="ss-reset-button login-btn" @tap="showAuthModal('mpMobileLogin')">快捷登录</button>
<view class="circle"></view>
</view>
@@ -99,7 +106,7 @@
</view>
<view
v-if="['accountLogin', 'smsLogin', 'smsRegister'].includes(authType)"
v-if="['accountLogin', 'smsLogin', 'smsRegister', 'mpMobileLogin'].includes(authType)"
class="agreement-box ss-flex ss-row-center"
:class="{ shake: currentProtocol }"
>
@@ -136,6 +143,7 @@
<script setup>
import { computed, reactive, ref } from 'vue';
import sheep from '@/sheep';
import mpMobileLogin from './components/mp-mobile-login.vue';
import accountLogin from './components/account-login.vue';
import smsLogin from './components/sms-login.vue';
import smsRegister from './components/sms-register.vue';

查看文件

@@ -13,7 +13,11 @@
class="menu-icon-wrap ss-flex ss-row-center ss-col-center"
@tap="sheep.$router.go(data.url)"
>
<image class="nav-image" :src="sheep.$url.cdn(data.src)" mode="aspectFit"></image>
<image
class="nav-image"
:src="data.src && data.src.startsWith('/static/') ? data.src : sheep.$url.cdn(data.src)"
mode="aspectFit"
></image>
</view>
<view class="ss-flex-1" v-if="data.type == 'search'" :style="[{ width: width }]">
<s-search-block
@@ -55,8 +59,11 @@
}
.menu-icon-wrap {
width: 100%;
height: 100%;
.nav-image {
height: 24px;
height: 30px;
max-width: 100%;
}
}
</style>

查看文件

@@ -29,15 +29,22 @@
<view class="ss-col-bottom">
<view
v-if="goodsFields.score_price?.show"
class="md-goods-price ss-m-t-16 font-OPPOSANS ss-m-r-10 ss-flex"
class="md-goods-price ss-m-t-16 font-OPPOSANS ss-m-r-10 ss-flex ss-col-center"
:style="[{ color: goodsFields.score_price.color }]"
>
<view>{{ Number(data.price[0]) > 0 ? '¥' + data.price[0] + '+' : '' }}</view>
<view v-if="Number(data.price?.[0] || data.price) > 0">
¥{{ data.price?.[0] || data.price }}
<text v-if="Number(data.score) > 0">+</text>
</view>
<image
v-if="Number(data.score) > 0"
:src="sheep.$url.static('/assets/addons/shopro/uniapp/goods/score1.svg')"
class="score-img"
></image>
{{ data.score }}
<text v-if="Number(data.score) > 0">{{ data.score }}</text>
<view v-if="Number(data.score) <= 0 && Number(data.price?.[0] || data.price) > 0" class="voucher-sub-tag ss-m-l-8">
兑换券
</view>
</view>
<view
@@ -88,15 +95,22 @@
<view class="ss-m-t-10">
<view
v-if="goodsFields.score_price?.show"
class="lg-goods-price ss-m-r-12 ss-flex ss-col-bottom font-OPPOSANS"
class="lg-goods-price ss-m-r-12 ss-flex ss-col-center font-OPPOSANS"
:style="[{ color: goodsFields.score_price.color }]"
>
<view>{{ Number(data.price[0]) > 0 ? '¥' + data.price[0] + '+' : '' }}</view>
<view v-if="Number(data.price?.[0] || data.price) > 0">
¥{{ data.price?.[0] || data.price }}
<text v-if="Number(data.score) > 0">+</text>
</view>
<image
v-if="Number(data.score) > 0"
:src="sheep.$url.static('/assets/addons/shopro/uniapp/goods/score1.svg')"
class="score-img"
></image>
{{ data.score }}
<text v-if="Number(data.score) > 0">{{ data.score }}</text>
<view v-if="Number(data.score) <= 0 && Number(data.price?.[0] || data.price) > 0" class="voucher-sub-tag ss-m-l-8">
兑换券
</view>
</view>
<view
v-if="goodsFields.price?.show && data.original_price > 0"
@@ -146,13 +160,20 @@
</view>
</slot>
<view class="ss-flex ss-col-bottom ss-p-l-16 ss-p-r-16 font-OPPOSANS">
<view class="sl-goods-price ss-m-r-12 ss-flex">
<view>{{ Number(data.price[0]) > 0 ? '¥' + data.price[0] + '+' : '' }}</view>
<view class="sl-goods-price ss-m-r-12 ss-flex ss-col-center">
<view v-if="Number(data.price?.[0] || data.price) > 0">
¥{{ data.price?.[0] || data.price }}
<text v-if="Number(data.score) > 0">+</text>
</view>
<image
v-if="Number(data.score) > 0"
:src="sheep.$url.static('/assets/addons/shopro/uniapp/goods/score1.svg')"
class="score-img"
></image>
<view>{{ data.score ? data.score : '' }}</view>
<view v-if="Number(data.score) > 0">{{ data.score }}</view>
<view v-if="Number(data.score) <= 0 && Number(data.price?.[0] || data.price) > 0" class="voucher-sub-tag ss-m-l-8">
兑换券
</view>
</view>
<view
v-if="data.original_price > 0"
@@ -454,4 +475,13 @@
height: 36rpx;
margin: 0 4rpx;
}
.voucher-sub-tag {
font-size: 20rpx;
color: #ff3000;
background: rgba(255, 48, 0, 0.1);
border: 1rpx solid rgba(255, 48, 0, 0.25);
border-radius: 6rpx;
padding: 2rpx 8rpx;
line-height: normal;
}
</style>

查看文件

@@ -2,15 +2,12 @@
<template>
<view class="ss-user-info-wrap ss-p-t-50">
<view class="ss-flex ss-col-center ss-row-between ss-m-b-20">
<view class="left-box ss-flex ss-col-center ss-m-l-36">
<!-- 已登录状态 -->
<view v-if="isLogin" class="left-box ss-flex ss-col-center ss-m-l-36">
<view class="avatar-box ss-m-r-24">
<image
class="avatar-img"
:src="
isLogin
? sheep.$url.cdn(userInfo.avatar)
: sheep.$url.static('/assets/addons/shopro/uniapp/default_avatar.png')
"
:src="sheep.$url.cdn(userInfo.avatar)"
mode="aspectFill"
@tap="sheep.$router.go('/pages/user/info')"
></image>
@@ -20,7 +17,7 @@
<view class="nick-name ss-m-r-20">{{ userInfo?.nickname || nickname }}</view>
</view>
<view
v-if="isLogin && showIdentityBadges"
v-if="showIdentityBadges"
class="identity-row ss-flex ss-col-center ss-flex-wrap"
@tap="onBadgeTap"
>
@@ -32,6 +29,55 @@
</view>
</view>
</view>
<!-- 未登录状态:微信小程序原生手机号快捷登录按钮 -->
<!-- #ifdef MP-WEIXIN -->
<button
v-else
class="ss-reset-button left-box ss-flex ss-col-center ss-m-l-36 unlogin-btn"
open-type="getPhoneNumber"
@getphonenumber="onQuickLogin"
>
<view class="avatar-box ss-m-r-24">
<image
class="avatar-img"
:src="sheep.$url.static('/assets/addons/shopro/uniapp/default_avatar.png')"
mode="aspectFill"
></image>
</view>
<view class="user-meta">
<view class="nickname-box ss-flex ss-col-center">
<view class="nick-name ss-m-r-20">点击登录</view>
</view>
<view class="quick-login-badge ss-flex ss-col-center">
<text class="cicon-wechat ss-m-r-8"></text>
<text class="quick-badge-text">手机号快捷登录</text>
</view>
</view>
</button>
<!-- #endif -->
<!-- 未登录状态:其他平台普通点击呼出弹窗 -->
<!-- #ifndef MP-WEIXIN -->
<view
v-else
class="left-box ss-flex ss-col-center ss-m-l-36"
@tap="showAuthModal()"
>
<view class="avatar-box ss-m-r-24">
<image
class="avatar-img"
:src="sheep.$url.static('/assets/addons/shopro/uniapp/default_avatar.png')"
mode="aspectFill"
></image>
</view>
<view class="user-meta">
<view class="nickname-box ss-flex ss-col-center">
<view class="nick-name ss-m-r-20">{{ nickname }}</view>
</view>
</view>
</view>
<!-- #endif -->
<view class="right-box ss-m-r-52">
<button class="ss-reset-button" @tap="showShareModal">
<text class="sicon-qrcode"></text>
@@ -41,13 +87,24 @@
<view
class="bind-mobile-box ss-flex ss-row-between ss-col-center"
v-if="isLogin && !userInfo.verification?.mobile"
v-if="showBindMobile"
>
<view class="ss-flex">
<text class="cicon-mobile-o"></text>
<view class="mobile-title ss-m-l-20"> 点击绑定手机号确保账户安全 </view>
</view>
<!-- #ifdef MP-WEIXIN -->
<button
class="ss-reset-button bind-btn"
open-type="getPhoneNumber"
@getphonenumber="onBindPhoneNumber"
>
去绑定
</button>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<button class="ss-reset-button bind-btn" @tap="onBind">去绑定</button>
<!-- #endif -->
</view>
</view>
</template>
@@ -158,10 +215,50 @@
hasPartner.value,
);
const hasBoundMobile = computed(() => {
return Boolean(userInfo.value?.mobile || userInfo.value?.verification?.mobile);
});
const showBindMobile = computed(() => {
// 1. 未登录不展示
if (!isLogin.value) return false;
// 2. 存在已绑定手机号不展示
if (hasBoundMobile.value) return false;
// 3. 微信小程序环境:默认手机号快捷登录体系,不展示此冗余横幅
// #ifdef MP-WEIXIN
return false;
// #endif
// 4. 其他环境:受平台设置“绑定手机号”开关控制
return Boolean(sheep.$store('app').platform?.bind_mobile);
});
function onBind() {
showAuthModal('changeMobile');
}
// 微信小程序一键快捷绑定手机号
async function onBindPhoneNumber(e) {
if (e.detail?.errMsg !== 'getPhoneNumber:ok') {
showAuthModal('changeMobile');
return;
}
uni.showLoading({ title: '绑定中...', mask: true });
try {
const result = await sheep.$platform.useProvider('wechat').bindUserPhoneNumber(e.detail);
if (result) {
await sheep.$store('user').getInfo();
sheep.$helper.toast('绑定成功');
} else {
showAuthModal('changeMobile');
}
} catch (err) {
console.error('onBindPhoneNumber error', err);
showAuthModal('changeMobile');
} finally {
uni.hideLoading();
}
}
function onBadgeTap() {
if (!isLogin.value) {
showAuthModal();
@@ -169,12 +266,71 @@
}
sheep.$router.go('/pages/commission/index');
}
// 微信小程序手机号一键快捷登录
async function onQuickLogin(e) {
if (e.detail?.errMsg !== 'getPhoneNumber:ok') {
showAuthModal();
return;
}
uni.showLoading({
title: '登录中...',
mask: true,
});
try {
const result = await sheep.$platform.useProvider('wechat').mobileLogin(e.detail);
if (result) {
await sheep.$store('user').loginAfter();
sheep.$helper.toast('登录成功');
} else {
sheep.$helper.toast('登录失败,请重试');
showAuthModal();
}
} catch (err) {
console.error('onQuickLogin error', err);
sheep.$helper.toast('登录异常,请稍后重试');
} finally {
uni.hideLoading();
}
}
</script>
<style lang="scss" scoped>
.ss-user-info-wrap {
box-sizing: border-box;
.unlogin-btn {
text-align: left;
background: transparent;
padding: 0;
line-height: normal;
}
.quick-login-badge {
display: inline-flex;
align-items: center;
margin-top: 8rpx;
padding: 4rpx 14rpx;
font-size: 20rpx;
line-height: 28rpx;
color: #07c160;
background: rgba(7, 193, 96, 0.1);
border-radius: 20rpx;
border: 1rpx solid rgba(7, 193, 96, 0.25);
.cicon-wechat {
font-size: 24rpx;
line-height: 1;
}
.quick-badge-text {
font-size: 20rpx;
font-weight: 500;
}
}
.avatar-box {
width: 100rpx;
height: 100rpx;

查看文件

@@ -6,13 +6,22 @@ import test from '@/sheep/helper/test.js';
import $api from '@/sheep/api';
// 打开授权弹框
export function showAuthModal(type = 'accountLogin') {
export function showAuthModal(type = '') {
const modal = $store('modal');
let targetType = type;
if (!targetType) {
// #ifdef MP-WEIXIN
targetType = 'mpMobileLogin';
// #endif
// #ifndef MP-WEIXIN
targetType = 'accountLogin';
// #endif
}
// closeAuthModal();
setTimeout(() => {
modal.$patch((state) => {
state.auth = type;
state.auth = targetType;
});
}, 200);

查看文件

@@ -50,20 +50,27 @@ const login = async () => {
// 微信小程序手机号授权登陆
const mobileLogin = async (e) => {
return new Promise(async (resolve, reject) => {
if (e.errMsg !== 'getPhoneNumber:ok') {
const detail = e?.detail || e || {};
if (detail.errMsg !== 'getPhoneNumber:ok') {
resolve(false);
return;
}
const { code } = await third.wechat.login(
let currentSessionId = uni.getStorageSync('sessionId');
if (!currentSessionId) {
await getSessionId(false);
currentSessionId = uni.getStorageSync('sessionId');
}
const res = await third.wechat.login(
{
platform: 'miniProgram',
payload: encodeURIComponent(
JSON.stringify({
sessionId: uni.getStorageSync('sessionId'),
code: e.code,
iv: e.iv,
encryptedData: e.encryptedData,
sessionId: currentSessionId,
code: detail.code,
iv: detail.iv,
encryptedData: detail.encryptedData,
}),
),
},
@@ -72,12 +79,35 @@ const mobileLogin = async (e) => {
},
);
if (code === 1) {
if (res?.code === 1) {
resolve(true);
return;
}
if (code === -1) {
getSessionId(false);
if (res?.code === -1) {
const got = await getSessionId(false);
if (got) {
const retryRes = await third.wechat.login(
{
platform: 'miniProgram',
payload: encodeURIComponent(
JSON.stringify({
sessionId: uni.getStorageSync('sessionId'),
code: e.code,
iv: e.iv,
encryptedData: e.encryptedData,
}),
),
},
{
shareInfo: uni.getStorageSync('shareLog') || {},
},
);
if (retryRes?.code === 1) {
resolve(true);
return;
}
}
}
resolve(false);
});
@@ -229,20 +259,57 @@ const checkUpdate = async (silence = true) => {
// 绑定用户手机号
const bindUserPhoneNumber = (e) => {
return new Promise(async (resolve, reject) => {
const { code } = await third.wechat.bindUserPhoneNumber({
const detail = e?.detail || e || {};
if (detail.errMsg && !detail.errMsg.includes('ok')) {
resolve(false);
return;
}
let sessionId = uni.getStorageSync('sessionId');
if (!sessionId) {
await getSessionId(false);
sessionId = uni.getStorageSync('sessionId');
}
const res = await third.wechat.bindUserPhoneNumber({
platform: 'miniProgram',
payload: encodeURIComponent(
JSON.stringify({
sessionId: uni.getStorageSync('sessionId'),
iv: e.iv,
encryptedData: e.encryptedData,
code: e.code,
sessionId: sessionId,
iv: detail.iv,
encryptedData: detail.encryptedData,
code: detail.code,
}),
),
});
if (code === 1) {
if (res?.code === 1) {
await sheep.$store('user').getInfo();
resolve(true);
return;
}
// sessionId 过期重试
if (res?.code === -1) {
await getSessionId(false);
const retryRes = await third.wechat.bindUserPhoneNumber({
platform: 'miniProgram',
payload: encodeURIComponent(
JSON.stringify({
sessionId: uni.getStorageSync('sessionId'),
iv: detail.iv,
encryptedData: detail.encryptedData,
code: detail.code,
}),
),
});
if (retryRes?.code === 1) {
await sheep.$store('user').getInfo();
resolve(true);
return;
}
}
resolve(false);
});
};
@@ -279,6 +346,7 @@ function subscribeMessage(event) {
export default {
load,
login,
mobileLogin,
bind,
unbind,
transfer,

查看文件

@@ -80,6 +80,9 @@ const user = defineStore({
if (!Array.isArray(merged.identities)) {
merged.identities = [];
}
if (merged.mobile) {
merged.verification = { ...(merged.verification || {}), mobile: 1 };
}
this.userInfo = merged;
return Promise.resolve(merged);
@@ -151,7 +154,7 @@ const user = defineStore({
// 登录后设置全局分享参数
$share.getShareInfo();
// 提醒绑定手机号
if (app().platform.bind_mobile && !this.userInfo.verification?.mobile) {
if (app().platform.bind_mobile && !this.userInfo?.mobile && !this.userInfo?.verification?.mobile) {
showAuthModal('changeMobile');
}

二进制
uniapp/static/logo.jpg 普通文件

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 77 KiB

二进制
uniapp/static/logo.png 普通文件

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 160 KiB

二进制
uniapp/static/logo_trans.png 普通文件

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 184 KiB