'净亏邀请奖励', 8 => '首充奖励', 9 => '转盘活动', 10 => '邀请升级奖励']; } public static function defaults(int $type): array { // Upgrade invite pays straight to the available balance, so it has no turnover ratio. if ($type === self::UPGRADE_INVITE) return ['tiers' => []]; $common = ['turnover_ratio' => '0.00']; return $common + match ($type) { 7 => ['loss_amount' => '500.00', 'invite_count' => 3, 'reward_amount' => '500.00', 'reward_mode' => 'cash', 'coupon_valid_from' => null, 'coupon_valid_until' => null, 'coupon_categories' => [], 'inviter_coupons' => [], 'invitee_coupons' => []], 8 => ['min_deposit' => '100.00', 'reward_amount' => '10.00'], 9 => ['completion_probability' => '20.00', 'online_seconds' => 3600, 'deposit_amount' => '3000.00', 'team_deposit_amount' => '500.00', 'bet_amount' => '5000.00', 'team_bet_amount' => '3000.00'], default => [], }; } public static function normalize(int $type, array $input): array { $defaults = self::defaults($type); Validator::make(['params' => $input], ['params' => ['array:' . implode(',', array_keys($defaults))]])->validate(); $data = array_replace($defaults, $input); if ($type === self::UPGRADE_INVITE) return ['tiers' => self::tiers($data['tiers'])]; $coupon = array_intersect_key($data, array_flip(self::COUPON_KEYS)); $data = array_diff_key($data, $coupon); $rules = []; foreach ($data as $key => $value) { $rules[$key] = match ($key) { 'invite_count' => ['required', 'integer', 'min:1', 'max:1000'], 'online_seconds' => ['required', 'integer', 'min:60', 'max:86400'], 'completion_probability' => ['required', 'numeric', 'between:0,100', 'regex:/^\d{1,3}(\.\d{1,2})?$/'], 'turnover_ratio' => ['required', 'numeric', 'between:0,10000', 'regex:/^\d{1,5}(\.\d{1,2})?$/'], default => ['required', 'numeric', 'gt:0', 'regex:/^\d{1,12}(\.\d{1,2})?$/'], }; } Validator::make($data, $rules)->validate(); foreach ($data as $key => $value) $data[$key] = in_array($key, ['invite_count', 'online_seconds'], true) ? (int)$value : bcadd((string)$value, '0', 2); return $coupon ? $data + self::coupons($coupon, $data['reward_amount']) : $data; } /** Coupon settings stay well-formed in cash mode so switching modes keeps them; completeness is enforced only in coupon mode. */ private static function coupons(array $input, string $rewardAmount): array { $invalid = fn (string $message) => throw ValidationException::withMessages(['params' => $message]); $mode = $input['reward_mode']; if (!is_string($mode) || !in_array($mode, self::REWARD_MODES, true)) $invalid('reward_mode参数错误'); $data = ['reward_mode' => $mode]; foreach (['coupon_valid_from', 'coupon_valid_until'] as $key) { $value = $input[$key]; if ($value === null || $value === '') { $data[$key] = null; continue; } $time = is_string($value) ? \DateTimeImmutable::createFromFormat('!Y-m-d H:i:s', $value) : false; if (!$time || $time->format('Y-m-d H:i:s') !== $value) $invalid($key . '参数错误'); $data[$key] = $value; } $categories = $input['coupon_categories']; if (!is_array($categories) || array_diff($categories, self::COUPON_CATEGORIES) || count(array_unique($categories)) !== count($categories)) { $invalid('coupon_categories参数错误'); } $data['coupon_categories'] = array_values(array_intersect(self::COUPON_CATEGORIES, $categories)); foreach (['inviter_coupons', 'invitee_coupons'] as $key) { $tiers = $input[$key]; if (!is_array($tiers) || array_values($tiers) !== $tiers || count($tiers) > 10) $invalid($key . '参数错误'); $data[$key] = []; foreach ($tiers as $tier) { if (!is_array($tier) || array_diff(array_keys($tier), ['min_amount', 'discount', 'quantity']) || count($tier) !== 3) $invalid($key . '参数错误'); foreach (['min_amount', 'discount'] as $field) { if (!is_numeric($tier[$field]) || !preg_match('/^\d{1,12}(\.\d{1,2})?$/', (string)$tier[$field])) $invalid($key . '参数错误'); } $quantity = is_scalar($tier['quantity']) ? filter_var($tier['quantity'], FILTER_VALIDATE_INT) : false; $min = bcadd((string)$tier['min_amount'], '0', 2); $discount = bcadd((string)$tier['discount'], '0', 2); if (bccomp($discount, '0', 2) <= 0 || bccomp($min, $discount, 2) < 0 || $quantity === false || $quantity < 1 || $quantity > 100) { $invalid($key . '参数错误:抵扣金额须大于0且不高于使用门槛,份数为1至100'); } $data[$key][] = ['min_amount' => $min, 'discount' => $discount, 'quantity' => $quantity]; } } if ($data['coupon_valid_from'] !== null && $data['coupon_valid_until'] !== null && $data['coupon_valid_until'] <= $data['coupon_valid_from']) { $invalid('优惠券结束时间必须晚于开始时间'); } if ($mode === 'coupon') { if (!$data['inviter_coupons']) $invalid('请至少配置一档邀请人优惠券'); if (bccomp(self::faceValue($data['inviter_coupons']), $rewardAmount, 2) > 0) $invalid('优惠券抵扣总额不能超过奖励金额'); if ($data['coupon_valid_from'] === null || $data['coupon_valid_until'] === null) $invalid('请填写优惠券可用时间'); if (!$data['coupon_categories']) $invalid('请选择优惠券可用游戏类型'); } return $data; } /** Upgrade tiers: one bonus per share level reached, distinct levels from 2 up, sorted by level. */ public static function tiers($tiers): array { $invalid = fn (string $message) => throw ValidationException::withMessages(['params' => $message]); if (!is_array($tiers) || array_values($tiers) !== $tiers || count($tiers) > 50) $invalid('tiers参数错误:最多50档'); $data = []; foreach ($tiers as $tier) { if (!is_array($tier) || array_diff(array_keys($tier), ['level', 'amount']) || count($tier) !== 2) $invalid('tiers参数错误:每档须包含level和amount'); $level = is_scalar($tier['level']) ? filter_var($tier['level'], FILTER_VALIDATE_INT) : false; if ($level === false || $level < 2) $invalid('tiers参数错误:等级须为2及以上的整数'); if (isset($data[$level])) $invalid('tiers参数错误:等级' . $level . '重复'); if (!is_numeric($tier['amount']) || !preg_match('/^\d{1,12}(\.\d{1,2})?$/', (string)$tier['amount']) || bccomp((string)$tier['amount'], '0', 2) <= 0) { $invalid('tiers参数错误:奖励金额须大于0且最多两位小数'); } $data[$level] = ['level' => $level, 'amount' => bcadd((string)$tier['amount'], '0', 2)]; } ksort($data); return array_values($data); } /** Total discount of one coupon set: sum of discount × quantity. */ public static function faceValue(array $tiers): string { $total = '0.00'; foreach ($tiers as $tier) $total = bcadd($total, bcmul((string)$tier['discount'], (string)$tier['quantity'], 2), 2); return $total; } public static function draw(int $coins, bool $complete, string $probability, ?callable $random = null): array { $random ??= fn (int $min, int $max) => random_int($min, $max); $remaining = 66600 - $coins; if ($remaining <= 0) throw new \LogicException('请先领取本轮奖金'); $hit = $complete && $random(1, 10000) <= (int)bcmul($probability, '100', 0); if ($hit) return ['prize_index' => 0, 'amount_cents' => $remaining, 'reached' => true]; // Choose eligible sectors before drawing. A fixed face value is never truncated. $eligible = [0]; foreach (self::PRIZES as $i => $amount) if ($i && (int)$amount * 100 < $remaining) $eligible[] = $i; $index = $eligible[$random(0, count($eligible) - 1)]; $amount = $index === 0 ? $random(0, $remaining - 1) : (int)self::PRIZES[$index] * 100; return ['prize_index' => $index, 'amount_cents' => $amount, 'reached' => false]; } }