where('status', 1)->lockForUpdate()->find($enterprise); if (!$row) { throw new ApiException('企业已停用或不存在'); } $code = WaterPromotionCode::query()->firstOrCreate(['enterprise_id' => $enterprise], [ 'code' => strtoupper(bin2hex(random_bytes(10))), 'created_at' => date('Y-m-d H:i:s'), ]); return ['invite_code' => $code->code]; }); } public function bind(int $enterprise, string $code, int $user): array { if (!preg_match('/^[A-F0-9]{20}$/D', $code)) { throw new ApiException('邀请码格式不正确'); } if (!PromotionPolicy::current()['initialized']) { throw new ApiException('请先初始化推广活动'); } return WaterModel::transaction(function () use ($enterprise, $code, $user) { // Serialize new attribution edges, preventing reciprocal/cyclic invitations. $policy = WaterPromotionSetting::query()->lockForUpdate()->find(1); if (!$policy || !$policy->enabled) { throw new ApiException('推广活动尚未开启'); } $invitation = WaterPromotionCode::query()->where('code', $code)->first(); if (!$invitation || (int) $invitation->enterprise_id === $enterprise) { throw new ApiException('邀请码无效或不能邀请自身企业'); } $inviter = (int) $invitation->enterprise_id; if (!WaterSku::query()->where('status', 1)->lockForUpdate()->find($policy->sku_id)) { throw new ApiException('活动赠水规格已停用,请联系平台'); } $ids = [$enterprise, $inviter]; sort($ids, SORT_NUMERIC); $companies = WaterEnterprise::query()->whereIn('id', $ids)->orderBy('id')->lockForUpdate()->get(); if ($companies->count() !== 2 || $companies->contains(fn ($row) => (int) $row->status !== 1)) { throw new ApiException('邀请企业或当前企业已停用'); } $old = WaterPromotionReferral::query()->where('invited_id', $enterprise)->lockForUpdate()->first(); if ($old) { if ((int) $old->inviter_id !== $inviter) { throw new ApiException('该企业已经绑定其他邀请人,不能修改'); } return ['id' => (int) $old->id, 'replayed' => true]; } if (WaterOrder::query()->where('enterprise_id', $enterprise)->lockForUpdate()->first(['id'])) { throw new ApiException('已有购水记录,不能参加新企业首购邀请'); } $inviterAdmins = MiniGrant::query()->where('role', 'enterprise_admin')->where('status', 1) ->where('resource_id', $inviter)->pluck('user_id'); if ($inviterAdmins->contains($user) || MiniGrant::query()->where('role', 'enterprise_admin') ->where('status', 1)->where('resource_id', $enterprise)->whereIn('user_id', $inviterAdmins)->exists()) { throw new ApiException('同一管理员管理的企业不能互相邀请'); } $cursor = $inviter; $seen = [$enterprise]; while ($cursor) { if (in_array($cursor, $seen, true) || count($seen) > 100) { throw new ApiException('邀请关系存在循环或层级过深'); } $seen[] = $cursor; $cursor = (int) (WaterPromotionReferral::query()->where('invited_id', $cursor)->lockForUpdate()->first()?->inviter_id ?? 0); } $referral = WaterPromotionReferral::query()->create([ 'inviter_id' => $inviter, 'invited_id' => $enterprise, 'bound_by' => $user, 'policy_revision' => (int) $policy->revision, 'sku_id' => (int) $policy->sku_id, 'quantity' => (int) $policy->quantity, 'min_amount_cents' => (int) $policy->min_amount_cents, 'status' => 'bound', 'revision' => 1, 'created_at' => date('Y-m-d H:i:s'), ]); return ['id' => (int) $referral->id]; }); } /** Called inside the paid receipt transaction; old orders are never backfilled. */ public function purchased(WaterOrder $order, ?int $checkoutId = null): void { if (!PromotionPolicy::current()['initialized'] || (int) $order->amount_cents <= 0 || str_starts_with($order->reference, 'TEST-') || str_contains($order->package_name, '测试')) { return; } $referral = WaterPromotionReferral::query()->where('invited_id', $order->enterprise_id)->lockForUpdate()->first(); if (!$referral || $referral->status !== 'bound' || $referral->created_at > $order->created_at) { return; } if (WaterOrder::query()->where('enterprise_id', $order->enterprise_id)->where('id', '<', $order->id) ->where('amount_cents', '>', 0)->where('reference', 'not like', 'TEST-%') ->where('package_name', 'not like', '%测试%')->lockForUpdate()->first(['id'])) { return; } // A combined checkout creates multiple receipts; the threshold uses the entire paid checkout. $checkout = $checkoutId ? MiniCheckout::query()->find($checkoutId) : null; if ($checkoutId && (!$checkout || (int) $checkout->enterprise_id !== (int) $order->enterprise_id || !str_starts_with($order->reference, $checkout->code . '-'))) { throw new ApiException('推广首购与结算订单不匹配'); } $amount = $checkout ? (int) $checkout->amount_cents : (int) $order->amount_cents; $deliveryFloor = WaterDelivery::query()->where('enterprise_id', $order->enterprise_id) ->where('sku_id', $order->sku_id)->orderByDesc('id')->lockForUpdate()->first(); // Even a below-threshold first purchase consumes eligibility; later purchases cannot replace it. $referral->fill([ 'order_id' => (int) $order->id, 'checkout_id' => $checkoutId, 'purchase_amount_cents' => $amount, 'delivery_floor_id' => (int) ($deliveryFloor?->id ?? 0), 'status' => $amount >= (int) $referral->min_amount_cents ? 'pending' : 'rejected', 'qualified_at' => date('Y-m-d H:i:s'), 'revision' => (int) $referral->revision + 1, 'review_note' => $amount >= (int) $referral->min_amount_cents ? '' : '首笔购水未达到绑定时约定金额', ])->save(); } }