access->enterprise($enterprise, true); Fields::only($input, [ 'address', 'contact', 'phone', 'revision', 'request_key', ]); $address = Fields::text($input, 'address', 255); $contact = Fields::text($input, 'contact', 60); $phone = Fields::phone($input); $revision = Fields::text($input, 'revision', 64); return Commands::run($this->access->actor, Fields::text($input, 'request_key', 80), 'address:' . $enterprise, $input, function () use ($enterprise, $address, $contact, $phone, $revision) { $this->access->enterprise($enterprise, true); $enterpriseRow = WaterEnterprise::query()->where('id', $enterprise)->lockForUpdate()->firstOrFail(); if (!hash_equals(WaterPresenter::revision($enterpriseRow), $revision)) { throw new ApiException(40901, '企业资料已更新,请刷新', 409); } // Booking uses the same enterprise lock. A locking read sees tasks committed after // the earlier authorization snapshot, even under MySQL REPEATABLE READ. $activeTask = WaterDelivery::query()->where('enterprise_id', $enterprise) ->whereNotNull('active_key')->lockForUpdate()->first(['id']); if ($activeTask) { throw new ApiException(40902, '存在未完成配送,请完成或取消后修改配送地址', 409); } $values = [ 'address' => $address, 'contact' => $contact, 'phone' => $phone, 'updated_at' => date('Y-m-d H:i:s'), ]; if ($enterpriseRow->address !== $address) { $values += [ 'longitude' => null, 'latitude' => null, ]; } $enterpriseRow->fill($values)->save(); $default = WaterEnterpriseAddress::query()->where('enterprise_id', $enterprise)->where('is_default', 1)->where('status', 1)->lockForUpdate()->first(); if ($default) { $default->fill([ 'address' => $values['address'], 'contact' => $values['contact'], 'phone' => $values['phone'], 'longitude' => $values['longitude'] ?? $default->longitude, 'latitude' => $values['latitude'] ?? $default->latitude, 'updated_at' => $values['updated_at'], ])->save(); } return [ 'id' => $enterprise, 'revision' => WaterPresenter::revision($enterpriseRow), ]; }, fn () => $this->access->enterprise($enterprise, true)); } private function addressValues(array $input): array { Fields::only($input, [ 'label', 'address', 'contact', 'phone', 'region', 'longitude', 'latitude', 'is_default', 'revision', 'request_key', ]); $coordinates = null; if (array_key_exists('longitude', $input) || array_key_exists('latitude', $input)) { if (!array_key_exists('longitude', $input) || !array_key_exists('latitude', $input)) { throw new ApiException(42201, '经纬度必须同时填写', 422); } $coordinates = Fields::location($input); } return [ 'label' => Fields::text($input, 'label', 80, false) ?: '配送地址', 'address' => Fields::text($input, 'address', 255), 'contact' => Fields::text($input, 'contact', 60), 'phone' => Fields::phone($input), 'region' => Fields::text($input, 'region', 80, false), 'longitude' => $coordinates['longitude'] ?? null, 'latitude' => $coordinates['latitude'] ?? null, 'is_default' => filter_var($input['is_default'] ?? false, FILTER_VALIDATE_BOOLEAN), 'revision' => Fields::text($input, 'revision', 64, false), ]; } public function createAddress(int $enterprise, array $input): array { $this->access->enterprise($enterprise, true); Fields::only($input, [ 'label', 'address', 'contact', 'phone', 'region', 'longitude', 'latitude', 'is_default', 'request_key', ]); $values = $this->addressValues($input); $requestKey = Fields::text($input, 'request_key', 80); return Commands::run($this->access->actor, $requestKey, 'address:create:' . $enterprise, $input, function () use ($enterprise, $values) { $enterpriseRow = WaterEnterprise::query()->where('id', $enterprise)->lockForUpdate()->firstOrFail(); $hasDefault = WaterEnterpriseAddress::query()->where('enterprise_id', $enterprise)->where('is_default', 1)->where('status', 1)->exists(); $makeDefault = !$hasDefault || $values['is_default']; if ($makeDefault) { WaterEnterpriseAddress::query()->where('enterprise_id', $enterprise)->update(['is_default' => 0]); } $now = date('Y-m-d H:i:s'); $id = WaterEnterpriseAddress::query()->insertGetId([ 'enterprise_id' => $enterprise, 'label' => $values['label'], 'contact' => $values['contact'], 'phone' => $values['phone'], 'region' => $values['region'], 'address' => $values['address'], 'longitude' => $values['longitude'], 'latitude' => $values['latitude'], 'is_default' => $makeDefault ? 1 : 0, 'status' => 1, 'created_at' => $now, 'updated_at' => $now, ]); if ($makeDefault) { $enterpriseRow->fill([ 'address' => $values['address'], 'contact' => $values['contact'], 'phone' => $values['phone'], 'region' => $values['region'], 'longitude' => $values['longitude'], 'latitude' => $values['latitude'], 'updated_at' => $now, ])->save(); } return ['id' => (int) $id, 'is_default' => $makeDefault]; }, fn () => $this->access->enterprise($enterprise, true)); } public function updateAddress(int $enterprise, int $address, array $input): array { $this->access->enterprise($enterprise, true); $values = $this->addressValues($input); $requestKey = Fields::text($input, 'request_key', 80); return Commands::run($this->access->actor, $requestKey, 'address:update:' . $address, $input, function () use ($enterprise, $address, $values) { $enterpriseRow = WaterEnterprise::query()->where('id', $enterprise)->lockForUpdate()->firstOrFail(); $row = WaterEnterpriseAddress::query()->where('enterprise_id', $enterprise)->where('status', 1)->lockForUpdate()->find($address); if (!$row) throw new ApiException(40401, '配送地址不存在或已停用', 404); if ($values['revision'] === '' || !hash_equals(EnterpriseAddressPresenter::revision($row), $values['revision'])) { throw new ApiException(40901, '配送地址已更新,请刷新', 409); } if (WaterDelivery::query()->where('address_id', $address)->whereNotNull('active_key')->exists() && ($row->address !== $values['address'] || $row->longitude != $values['longitude'] || $row->latitude != $values['latitude'])) { throw new ApiException(40902, '该地址存在未完成配送,请完成或取消后修改位置', 409); } $makeDefault = $values['is_default'] || (int) $row->is_default === 1; if ($makeDefault) WaterEnterpriseAddress::query()->where('enterprise_id', $enterprise)->update(['is_default' => 0]); $now = date('Y-m-d H:i:s'); $row->fill([ 'label' => $values['label'], 'address' => $values['address'], 'contact' => $values['contact'], 'phone' => $values['phone'], 'region' => $values['region'], 'longitude' => $values['longitude'], 'latitude' => $values['latitude'], 'is_default' => $makeDefault ? 1 : 0, 'updated_at' => $now, ])->save(); if ($makeDefault) { $enterpriseRow->fill([ 'address' => $values['address'], 'contact' => $values['contact'], 'phone' => $values['phone'], 'region' => $values['region'], 'longitude' => $values['longitude'], 'latitude' => $values['latitude'], 'updated_at' => $now, ])->save(); } return ['id' => $address, 'revision' => EnterpriseAddressPresenter::revision($row), 'is_default' => $makeDefault]; }, fn () => $this->access->enterprise($enterprise, true)); } public function book(int $enterprise, array $input): array { $this->access->enterprise($enterprise, true); Fields::only($input, ['sku_id', 'quantity', 'address_id', 'request_key']); $sku = Fields::id($input, 'sku_id'); $quantity = Input::integer($input['quantity'] ?? null, 'quantity', 1, 10000); $address = Input::integer($input['address_id'] ?? 0, 'address_id', 0, 10000000); return Commands::run($this->access->actor, Fields::text($input, 'request_key', 80), 'delivery.book:' . $enterprise, $input, function () use ($enterprise, $sku, $quantity, $address) { // Match the existing SKU -> enterprise -> account lock order. WaterSku::query()->where('id', $sku)->lockForUpdate()->first(); WaterEnterprise::query()->where('id', $enterprise)->lockForUpdate()->first(); $this->access->enterprise($enterprise, true); return (new WaterBusiness())->createDelivery([ 'enterprise_id' => $enterprise, 'sku_id' => $sku, 'quantity' => $quantity, 'address_id' => $address, ], 0); }, fn () => $this->access->enterprise($enterprise, true)); } public function cancel(int $enterprise, int $id, array $input): array { $this->access->enterprise($enterprise, true); $this->access->enterpriseTask($enterprise, $id); Fields::only($input, ['request_key']); return Commands::run($this->access->actor, Fields::text($input, 'request_key', 80), 'delivery.cancel:' . $id, $input, function () use ($enterprise, $id) { $this->access->enterprise($enterprise, true); $task = $this->access->enterpriseTask($enterprise, $id); WaterSku::query()->where('id', $task->sku_id)->lockForUpdate()->first(); $locked = WaterDelivery::query()->where('id', $id)->lockForUpdate()->firstOrFail(); // Customers may cancel only unassigned requests; dispatched work requires operator handling. if (!in_array($locked->status, ['pending', 'cancelled'], true)) { throw new ApiException(40902, '已派单的配送请联系客服取消', 409); } return (new WaterBusiness())->transition([ 'id' => $id, 'action' => 'cancel', ], 0); }, fn () => $this->access->enterprise($enterprise, true)); } }