| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141 |
- <?php
- declare(strict_types=1);
- namespace plugin\saiadmin\app\service\promotion;
- use app\miniapi\model\MiniGrant;
- use app\miniapi\model\MiniCheckout;
- use plugin\saiadmin\app\model\water\WaterEnterprise;
- use plugin\saiadmin\app\model\water\WaterDelivery;
- use plugin\saiadmin\app\model\water\WaterSku;
- use plugin\saiadmin\app\model\water\WaterModel;
- use plugin\saiadmin\app\model\water\WaterOrder;
- use plugin\saiadmin\app\model\water\WaterPromotionCode;
- use plugin\saiadmin\app\model\water\WaterPromotionReferral;
- use plugin\saiadmin\app\model\water\WaterPromotionSetting;
- use plugin\saiadmin\exception\ApiException;
- final class PromotionReferrals
- {
- public function code(int $enterprise): array
- {
- if (!PromotionPolicy::current()['enabled']) {
- throw new ApiException('推广活动尚未开启');
- }
- return WaterModel::transaction(function () use ($enterprise) {
- $row = WaterEnterprise::query()->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();
- }
- }
|