From 5ef2076b7402b8d64d3cb7c5d422bef43422aa43 Mon Sep 17 00:00:00 2001 From: weigang Date: Wed, 12 Aug 2020 11:49:24 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=96=E5=8D=96=E8=AE=A2=E5=8D=95=E4=B8=8B?= =?UTF-8?q?=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Controller/NotifyController.php | 10 + app/Controller/OrderController.php | 23 + app/Controller/PaymentController.php | 10 + app/Libs/MQTTClient.php | 1098 ++++++++++++++++++++++++ app/Middleware/Auth/ApiMiddleware.php | 2 +- app/Model/Coupon.php | 3 + app/Model/CouponUserRec.php | 10 + app/Model/CouponUserUse.php | 10 + app/Model/Goods.php | 12 + app/Model/Order.php | 11 + app/Model/OrderGoods.php | 8 + app/Model/OrderMain.php | 81 ++ app/Model/SpecCombination.php | 17 + app/Request/OrderOnlineRequest.php | 72 ++ app/Service/CoupnoServiceInterface.php | 20 + app/Service/CouponService.php | 85 ++ app/Service/OrderService.php | 423 +++++++++ app/Service/OrderServiceInterface.php | 29 + app/TaskWorker/MQTTClientTask.php | 27 + composer.json | 5 +- config/autoload/dependencies.php | 4 +- config/autoload/server.php | 3 - config/autoload/snowflake.php | 24 + config/routes.php | 1 + 24 files changed, 1982 insertions(+), 6 deletions(-) create mode 100644 app/Controller/NotifyController.php create mode 100644 app/Controller/OrderController.php create mode 100644 app/Controller/PaymentController.php create mode 100644 app/Libs/MQTTClient.php create mode 100644 app/Model/CouponUserRec.php create mode 100644 app/Model/CouponUserUse.php create mode 100644 app/Model/Goods.php create mode 100644 app/Model/Order.php create mode 100644 app/Model/OrderGoods.php create mode 100644 app/Model/OrderMain.php create mode 100644 app/Model/SpecCombination.php create mode 100644 app/Request/OrderOnlineRequest.php create mode 100644 app/Service/CoupnoServiceInterface.php create mode 100644 app/Service/CouponService.php create mode 100644 app/Service/OrderService.php create mode 100644 app/Service/OrderServiceInterface.php create mode 100644 app/TaskWorker/MQTTClientTask.php create mode 100644 config/autoload/snowflake.php diff --git a/app/Controller/NotifyController.php b/app/Controller/NotifyController.php new file mode 100644 index 0000000..8aa0ba0 --- /dev/null +++ b/app/Controller/NotifyController.php @@ -0,0 +1,10 @@ +orderService->addOnlineOrder($request->validated()); + return $this->success(['order_main_id' => $orderMainId]); + } +} \ No newline at end of file diff --git a/app/Controller/PaymentController.php b/app/Controller/PaymentController.php new file mode 100644 index 0000000..86ce06c --- /dev/null +++ b/app/Controller/PaymentController.php @@ -0,0 +1,10 @@ +setConnection($address, $port, $protocol)) { + ; + } + $this->packetId = rand(1,100)*100; // Reduce risk of creating duplicate ids in sequential sessions + } + + /** + * Class destructor - Close socket + */ + function __destruct(){ + $this->close(); + } + + /** + * Setup conection parameters + * + * @param string $address + * @param string $port + * @param string $protocol + * @return boolean If return false then using default parameters where validation failed + */ + function setConnection($address, $port=null, $protocol='tcp'){ + $this->serverAddress = $address; + $this->serverPort = $port; + + // Validate protocol + $protocol = strtolower($protocol); + if (($protocol != 'tcp') && !self::isEncryptionProtocol($protocol)) { + $this->debugMessage('Invalid protocol ('.$protocol.'). Setting to default (tcp).'); + $this->protocol = 'tcp'; + return false; + } + $this->protocol = $protocol; + + return true; + } + + /** + * Build url for connecting to stream + * + * @return string + */ + private function getUrl() { + $url = ''; + if ($this->protocol) $url .= $this->protocol .'://'; + $url .= $this->serverAddress; + if ($this->serverPort) $url .= ':'. $this->serverPort; + return $url; + } + + /** + * Check if encryption protocol is supported + * + * @param string $protcol + * @return boolean + */ + private static function isEncryptionProtocol($protocol) { + return in_array(strtolower($protocol), ['ssl', 'tls', 'tlsv1.0', 'tlsv1.1', 'tlsv1.2', 'sslv3']); + } + + /** + * Sets server certificate and protocol for ssl/tls encryption + * + * @param string $caFile CA file to identify server + * @param string $protocl Crypto protocol (See http://php.net/manual/en/migration56.openssl.php) + * @return boolean False if settings failed, else true + */ + public function setEncryption($caFile, $protocol = null) { + if (file_exists($caFile)) { + $this->caFile = $caFile; + } else { + $this->debugMessage('CA file not found'); + return false; + } + if(self::isEncryptionProtocol($protocol)) { + $this->protocol = $protocol; + } else if (!is_null($protocol)) { + $this->debugMessage('Unknown encryption protocol'); + return false; + } + return true; + } + + /** + * Sets client crt and key files for client-side authentication + * + * @param string $crtFile Client certificate file + * @param string $keyFile Client key file + * @return boolean False if settings failed, else true + */ + public function setClientEncryption($certificateFile, $keyFile) { + if (!file_exists($certificateFile)) { + $this->debugMessage('Client certificate file not found'); + return false; + } + if (!file_exists($keyFile)) { + $this->debugMessage('Client key file not found'); + return false; + } + $this->localCert= $certificateFile; + $this->localPrivateKey = $keyFile; + return true; + } + + /** + * Set authentication details to be used when connecting + * + * @param string $username Username + * @param string $password Password + */ + public function setAuthentication($username, $password) { + $this->connectUsername= $username; + $this->connectPassword = $password; + } + + /** + * Set will (last message defined by MQTT) to send when connection is lost + * + * @param string $topic + * @param string $message + * @param integer $qos + * @param boolean $retain + */ + public function setWill($topic, $message, $qos=1, $retain=false) { + $this->connectWill = true; + $this->connectWillQos = $qos; + $this->connectWillRetain = $retain; + $this->willTopic = $topic; + $this->willMessage = $message; + } + + /** + * Connect to MQTT server + * + * @param string $clientId Unique id used by the server to identify the client + * @param boolean $cleanSession Set true to clear session on server, ie queued messages are purged (not recieved) + * @param integer $keepAlive Number of seconds a connection is considered to be alive without traffic + * @param integer $timeout Number of millliseconds before timeout when reading from socket + * @return boolean Returns false if connection failed + */ + public function sendConnect($clientId, $cleanSession=false, $keepAlive=10, $timeout=5000) { + if (!$this->serverAddress) return false; + + // Basic validation of clientid + // Note: A MQTT server may accept other chars and more than 23 chars in the clientid but that is optional, + // all chars below up to 23 chars are required to be accepted (see section "3.1.3.1 Client Identifier" of the standard) + if(preg_match("/[^0-9a-zA-Z]/",$clientId)) { + $this->debugMessage('ClientId can only contain characters 0-9,a-z,A-Z'); + return false; + } + if(strlen($clientId) > 23) { + $this->debugMessage('ClientId max length is 23 characters/numbers'); + return false; + } + $this->clientId = $clientId; + + $this->connectCleanSession = $cleanSession; + $this->connectKeepAlive = $keepAlive; + $this->socketTimeout = $timeout; + + // Setup certificates if encryption protocol selected + if ($this->isEncryptionProtocol($this->protocol)) { + $mozillaCiphers = implode(':', array( + 'ECDHE-RSA-AES128-GCM-SHA256', + 'ECDHE-ECDSA-AES128-GCM-SHA256', + 'ECDHE-RSA-AES256-GCM-SHA384', + 'ECDHE-ECDSA-AES256-GCM-SHA384', + 'DHE-RSA-AES128-GCM-SHA256', + 'DHE-DSS-AES128-GCM-SHA256', + 'kEDH+AESGCM', + 'ECDHE-RSA-AES128-SHA256', + 'ECDHE-ECDSA-AES128-SHA256', + 'ECDHE-RSA-AES128-SHA', + 'ECDHE-ECDSA-AES128-SHA', + 'ECDHE-RSA-AES256-SHA384', + 'ECDHE-ECDSA-AES256-SHA384', + 'ECDHE-RSA-AES256-SHA', + 'ECDHE-ECDSA-AES256-SHA', + 'DHE-RSA-AES128-SHA256', + 'DHE-RSA-AES128-SHA', + 'DHE-DSS-AES128-SHA256', + 'DHE-RSA-AES256-SHA256', + 'DHE-DSS-AES256-SHA', + 'DHE-RSA-AES256-SHA', + 'AES128-GCM-SHA256', + 'AES256-GCM-SHA384', + 'ECDHE-RSA-RC4-SHA', + 'ECDHE-ECDSA-RC4-SHA', + 'AES128', + 'AES256', + 'RC4-SHA', + 'HIGH', + '!aNULL', + '!eNULL', + '!EXPORT', + '!DES', + '!3DES', + '!MD5', + '!PSK' + )); + // Secure socket communication with these parameters, a ca-file is required + $options = []; + $options['verify_peer'] = true; + $options['verify_peer_name'] = true; + $options['verify_depth'] = 5; + $options['disable_compression'] = true; + $options['SNI_enabled'] = true; + $options['ciphers'] = $mozillaCiphers; + if($this->caFile) { + $options['cafile'] = $this->caFile; + } + if($this->localCert) { + $options['local_cert'] = $this->localCert; + if ($this->localPrivateKey) { + $options['local_pk'] = $this->localPrivateKey; + } + } + $socketContext = stream_context_create(['ssl' => $options]); + $this->debugMessage('Settings socket options: '. var_export($options, true)); + } else { + $socketContext = null; + } + + // Try to open socket + try { + $this->debugMessage('Opening socket to: '. $this->getUrl()); + if ($socketContext) { + $this->socket = stream_socket_client($this->getUrl(), $errno, $errstr, 10, STREAM_CLIENT_CONNECT, $socketContext); + } else { + $this->socket = stream_socket_client($this->getUrl(), $errno, $errstr, 10, STREAM_CLIENT_CONNECT); + } + } catch (\ErrorException $error) { + $this->debugMessage('Exception: Could not open stream with error message: '. $error->getMessage()); + $this->socket = null; + return false; + } + + // Check if socket was opened successfully + if ($this->socket === false) { + $this->socket = null; + $this->debugMessage('Connection failed. Error-no:'. $errno .' Error message: '. $errstr); + return false; + } + + // Set socket timeout + ini_set('default_socket_timeout', 10); + stream_set_timeout($this->socket, 0, $this->socketTimeout * 1000); + // Set stream to non-blocking mode, ie do not wait to read if stream is empty + stream_set_blocking($this->socket, true); + + // Calculate connect flags to use in CONNECT header + $connectFlags = 0; + if ($this->connectCleanSession) $connectFlags += 0x02; + if ($this->connectWill) { + $connectFlags += 0x04; + if ($this->connectWillQos) $connectFlags += ($this->connectWill << 3); + if ($this->connectWillRetain) $connectFlags += 0x20; + } + if ($this->connectUsername) { + $connectFlags += 0x80; + if ($this->connectPassword) $connectFlags += 0x40; + } + + // Build payload and header for CONNECT-packet + $payload = chr(0x00).chr(0x04); // MSB & LSB length of MQTT = 4 + $payload .= 'MQTT'; + $payload .= chr(0x04); // Protocol level (3.1.1) + $payload .= chr($connectFlags); // Connect flags + $payload .= chr($this->connectKeepAlive >> 8); // Keepalive (MSB) + $payload .= chr($this->connectKeepAlive & 0xff); // Keepalive (LSB) + if ($this->connectCleanSession && empty($this->clientId)) { + $this->clientId = rand(1,999999999); + } + if ($this->clientId) { + $payload .= $this->createPayload($this->clientId); + } + if($this->connectWill){ + $payload .= $this->createPayload($this->willTopic); + $payload .= $this->createPayload($this->willMessage); + } + if($this->connectUsername) { + $payload .= $this->createPayload($this->connectUsername); + } + if ($this->connectPassword) { + $payload .= $this->createPayload($this->connectPassword); + } + $header = $this->createHeader(self::MQTT_CONNECT, $payload); + $this->debugMessage('Sending CONNECT'); + $this->send($header . $payload); + + // Wait for CONNACK packet + $response = $this->waitForPacket(self::MQTT_CONNACK); + if($response !== false && ($response[2] == chr(0))) { + $this->debugMessage('Connected to MQTT'); + $this->lastConnectResult = 0; + return true; + } else { + $this->debugMessage('Connection failed! Error: '. ord($response[2])); + $this->lastConnectResult = ord($response[2]); + $this->close(); + return false; + } + } + + /** + * Publish a topic and message (QoS 0,1,2 supported) + * + * @param string $topic + * @param string $message + * @param byte $qos + * @return boolean + */ + public function sendPublish($topic, $message, $qos = self::MQTT_QOS1, $retain = 0) { + if(!$this->isConnected()) return false; + + if($qos!=self::MQTT_QOS0 && $qos!=self::MQTT_QOS1 && $qos!=self::MQTT_QOS2) return false; + + $packetId = $this->getNextPacketId(); + $payload = $this->createPayload($topic); + if($qos >= self::MQTT_QOS1) { + // Packet identifier required for QoS level >= 1 + $payload .= $this->getPacketIdPayload(); + } + $payload .= $message; + + $dupFlag = 0; + $header = $this->createHeader(self::MQTT_PUBLISH + ($dupFlag<<3) + ($qos<<1) + $retain, $payload); + $this->debugMessage('Sending PUBLISH'); + $this->send($header . $payload); + + if($qos == self::MQTT_QOS1) { + // If QoS level 1, only a PUBACK packet is expected + $response = $this->waitForPacket(self::MQTT_PUBACK, $packetId); + if($response === false) { + $this->debugMessage('Packet missing, expecting PUBACK'); + return false; + } + } elseif($qos == self::MQTT_QOS2) { + // If QoS level 2, a PUBREC packet is expected + $response = $this->waitForPacket(self::MQTT_PUBREC, $packetId); + if($response === false) { + $this->debugMessage('Packet missing, expecting PUBREC'); + return false; + } + + // Send PUBREL + $response = $this->sendPubRel($packetId); + if($response === false) { + $this->debugMessage('Failed to send PUBREL'); + return false; + } + + // A PUBCOMP packet is expected + $response = $this->waitForPacket(self::MQTT_PUBCOMP, $packetId); + if($response === false) { + $this->debugMessage('Packet missing, expecting PUBCOMP'); + return false; + } + } + + return true; + } + + /** + * Send PUBACK as response to a recieved PUBLISH packet (QoS Level 1) + * + * @param integer $packetId Packet identifier of PUBLISH packet + * @return boolean Returns true if packet sent successfully + */ + public function sendPubAck($packetId) { + if(!$this->isConnected()) return false; + + $payload = chr(($packetId & 0xff00)>>8) . chr($packetId & 0xff); + $header = $this->createHeader(self::MQTT_PUBACK, $payload); + $this->debugMessage('Sending PUBACK'); + $this->send($header . $payload); + + return true; + } + + /** + * Send PUBREC as response to a recieved PUBLISH packet (QoS Level 2) + * + * @param integer $packetId Packet identifier of PUBLISH packet + * @return boolean Returns true if packet sent successfully + */ + public function sendPubRec($packetId) { + if(!$this->isConnected()) return false; + + $payload = chr(($packetId & 0xff00)>>8) . chr($packetId & 0xff); + $header = $this->createHeader(self::MQTT_PUBREC, $payload); + $this->debugMessage('Sending PUBREC'); + $this->send($header . $payload); + + return true; + } + + /** + * Send PUBREL as response to a recieved PUBREC packet (QoS Level 2) + * + * @param integer $packetId Packet identifier of PUBLISH packet + * @return boolean Returns true if packet sent successfully + */ + public function sendPubRel($packetId) { + if(!$this->isConnected()) return false; + + $payload = chr(($packetId & 0xff00)>>8) . chr($packetId & 0xff); + $header = $this->createHeader(self::MQTT_PUBREL, $payload); + $this->debugMessage('Sending PUBREL'); + $this->send($header . $payload); + + return true; + } + + /** + * Send PUBCOMP as response to a recieved PUBREL packet (QoS Level 2) + * + * @param integer $packetId Packet identifier of PUBLISH packet + * @return boolean Returns true if packet sent successfully + */ + public function sendPubComp($packetId) { + if(!$this->isConnected()) return false; + + $payload = chr(($packetId & 0xff00)>>8) . chr($packetId & 0xff); + $header = $this->createHeader(self::MQTT_PUBCOMP, $payload); + $this->debugMessage('Sending PUBCOMP'); + $this->send($header . $payload); + + return true; + } + + /** + * Subscribe to topics with a quality of service + * + * @param string[] $topics Topics to subscribe for + * @param integer $qos Quality of serivce for all topics + * @return boolean Returns true if SUBACK was recieved + */ + public function sendSubscribe($topics, $qos = self::MQTT_QOS1) { + if (!is_array($topics)) $topics = [$topics]; + if(!$this->isConnected()) return false; + + $packetId = $this->getNextPacketId(); + $payload = $this->getPacketIdPayload(); + foreach($topics as $topic) { + $payload .= $this->createPayload($topic); + $payload .= chr($qos); + } + $header = $this->createHeader(self::MQTT_SUBSCRIBE + 0x02, $payload); + $this->debugMessage('Sending SUBSCRIBE'); + $this->send($header . $payload); + + // A SUBACK packet is expected + $response = $this->waitForPacket(self::MQTT_SUBACK, $packetId); + if($response === false) { + $this->debugMessage('Packet missing, expecting SUBACK'); + return false; + } + $responsePayload = substr($response, 3); // skip header and identifier (3 bytes) + if (strlen($responsePayload) != count($topics)) { + $this->debugMessage('Did not recieve SUBACK for all topics'); + return false; + } + + // Check which subscriptions that were approved + $topicsResult = []; + $i = 0; + foreach ($topics as $topic) { + $topicsResult[$topic] = []; + if ($responsePayload[$i] > 0x02) { + $topicsResult[$topic]['success'] = false; + $topicsResult[$topic]['qosGiven'] = null; + } else { + $topicsResult[$topic]['success'] = true; + $topicsResult[$topic]['qosGiven'] = (int) ord($responsePayload[$i]); + } + $i++; + } + + return $topicsResult; + } + + /** + * Send unsubscribe packet for given topics + * + * @param string[] $topics + * @return boolean Returns true if UNSUBACK was recieved + */ + public function sendUnsubscribe($topics) { + if(!$this->isConnected()) return false; + + $packetId = $this->getNextPacketId(); + $payload = $this->getPacketIdPayload(); + foreach($topics as $topic) { + $payload .= $this->createPayload($topic); + } + $header = $this->createHeader(self::MQTT_UNSUBSCRIBE + 0x02, $payload); + $this->debugMessage('Sending UNSUBSCRIBE'); + $this->send($header . $payload); + + // An UNSUBACK packet is expected + $response = $this->waitForPacket(self::MQTT_UNSUBACK, $packetId); + if($response === false) { + $this->debugMessage('Invalid packet received, expecting UNSUBACK'); + return false; + } + return true; + } + + /** + * Sends PINGREQ packet to server + * + * @return boolean Returns true if PINGRESP was recieved + */ + public function sendPing() { + if(!$this->isConnected()) return false; + + $this->timeSincePingReq = time(); + $header = $this->createHeader(self::MQTT_PINGREQ); + $this->debugMessage('Sending PING'); + $this->send($header); + $this->pingReqTime = time(); + + // A PINGRESP packet is expected + $response = $this->waitForPacket(self::MQTT_PINGRESP); + if($response === false) { + $this->debugMessage('Invalid packet received, expecting PINGRESP'); + return false; + } + + return true; + } + + /** + * Send disconnect and close socket + */ + public function sendDisconnect() { + if($this->isConnected()) { + $header = $this->createHeader(self::MQTT_DISCONNECT); + $this->debugMessage('Sending DISCONNECT'); + $this->send($header); + $this->close(); + } + } + + /** + * Close socket + */ + public function close() { + if($this->isConnected()) { + $this->debugMessage('Closing socket'); + stream_socket_shutdown($this->socket, STREAM_SHUT_RDWR); + $this->socket = null; + $this->serverAliveTime = null; + } + } + + /** + * Check if connected to stream + * @return boolean + */ + public function isConnected() { + return !empty($this->socket); + } + + /** + * Check if connection is alive + * @return boolean + */ + public function isAlive() { + return $this->isConnected() && ($this->serverAliveTime + $this->connectKeepAlive <= time()); + } + + /** + * Set debug mode, if true then output progress messages + * + * @param boolean $mode + */ + public function setDebug($mode = true) { + $this->debug = $mode; + } + + /** + * Print message to console if debug mode is on + * + * @param string $message + */ + private function debugMessage($message) { + if ($this->debug) { + echo 'MQTT: '. $message .PHP_EOL; + } + } + + /** + * Return next packet identifier to use in MQTT packet + * Max 2 bytes to be used, restart on 0 if end reached + * + * @return integer + */ + private function getNextPacketId() { + return ($this->packetId = ($this->packetId + 1) & 0xffff); + } + + /** + * Return payload of packet id, use latest generated packet id as default + * + * @param integer $packetId + * @return string Two chars with apyload to add in MQTT-message + */ + private function getPacketIdPayload($packetId = null) { + if (empty($packetId)) $packetId = $this->packetId; + return chr(($packetId & 0xff00)>>8) . chr($packetId & 0xff); + } + + /** + * Add payload length as bytes to begining of string and return + * + * @param string $payload + * @return string + */ + private function createPayload($payload) { + $fullLength = strlen($payload); + $retval = chr($fullLength>>8).chr($fullLength&0xff).$payload; + return $retval; + } + + /** + * Decode payload using inital length (2 bytes) and return as string array + * + * @param string $payload + * @return string[] + */ + private function decodePayload($payload) { + $result = []; + while (strlen($payload) >= 2) { + $length = ord($payload[0])<<8 + ord($payload[1]); + if (strlen($payload) <= $length + 2) { + $result[] = substr($payload, 2, $length); + } + $payload = substr($payload, min($length + 2, strlen($payload))); + } + return $result; + } + + /** + * Send data to open socket + * + * @param string $data + * @return boolean Only returns true if all data was sent + */ + private function send($data) { + if ($this->socket) { + $result = fwrite($this->socket, $data); + if (($result !== false) && ($result == strlen($data))) { + $this->serverAliveTime = time(); + return true; + } + } + return false; + } + + /** + * Read bytes from socket until x bytes read, eof reached or socket timeout + * + * @param int $bytes Number of bytes to read + * @return string Return bytes read as a string + */ + private function readBytes($bytes) { + if (!$this->socket) return false; + //if ($bytes == 0) return ''; + $bytesLeft = $bytes; + $result = ''; + do { + // If stream at end, close down socket and exit + if(feof($this->socket)) { + $this->debugMessage('Reached EOF for stream'); + $this->close(); + return $result; + } + // Try to read from stream + $str = fread($this->socket, $bytesLeft); + if ($str !== false && strlen($str) > 0) { + $result .= $str; + $bytesLeft -= strlen($str); + } + if ($bytesLeft <= 0) { + // If all bytes read, then return them + $this->serverAliveTime = time(); + return $result; + } + // Check if timeout + $info = stream_get_meta_data($this->socket); + if ($info['timed_out']) { + $this->debugMessage('Read timeout'); + return false; + } + // Wait a while before trying to read again (in micro seconds) + usleep($this->socketReadDelay * 1000); + } while (true); + } + + /** + * Encode length to bytes to send in stream + * + * @param integer $len + * @return string + */ + private function encodeLength($len) { + if ($len < 0 || $len >= 128*128*128*128) { + // illegal length + return false; + } + $output = ''; + do { + $byte = $len & 0x7f; // keep lowest 7 bits + $len = $len >> 7; // shift away lowest 7 bits + if ($len > 0) { + $byte = $byte | 0x80; // set high bit to indicate continuation + } + $output .= chr($byte); + } while ($len > 0); + return $output; + } + + /** + * Return length of packet by reading from stream + * + * @return integer + */ + private function readPacketLength() { + $bytesRead = 0; + $len = 0; + $multiplier = 1; + do { + if ($bytesRead > 4) { + return false; // Malformed length + } + $str = $this->readBytes(1); + if ($str === false || strlen($str) != 1) { + return false; // Unexpected end of stream + } + $byte = ord($str[0]); + $len += ($byte & 0x7f) * $multiplier; + $isContinued = ($byte & 0x80); + if ($isContinued) { + $multiplier *= 128; + } + $bytesRead++; + } while ($isContinued); + return $len; + } + + /** + * Create MQTT header from command and payload + * + * @param int $command Command to send + * @param string $payload Payload to be sent + * + * @return string Header to send + */ + private function createHeader($command, $payload = '') { + return chr($command) . $this->encodeLength(strlen($payload)); + } + + /** + * Read next packet from stream + * + * @return boolean + */ + private function readNextPacket() { + do { + $header = $this->readBytes(1); + if ($header === false) { + $this->lastReadStatus = self::READ_STATUS_ERROR_HEADER; + return false; + } + } while ((ord($header)&0xf0) == 0); // 0 is illegal control code to start with + + $packetLength = $this->readPacketLength(); + if ($packetLength === false) { + $this->debugMessage('Could not decode packet length'); + $this->lastReadStatus = self::READ_STATUS_ERROR_PACKETLENGTH; + return false; + } + + $payload = $packetLength > 0 ? $this->readBytes($packetLength) : ''; + if ($payload === false) { + $this->lastReadStatus = self::READ_STATUS_ERROR_PAYLOAD; + return false; + } + $this->debugMessage('Packet response: '. self::str2hex($header . $payload)); + $this->lastReadStatus = self::READ_STATUS_OK; + return $header . $payload; + } + + public function getLastReadStatus() { + return $this->lastReadStatus; + } + + public function hasMoreToRead() { + return ($this->lastReadStatus == self::READ_STATUS_OK) && $this->isConnected(); + } + + /** + * Read packets from stream and save to queue. Quit after x packets or timeout. + * + * @param integer $maxPackets Packet id the message must match + * @return integer Number of packets read + */ + private function readPackets($maxPackets = 100) { + $receivedPackets = 0; + while (($receivedPackets < $maxPackets) && ($packet = $this->readNextPacket()) !== false) { + $this->packetQueue[] = $packet; + $receivedPackets++; + } + return $receivedPackets; + } + + /** + * Wait until a certain packet is found in the stream. + * Save other recieved packets in queue. + * + * @param byte $header Header to look for (only 4 high bits) 0xf0 + * @param integer $verifyPacketId Packet id the message must match + * @return boolean + */ + private function waitForPacket($header, $verifyPacketId = false) { + // first check unhandled packets + foreach ($this->packetQueue as $key => $packet) { + if ($this->isPacketVerified($packet, $header, $verifyPacketId)) { + // if found, remove from queue and return packet + unset($this->packetQueue[$key]); + return $packet; + } + } + // if not found in queue, start reading from stream until found or timeout + do { + $packet = $this->readNextPacket(); + if ($packet === false || empty($packet)) return false; + if ($this->isPacketVerified($packet, $header, $verifyPacketId)) { + return $packet; + } + // another packet found, save it to queue + $this->packetQueue[] = $packet; + } while(true); + } + + /** + * Check if packet is of a given type and packet id match latest sent packet id + * + * @param string $packet + * @param char $header + * @param integer $verifyPacketId + * @return boolean + */ + private function isPacketVerified($packet, $header, $verifyPacketId = false) { + if (is_string($packet) && strlen($packet) >= 1) { + if ((int)(ord($packet[0])&0xf0) == (int)($header&0xf0)) { + if ($verifyPacketId === false) return true; + if (strlen($packet) >= 3) { + $receivedPacketId = (int)(ord($packet[1])<<8) + ord($packet[2]); + if($verifyPacketId == $receivedPacketId) { + return true; + } + } + } + } + return false; + } + + /** + * Get packets matching a header from the queue and remove from queue + * + * @param char $header + * @return string[] + */ + public function getQueuePackets($header) { + $foundPackets = []; + foreach ($this->packetQueue as $key => $packet) { + if ($this->isPacketVerified($packet, $header)) { + $foundPackets[] = $packet; + unset($this->packetQueue[$key]); + } + } + return $foundPackets; + } + + /** + * Get PUBLISH packets and return them as messages + * + * @param integer $maxMessages Max messages to read + * @param boolean $sendPubAck If true, then send PUBACK to MQTT-server (QoS 1) + * @param boolean $sendPubRec If true, then send PUBREC to MQTT-server, wait for PUBREL and send PUBCOMP (QoS 2) + * @return string[] All PUBLISH messages which were confirmed or no confirmation needed/wanted + */ + public function getPublishMessages($maxMessages = 100, $sendPubAck = false, $sendPubRec = false) { + $packetsRead = $this->readPackets($maxMessages); + $packets = $this->getQueuePackets(self::MQTT_PUBLISH); + $messages = []; + foreach ($packets as $key => $packet) { + $message = $this->decodePublish($packet); + if ($message === false) { + $this->debugMessage('Message could not be decoded'); + continue; + } + + if ($sendPubAck && ($message['qos'] == self::MQTT_QOS1)) { + if($this->sendPubAck($message['packetId']) === false) { + $this->debugMessage('Failed to send PUBACK'); + continue; + } + } elseif ($sendPubRec && ($message['qos'] == self::MQTT_QOS2)) { + // Send PUBREC + if($this->sendPubRec($message['packetId']) === false) { + $this->debugMessage('Failed to send PUBREC'); + continue; + } + // A PUBREL packet is expected + $response = $this->waitForPacket(self::MQTT_PUBREL, $message['packetId']); + if($response === false) { + $this->debugMessage('Packet missing, expecting PUBREL'); + continue; + } + // Send PUBCOMP + if($this->sendPubComp($message['packetId']) === false) { + $this->debugMessage('Failed to send PUBCOMP'); + continue; + } + } + + // Package was successfully confirmed or no confirmation needed/wanted --> store it + $messages[] = $message; + } + return $messages; + } + + /** + * Decode a publish packet to its attributes + * + * @param string $packet + * @return array|boolean Return message or false if decode failed + */ + public function decodePublish($packet) { + if (!is_string($packet) || (strlen($packet) <= 3)) { + return false; + } + $flags = ord($packet[0]) & 0x0f; + $duplicate = ($flags == 0x80); + $retain = ($flags == 0x01); + $qos = ($flags>>1) & 0x03; + $topicLength = (ord($packet[1])<<8) + ord($packet[2]); + $topic = substr($packet, 3, $topicLength); + + $payload = substr($packet, 3 + $topicLength); // Get the payload of the packet + if ($qos == 0) { + // no packet id for QoS 0, the payload is the message + $message = $payload; + $packetId = NULL; + } else { + if (strlen($payload) >= 2) { + $packetId = (ord($payload[0])<<8) + ord($payload[1]); + $message = substr($payload, 2); // skip packet id (2 bytes) for QoS 1 and 2 + } else { + // 2 byte packet id required, but not found. exit gracefully (no failure) + $packetId = NULL; + $message = ''; + } + } + return [ + 'topic' => self::convertActiveMqTopic($topic), + 'message' => $message, + 'retain' => $retain, + 'duplicate' => $duplicate, + 'qos' => $qos, + 'packetId' => $packetId, + ]; + } + + /** + * Replace ActiveMQ special characters to MQTT-standard + * + * @param string $topic + * @return string + */ + private static function convertActiveMqTopic($topic) { + $topic = str_replace(".","/", $topic); + $topic = str_replace("*","+", $topic); + $topic = str_replace(">","#", $topic); + return $topic; + } + + /** + * Return a string interpreted as hex and ASCII (between 0x20-0x7f) + * Good for displaying recieved packets + * + * @param string $str + * @return string + */ + private function str2hex($str) { + $hex = ''; + $ascii = ''; + for ($i=0; $i= 0x20 && ord($char) <= 0x7f) { + $ascii .= $char; + } else { + $ascii .= '.'; + } + $hex .= dechex(ord($char)).' '; + } + return $hex . '"'. $ascii .'"'; + } + + public function dumpQueue() { + foreach ($this->packetQueue as $packet) { + $this->str2hex($packet) . PHP_EOL; + } + } +} \ No newline at end of file diff --git a/app/Middleware/Auth/ApiMiddleware.php b/app/Middleware/Auth/ApiMiddleware.php index ce06bff..44cf3fc 100644 --- a/app/Middleware/Auth/ApiMiddleware.php +++ b/app/Middleware/Auth/ApiMiddleware.php @@ -39,7 +39,7 @@ class ApiMiddleware implements MiddlewareInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - if (env('APP_ENV') == 'dev') { + if (env('APP_ENV') == 'dev' || env('APP_ENV') == 'local') { return $handler->handle($request); } diff --git a/app/Model/Coupon.php b/app/Model/Coupon.php index b6d984d..e50574b 100644 --- a/app/Model/Coupon.php +++ b/app/Model/Coupon.php @@ -6,6 +6,9 @@ use App\Model\Model; class Coupon extends Model { + const DISCOUNT_TYPE_CASH = 1; + const DISCOUNT_TYPE_RATE = 2; + protected $table = 'ims_system_coupon_user'; } diff --git a/app/Model/CouponUserRec.php b/app/Model/CouponUserRec.php new file mode 100644 index 0000000..c5c2608 --- /dev/null +++ b/app/Model/CouponUserRec.php @@ -0,0 +1,10 @@ +belongsTo(Goods::class, 'good_id', 'id'); + } + +} \ No newline at end of file diff --git a/app/Request/OrderOnlineRequest.php b/app/Request/OrderOnlineRequest.php new file mode 100644 index 0000000..dfe0ae6 --- /dev/null +++ b/app/Request/OrderOnlineRequest.php @@ -0,0 +1,72 @@ + 'nonempty', + 'delivery_no' => '', + 'dada_fee' => 'nonempty', + 'market_id' => 'required|nonempty|integer', + 'user_id' => 'required|nonempty|integer', + 'money' => 'required|nonempty', + 'box_money' => '', + 'ps_money' => '', + 'mj_money' => '', + 'xyh_money' => '', + 'yhq_money' => '', + 'yhq_money2' => '', + 'zk_money' => '', + 'tel' => 'required|nonempty', + 'name' => 'required|nonempty', + 'address' => 'required|nonempty', + 'area' => '', + 'lat' => 'required|nonempty', + 'lng' => 'required|nonempty', + 'note' => '', + 'type' => 'required|nonempty', + 'form_id' => '', + 'form_id2' => '', + 'delivery_time' => '', + 'order_type' => 'nonempty', + 'pay_type' => 'nonempty', + 'coupon_id' => '', + 'coupon_id2' => '', + 'uniacid' => 'nonempty', + 'store_list' => 'nonempty', + 'receive_coupon_ids' => '', + ]; + } + + public function messages(): array + { + return [ + '*.*' => ':attribute 参数异常' + ]; + } + + public function attributes(): array + { + return [ + + ]; + } +} diff --git a/app/Service/CoupnoServiceInterface.php b/app/Service/CoupnoServiceInterface.php new file mode 100644 index 0000000..4e3d9a4 --- /dev/null +++ b/app/Service/CoupnoServiceInterface.php @@ -0,0 +1,20 @@ +get(Redis::class); + $couponTodayUsedIds = $redis->sMembers('coupon_'.date('Ymd').'_used_'.$userId); + + $currentTime = time(); + + $builder = Db::table('ims_system_coupon_user_receive as receive') + ->join('ims_system_coupon_user as coupon', 'coupon.id', '=', 'receive.system_coupon_user_id', 'inner'); + + if (is_array($fields)&&!empty($fields)) { + $builder->select([ + 'receive.id as receive_id', + 'receive.user_id', + 'receive.number_remain', + 'coupon.id', + 'coupon.title', + 'coupon.full_amount', + 'coupon.discounts', + 'coupon.usable_start_time', + 'coupon.usable_end_time', + 'coupon.discount_type' + ]); + } + + if (is_array($couponTodayUsedIds)&&!empty($couponTodayUsedIds)) { + $builder->whereNotIn('coupon.id', $couponTodayUsedIds); + } + + return $builder->where(['receive.user_id' => $userId]) + ->whereIn('receive.status', [0,1]) + ->where('receive.number_remain', '>', 0) + ->whereIn('coupon.type', [1,$type]) + ->where('coupon.full_amount', '<=', $orderAmount) + ->where('coupon.usable_start_time', '<=', $currentTime) + ->where('coupon.usable_end_time', '>=', $currentTime) + ->where('coupon.usable_number', '<=', Db::raw('receive.number_remain')) + ->where('coupon.market_id', 'in', [0, $marketId]) + ->whereIn('coupon.storetype_id', $storeTypeIds) + ->orderByRaw('coupon.discounts DESC, coupon.full_amount DESC') + ->get() + ->toArray(); + } + + /** + * 缓存优惠券今日使用情况 + * @param $userId + * @param $couponId + * @param $couponRecId + * @return bool + */ + function cacheTodayCouponUsed($userId, $couponId, $couponRecId) + { + + $redis = ApplicationContext::getContainer()->get(Redis::class); + + $setRes = $redis->sAdd( + 'coupon_'.date('Ymd').'_used_'.$userId, + $couponId + ); + + $expireRes = $redis->expire( + 'coupon_'.date('Ymd').'_used_'.$userId, + strtotime(date('Y-m-d').' 23:59:59')-time() + ); + + return $setRes&&$expireRes; + } +} \ No newline at end of file diff --git a/app/Service/OrderService.php b/app/Service/OrderService.php new file mode 100644 index 0000000..e16d2cb --- /dev/null +++ b/app/Service/OrderService.php @@ -0,0 +1,423 @@ +existsByOrderNum($data['order_num'])) { + return $orderMainId; + } + + Db::beginTransaction(); + try { + + // 计算当前订单可用红包优惠金额 + $couponMoney = 0; + if (isset($data['receive_coupon_ids'])&&$data['receive_coupon_ids']) { + $receiveCouponIds = explode(',', str_replace(',',',',$data['receive_coupon_ids'])); + $couponMoney = $this->getCouponAmount($receiveCouponIds, $data['money'], $data['user_id'], $data['market_id']); + } + $dataMain['yhq_money2'] = $couponMoney; + + // 获取分布式全局ID + $generator = ApplicationContext::getContainer()->get(IdGeneratorInterface::class); + $dataMain['global_order_id'] = $generator->generate(); + + // 店铺IDs + $dataMain['store_ids'] = ''; + $storeList = json_decode(json_encode($data['store_list']), true); + if (!is_array($storeList)||empty($storeList)) { + Db::rollBack(); + return '订单中商品不存在或已失效'; + } + + // 获取商户IDs + foreach ($storeList as &$item) { + $dataMain['store_ids'] .= empty($dataMain['store_ids']) ? $item['store_id'] : ','.$item['store_id']; + } + + // 主订单插入数据 + $currentTime = time(); + $dataMain['time'] = date('Y-m-d H:i:s', $currentTime); + $dataMain['time_add'] = $currentTime; + $dataMain['state'] = OrderMain::ORDER_STATE_UNPAY; + $dataMain['code'] = $dataMain['global_order_id']; + + // 主订单模型保存 + $orderMain = OrderMain::create($dataMain); + $orderMainId = $orderMain->id; + + // 统计订单中所有店铺当日订单数,做店铺订单序号 + $countsArr = Order::query()->select('COUNT(*) AS count, id') + ->whereIn('store_id', explode(',', $dataMain['store_ids'])) + ->where(['type' => OrderMain::ORDER_TYPE_ONLINE]) + ->whereBetween('time', [date('Y-m-d 00:00:00'), date('Y-m-d 23:59:59')]) + ->get() + ->toArray(); + + $storeOrderCounts = []; + foreach ($countsArr as $key => &$row) { + $storeOrderCounts[$row['id']] = $row['count']; + } + + // 循环处理订单总额、子订单总额、商品、商户订单等信息 + $orderAmountTotal = 0; # 总订单金额 + foreach ($storeList as $key => &$item) { + + // 子订单数据处理 + $dataChild = [ + 'uniacid' => $data['uniacid'], + 'order_num' => 's'.date('YmdHis', time()) . rand(1111, 9999), + 'user_id' => $orderMain->user_id, + 'store_id' => $item['store_id'], + 'order_main_id' => $orderMain, + 'state' => OrderMain::ORDER_STATE_UNPAY, + 'tel' => $orderMain->tel, + 'name' => $orderMain->name, + 'address' => $orderMain->address, + 'area' => $orderMain->area, + 'time' => date("Y-m-d H:i:s"), + 'note' => $item['note'], + 'delivery_time' => $orderMain->delivery_time, + 'type' => $orderMain->type, + 'lat' => $orderMain->lat, + 'lng' => $orderMain->lng, + 'pay_type' => $orderMain->pay_type, + 'order_type' => $orderMain->order_type, + 'money' => floatval($item['subtotal']), + 'box_money' => floatval($item['box_money']), + 'mj_money' => floatval($item['mj_money']), + 'yhq_money' => floatval($item['yhq_money']), + 'yhq_money2' => floatval($item['yhq_money2']), + 'zk_money' => floatval($item['zk_money']), + 'coupon_id' => $item['coupon_id'], + 'coupon_id2' => $item['coupon_id2'], + 'xyh_money' => floatval($item['xyh_money']), + 'oid' => (isset($storeOrderCounts[$item['store_id']]) ? $item['store_id'] : 0) + 1, + 'time_add' => date("Y-m-d H:i:s"), + ]; + + $order = Order::create($dataChild); + $orderChildId = $order->id; + + // 子订单内商品处理 + $goodsAmountTotal = 0; + $orderGoods = []; + if (!is_array($item['good_list'])||empty($item['good_list'])) { + Db::rollBack(); + return '订单商品异常'; + } + foreach ($item['good_list'] as &$goods) { + + $goodsAmount = bcadd(floatval($goods['money']), floatval($goods['box_money'])); + $goodsAmount = bcmul($goodsAmount, $goods['num']); + $goodsAmountTotal = bcadd($goodsAmountTotal, $goodsAmount); + + $orderGoods[$goods['id']] = $goods; + $orderGoods[$goods['id']]['uniacid'] = $data['uniacid']; + $orderGoods[$goods['id']]['order_id'] = $orderChildId; + $orderGoods[$goods['id']]['user_id'] = $dataMain['user_id']; + $orderGoods[$goods['id']]['store_id'] = $item['store_id']; + } + + // 子订单优惠总额 + $discountAmountTotal = bcadd($dataChild['mj_money'], $dataChild['yhq_money']); + $discountAmountTotal = bcadd($discountAmountTotal, $dataChild['yhq_money2']); + $discountAmountTotal = bcadd($discountAmountTotal, $dataChild['zk_money']); + $discountAmountTotal = bcadd($discountAmountTotal, $dataChild['xyh_money']); + + $goodsAmountTotal = bcsub($goodsAmountTotal, $discountAmountTotal, 2); + $orderAmountTotal = bcadd($orderAmountTotal, $goodsAmountTotal, 2); + + // 校验子订单金额 + if ($goodsAmountTotal != $dataChild['money']) { + Db::rollBack(); + return '店铺订单总金额错误'; + } + + } + + // 校验库存 + foreach ($orderGoods as $Key=>&$goodsItem) { + + $goodsItem['combination_id'] = intval($goodsItem['combination_id']); + + // 存在规格,则去规格处查库存,整个接口还有很多别的问题,目前 + $goods = (object)[]; + if ($goodsItem['combination_id'] > 0) { + + $goods = SpecCombination::query() + ->select('id, number AS inventory') + ->where(['id' => $goodsItem['combination_id']]) + ->first(); + $goods->name = $goods->goods->name; + $goods->is_max = $goods->goods->is_max; + + } else { + + $goods = Goods::query() + ->select('id, name, is_max, inventory') + ->where(['id' => $goodsItem['good_id']]) + ->first(); + + } + + if (!$goods) { + Db::rollBack(); + return '缺少商品'; + } + + if($goodsItem['num'] > $goods->inventory && $goods->is_max != Goods::INVENTORY_NOLIMIT){ + Db::rollBack(); + return '商品 '.$goods->name.' 库存不足!'; + } + + } + + // 校验总订单金额 + $deliveryAmount = 0; # 配送费用 + if($dataMain['order_type'] == OrderMain::ORDER_TYPE_ONLINE){ + $deliveryAmount = $dataMain['dada_fee']; + } + + $orderAmountTotal = bcadd($orderAmountTotal, $deliveryAmount); + # 总订单优惠总额 + $discountAmountTotal = bcadd($dataMain['mj_money'], $dataMain['yhq_money']); + $discountAmountTotal = bcadd($discountAmountTotal, $dataMain['yhq_money2']); + $discountAmountTotal = bcadd($discountAmountTotal, $dataMain['zk_money']); + $discountAmountTotal = bcadd($discountAmountTotal, $dataMain['xyh_money']); + $orderAmountTotal = bcsub($orderAmountTotal, $discountAmountTotal, 2); + + if ($orderAmountTotal != bcsub(bcadd($dataMain['money'], $deliveryAmount), $discountAmountTotal, 2)) { + Db::rollBack(); + return '订单总金额错误'; + } + + // 添加订单商品 + $tempGoods = $orderGoods; + $orderGoods = []; + foreach ($tempGoods as $key => &$value) { + $goods['good_id'] = $value['good_id']; + $goods['img'] = $value['logo']; + $goods['number'] = $value['num']; + $goods['order_id'] = $value['order_id']; + $goods['name'] = $value['name']; + $goods['money'] = $value['money']; + $goods['dishes_id'] = $value['dishes_id']; + $goods['spec'] = $value['spec']; + $goods['is_qg'] = $value['is_qg']; + $goods['good_unit'] = $value['good_unit']; + $goods['uniacid'] = $value['uniacid']; + $goods['combination_id'] = $value['combination_id']; + $orderGoods[] = $goods; + } + + $addOrderGoods = OrderGoods::query()->insert($orderGoods); + if (!$addOrderGoods) { + Db::rollBack(); + return '订单商品异常'; + } + + // 修改总订单金额,金额是计算来的 + // TODO 这部分其实可以结合处理优化一下,循环前后关联处理太多 + $updateOrderMain = OrderMain::query()->where(['id' => $orderMainId])->update(['money' => $orderAmountTotal, 'total_money' => $dataMain['money']]); + if (!$updateOrderMain) { + Db::rollBack(); + return '订单总金额记录失败'; + } + + // 处理红包的使用 + $canUseConpons = $this->couponService->getOrderCanUseCoupons( + $data['money'], + $data['market_id'], + $data['user_id'], + [ + 'receive.id', + 'receive.user_id', + 'receive.number', + 'receive.number_remain', + 'receive.system_coupon_user_id', + 'coupon.discounts', + 'coupon.discount_type', + ] + ); + + if (is_array($canUseConpons)&&!empty($canUseConpons)) { + # 使用记录、更新当前优惠券 + foreach ($canUseConpons as $key => &$coupon) { + $couponUse = [ + 'user_id' => $coupon['user_id'], + 'user_receive_id' => $coupon['id'], + 'system_coupon_id' => $coupon['system_coupon_user_id'], + 'order_main_id' => $orderMainId, + 'use_time' => $currentTime, + 'return_time' => 0, + 'number' => 1, + 'status' => 1, + 'update_time' => 0, + ]; + + $insertRes = CouponUserUse::query()->insert($couponUse); + + if ($insertRes) { + $numberRemain = $coupon['number_remain'] - 1; + if ($numberRemain == 0) { + $status = 2; + } elseif ($numberRemain > 0 && $numberRemain < $coupon['number']) { + $status = 1; + } elseif ($numberRemain == $coupon['number']) { + $status = 0; + } + + $upRes = CouponUserRec::query()->where(['id' => $coupon['id']])->update(['number_remain' => $numberRemain, 'status' => $status]); + + if (!$upRes) { + Db::rollBack(); + return '优惠券使用失败'; + } + + // 缓存使用记录 + $usedRes = $this->couponService->cacheTodayCouponUsed($coupon['user_id'], $coupon['system_coupon_user_id'], $coupon['id']); + if (!$usedRes) { + Db::rollBack(); + return '优惠券使用失败'; + } + + } else { + Db::rollBack(); + return '优惠券使用失败'; + } + + } + } + + Db::commit(); + + + } catch (Exception $e) { + + Db::rollBack(); + return $e->getMessage(); + + } + + // 订单成功后处理 + if ($orderMainId) { + + // 处理喇叭播报 + + + } + + return $orderMainId; + } + + /** + * @inheritDoc + */ + public function addOfflineOrder() + { + // TODO: Implement addOfflineOrder() method. + } + + + + /** + * 计算和校验当前订单可用红包及金额 + * @param $couponIds + * @param $orderAmount + * @param $userId + * @param $marketId + * @throws Exception + */ + protected function getCouponAmount($couponIds, $orderAmount, $userId, $marketId) + { + + // 用户当前订单可用优惠券 + $couponsCanUse = $this->couponService->getOrderCanUseCoupons( + $orderAmount, + $marketId, + $userId, + [ + 'receive.id', + 'receive.user_id', + 'receive.number', + 'receive.number_remain', + 'receive.system_coupon_user_id', + 'coupon.discounts', + 'coupon.discount_type', + ] + ); + + $couponCanUseIds = array_column($couponsCanUse, 'id'); + $couponCanUseIds = array_intersect($couponCanUseIds, $couponIds); + $couponCannotUseIds = array_diff($couponIds, $couponCanUseIds); + + if (empty($couponCanUseIds)||!empty($couponCannotUseIds)) { + throw new Exception('您的订单中有优惠券已经失效'); + } + + // 计算红包折扣金额 + $couponMoney = 0; + foreach ($couponsCanUse as $key => $coupon) { + + if (!in_array($coupon->id, $couponIds)) { + continue; + } + + if ($coupon->discount_type == Coupon::DISCOUNT_TYPE_CASH) { + $couponMoney = bcadd($couponMoney, $coupon->discounts, 2); + } elseif ($coupon->discount_type == Coupon::DISCOUNT_TYPE_RATE) { + $discountRate = bcdiv($coupon->discounts,10); + $discountRate = bcsub(1,$discountRate); + $discountMoney = bcmul($orderAmount, $discountRate); + $couponMoney = bcadd($couponMoney, $discountMoney, 2); + } + } + + return $couponMoney; + + } + + /** + * 订单是否存在 + * @param $orderNum + * @return \Hyperf\Utils\HigherOrderTapProxy|mixed|void|null + */ + public function existsByOrderNum($orderNum) + { + return OrderMain::query()->where(['order_num' => $orderNum])->value('id'); + } +} \ No newline at end of file diff --git a/app/Service/OrderServiceInterface.php b/app/Service/OrderServiceInterface.php new file mode 100644 index 0000000..08ee815 --- /dev/null +++ b/app/Service/OrderServiceInterface.php @@ -0,0 +1,29 @@ +mqttClient = new MQTTClient(env('MQTT_HOST'), env('MQTT_PORT')); + $this->mqttClient->setAuthentication(env('MQTT_NAME'), env('MQTT_PASS')); + + if (env('MQTT_CERT')) { + $this->mqttClient->setEncryption(env('MQTT_CERT')); + } + + return $this->mqttClient; + } +} \ No newline at end of file diff --git a/composer.json b/composer.json index 1ac7889..d8f86c6 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,10 @@ "hyperf/constants": "~2.0.0", "hyperf/model-cache": "~2.0.0", "hyperf/validation": "^2.0", - "hyperf/paginator": "^2.0" + "hyperf/paginator": "^2.0", + "hyperf/snowflake": "^2.0", + "ext-bcmath": "*", + "hyperf/task": "^2.0" }, "require-dev": { "swoole/ide-helper": "^4.5", diff --git a/config/autoload/dependencies.php b/config/autoload/dependencies.php index cccee93..a4028ee 100644 --- a/config/autoload/dependencies.php +++ b/config/autoload/dependencies.php @@ -10,5 +10,7 @@ declare(strict_types=1); * @license https://github.com/hyperf/hyperf/blob/master/LICENSE */ return [ - \App\Service\ServiceEvaluateServiceInterface::class => \App\Service\ServiceEvaluateService::class + \App\Service\ServiceEvaluateServiceInterface::class => \App\Service\ServiceEvaluateService::class, + \App\Service\OrderServiceInterface::class => \App\Service\OrderService::class, + \App\Service\CoupnoServiceInterface::class => \App\Service\CouponService::class, ]; diff --git a/config/autoload/server.php b/config/autoload/server.php index e0e6cd8..f44b083 100644 --- a/config/autoload/server.php +++ b/config/autoload/server.php @@ -36,16 +36,13 @@ return [ 'max_request' => 100000, 'socket_buffer_size' => 2 * 1024 * 1024, 'buffer_output_size' => 2 * 1024 * 1024, - // Task Worker 数量,根据您的服务器配置而配置适当的数量 'task_worker_num' => 8, - // 因为 `Task` 主要处理无法协程化的方法,所以这里推荐设为 `false`,避免协程下出现数据混淆的情况 'task_enable_coroutine' => false, ], 'callbacks' => [ SwooleEvent::ON_WORKER_START => [Hyperf\Framework\Bootstrap\WorkerStartCallback::class, 'onWorkerStart'], SwooleEvent::ON_PIPE_MESSAGE => [Hyperf\Framework\Bootstrap\PipeMessageCallback::class, 'onPipeMessage'], SwooleEvent::ON_WORKER_EXIT => [Hyperf\Framework\Bootstrap\WorkerExitCallback::class, 'onWorkerExit'], - // Task callbacks SwooleEvent::ON_TASK => [Hyperf\Framework\Bootstrap\TaskCallback::class, 'onTask'], SwooleEvent::ON_FINISH => [Hyperf\Framework\Bootstrap\FinishCallback::class, 'onFinish'], ], diff --git a/config/autoload/snowflake.php b/config/autoload/snowflake.php new file mode 100644 index 0000000..279de42 --- /dev/null +++ b/config/autoload/snowflake.php @@ -0,0 +1,24 @@ + MetaGeneratorInterface::DEFAULT_BEGIN_SECOND, + RedisMilliSecondMetaGenerator::class => [ + 'pool' => 'default', + ], + RedisSecondMetaGenerator::class => [ + 'pool' => 'default', + ], +]; diff --git a/config/routes.php b/config/routes.php index 706a9ab..c14f389 100644 --- a/config/routes.php +++ b/config/routes.php @@ -23,4 +23,5 @@ Router::addGroup('/v1/',function (){ Router::post('ServiceEvaluate/isPersonnel', 'App\Controller\ServiceEvaluateController@isPersonnel'); Router::post('ServiceEvaluate/getPersonnelInfo', 'App\Controller\ServiceEvaluateController@getPersonnelInfo'); Router::post('ServiceEvaluate/getEvaluateList', 'App\Controller\ServiceEvaluateController@getEvaluateList'); + Router::post('Order/addOnline', 'App\Controller\OrderController@addOnlineOrder'); }); \ No newline at end of file