• php 实现paypal订阅


    paypal订阅与google订阅的不同之处:

    1. google设置首周优惠时,用户第一次订阅会有优惠,用户退订之后再次订阅就不会执行首周订阅优惠; 而paypal设置首周优惠时,用户的每次取消后订阅都视为新订阅,都满足首周优惠条件

    2. paypal订阅成功回调时不会返回 “订阅到期的时间”

    订阅流程及创建计划查看:https://blog.csdn.net/weixin_39461487/article/details/125900163

    1、后台创建订阅计划

    正式环境创建订阅计划地址:

    PayPal Subscription | Automate repeat payments | PayPal US

    沙盒环境创建订阅计划计划:

    • 对于您的一个沙盒业务帐户-->https://www.sandbox.paypal.com/billing/plans
    • 对于您的live帐户-->https://www.paypal.com/billing/plans

    2、curl请求并订阅计划

     2.1 初始化参数

    1. class PaypalService extends Service
    2. {
    3. public $baseUrl;
    4. public $paypalParams;
    5. public $access_token;
    6. public function __construct($config = [])
    7. {
    8. parent::__construct();
    9. $this->paypalParams = [
    10. 'return_url' => '支付成功跳转地址',
    11. 'cancel_url' => '支付失败跳转地址',
    12. 'client_id' => '',
    13. 'client_secret' => '',
    14. ];
    15. if (!'test') {
    16. $this->baseUrl = 'https://api.paypal.com';
    17. } else {
    18. $this->baseUrl = 'https://api.sandbox.paypal.com';
    19. }
    20. }
    21. }

    2.2 获取token

    官方文档地址:https://developer.paypal.com/api/rest/authentication/

    1. public function getToken($is_expires=false)
    2. {
    3. $redis = new RedisStore();
    4. $key = 'paypal_token';
    5. $token = $redis->get($key);
    6. if($token && !$is_expires) {
    7. $this->access_token = $token;
    8. return true;
    9. }
    10. $ch = curl_init();
    11. curl_setopt($ch, CURLOPT_URL, $this->baseUrl . "/v1/oauth2/token");
    12. curl_setopt($ch, CURLOPT_HEADER, false);
    13. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    14. curl_setopt($ch, CURLOPT_POST, true);
    15. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    16. curl_setopt($ch, CURLOPT_USERPWD, $this->paypalParams['client_id'] . ':' . $this->paypalParams['client_secret']);
    17. curl_setopt($ch, CURLOPT_POSTFIELDS, "grant_type=client_credentials");
    18. $result = curl_exec($ch);
    19. if (empty($result)) {
    20. curl_close($ch);
    21. return false;
    22. } else {
    23. curl_close($ch);
    24. $result = json_decode($result);
    25. $redis->set($key, $result->access_token);
    26. $redis->expireAt($key, $result->expires_in - 300);
    27. $this->access_token = $result->access_token;
    28. return true;
    29. }
    30. }

    2.3 创建订阅

    官方文档地址:https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_create

    1. public function createSubscriptions($subscriptions_plan_id, $start_time = null, $is_expires=false)
    2. {
    3. if(!$this->access_token) $this->getToken();
    4. if ($start_time === null) $start_time = date('c', time()+5);
    5. $subscriptionsData = [
    6. "plan_id" => $subscriptions_plan_id,
    7. "start_time" => $start_time,
    8. "quantity" => "1",
    9. "auto_renewal" => 'true',
    10. "application_context" => [
    11. // "brand_name" => "Your brand name",
    12. "locale" => "en-US",
    13. "shipping_preference" => "NO_SHIPPING",
    14. "user_action" => "SUBSCRIBE_NOW",
    15. "payment_method" => [
    16. "payer_selected" => "PAYPAL",
    17. "payee_preferred" => "IMMEDIATE_PAYMENT_REQUIRED"
    18. ],
    19. "return_url" => $this->paypalParams['return_url'],
    20. "cancel_url" => $this->paypalParams['cancel_url'],
    21. ]
    22. ];
    23. $ch1 = curl_init();
    24. curl_setopt($ch1, CURLOPT_URL, $this->baseUrl . "/v1/billing/subscriptions");
    25. curl_setopt($ch1, CURLOPT_SSL_VERIFYPEER, false);
    26. curl_setopt($ch1, CURLOPT_RETURNTRANSFER, true);
    27. curl_setopt($ch1, CURLOPT_POSTFIELDS, json_encode($subscriptionsData));
    28. curl_setopt($ch1, CURLOPT_POST, true);
    29. curl_setopt($ch1, CURLOPT_HTTPHEADER, [
    30. 'Authorization: Bearer ' . $this->access_token,
    31. 'Accept: application/json',
    32. 'Content-Type: application/json'
    33. ]);
    34. $result = json_decode(curl_exec($ch1));
    35. curl_close($ch1);
    36. if($result->error == 'invalid_token') {
    37. // token 失效时重新获取token
    38. $this->getToken(true);
    39. return $this->createSubscriptions($subscriptions_plan_id, $start_time, true);
    40. }
    41. if($result->debug_id) {
    42. return ['code' => false, 'msg' => $result->details[0]->description];
    43. }
    44. return ['code' => true, 'url' => $result->links[0]->href, 'agreement_id' => $result->id];
    45. }

    2.4 查询用户订阅详情

    官方文档地址:https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_get

    1. public function getSubscriptionDetails($subscription_id, $is_expires=false)
    2. {
    3. if(!$this->access_token) $this->getToken();
    4. $ch1 = curl_init();
    5. curl_setopt($ch1, CURLOPT_URL, $this->baseUrl . "/v1/billing/subscriptions/" . $subscription_id);
    6. curl_setopt($ch1, CURLOPT_SSL_VERIFYPEER, false);
    7. curl_setopt($ch1, CURLOPT_RETURNTRANSFER, true);
    8. curl_setopt($ch1, CURLOPT_HTTPHEADER, [
    9. 'Authorization: Bearer ' . $this->access_token,
    10. 'Accept: application/json',
    11. 'Content-Type: application/json',
    12. ]);
    13. $result = json_decode(curl_exec($ch1));
    14. curl_close($ch1);
    15. if($result->error == 'invalid_token') {
    16. // token 失效时重新获取token
    17. $this->getToken(true);
    18. return $this->getSubscriptionDetails($subscription_id, true);
    19. }
    20. return json_decode(json_encode($result), true);
    21. }

    2.5 封装完整代码

    1. class PaypalService extends Service
    2. {
    3. public $baseUrl;
    4. public $paypalParams;
    5. public $access_token;
    6. public function __construct($config = [])
    7. {
    8. parent::__construct();
    9. $this->paypalParams = [
    10. 'return_url' => '支付成功跳转地址',
    11. 'cancel_url' => '支付失败跳转地址',
    12. 'client_id' => '',
    13. 'client_secret' => '',
    14. ];
    15. if (!'test') {
    16. $this->baseUrl = 'https://api.paypal.com';
    17. } else {
    18. $this->baseUrl = 'https://api.sandbox.paypal.com';
    19. }
    20. }
    21. /**
    22. * 获取token
    23. * @param false $is_expires 是否强制更新token
    24. * https://developer.paypal.com/api/rest/authentication/
    25. * @return false|mixed
    26. */
    27. public function getToken($is_expires=false)
    28. {
    29. $redis = new RedisStore();
    30. $key = 'paypal_token';
    31. $token = $redis->get($key);
    32. if($token && !$is_expires) {
    33. $this->access_token = $token;
    34. return true;
    35. }
    36. $ch = curl_init();
    37. curl_setopt($ch, CURLOPT_URL, $this->baseUrl . "/v1/oauth2/token");
    38. curl_setopt($ch, CURLOPT_HEADER, false);
    39. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    40. curl_setopt($ch, CURLOPT_POST, true);
    41. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    42. curl_setopt($ch, CURLOPT_USERPWD, $this->paypalParams['client_id'] . ':' . $this->paypalParams['client_secret']);
    43. curl_setopt($ch, CURLOPT_POSTFIELDS, "grant_type=client_credentials");
    44. $result = curl_exec($ch);
    45. if (empty($result)) {
    46. curl_close($ch);
    47. return false;
    48. } else {
    49. curl_close($ch);
    50. $result = json_decode($result);
    51. $redis->set($key, $result->access_token);
    52. $redis->expireAt($key, $result->expires_in - 300);
    53. $this->access_token = $result->access_token;
    54. return true;
    55. }
    56. }
    57. /**
    58. * 创建订阅
    59. * @param $subscriptions_plan_id
    60. * @param null $start_time
    61. * @param false $is_expires 是否强制更新token
    62. * https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_create
    63. * @return array
    64. */
    65. public function createSubscriptions($subscriptions_plan_id, $start_time = null, $is_expires=false)
    66. {
    67. if(!$this->access_token) $this->getToken();
    68. if ($start_time === null) $start_time = date('c', time()+5);
    69. $subscriptionsData = [
    70. "plan_id" => $subscriptions_plan_id,
    71. "start_time" => $start_time,
    72. "quantity" => "1",
    73. "auto_renewal" => 'true',
    74. "application_context" => [
    75. // "brand_name" => "Your brand name",
    76. "locale" => "en-US",
    77. "shipping_preference" => "NO_SHIPPING",
    78. "user_action" => "SUBSCRIBE_NOW",
    79. "payment_method" => [
    80. "payer_selected" => "PAYPAL",
    81. "payee_preferred" => "IMMEDIATE_PAYMENT_REQUIRED"
    82. ],
    83. "return_url" => $this->paypalParams['return_url'],
    84. "cancel_url" => $this->paypalParams['cancel_url'],
    85. ]
    86. ];
    87. $ch1 = curl_init();
    88. curl_setopt($ch1, CURLOPT_URL, $this->baseUrl . "/v1/billing/subscriptions");
    89. curl_setopt($ch1, CURLOPT_SSL_VERIFYPEER, false);
    90. curl_setopt($ch1, CURLOPT_RETURNTRANSFER, true);
    91. curl_setopt($ch1, CURLOPT_POSTFIELDS, json_encode($subscriptionsData));
    92. curl_setopt($ch1, CURLOPT_POST, true);
    93. curl_setopt($ch1, CURLOPT_HTTPHEADER, [
    94. 'Authorization: Bearer ' . $this->access_token,
    95. 'Accept: application/json',
    96. 'Content-Type: application/json'
    97. ]);
    98. $result = json_decode(curl_exec($ch1));
    99. curl_close($ch1);
    100. if($result->error == 'invalid_token') {
    101. // token 失效时重新获取token
    102. $this->getToken(true);
    103. return $this->createSubscriptions($subscriptions_plan_id, $start_time, true);
    104. }
    105. if($result->debug_id) {
    106. return ['code' => false, 'msg' => $result->details[0]->description];
    107. }
    108. return ['code' => true, 'url' => $result->links[0]->href, 'agreement_id' => $result->id];
    109. }
    110. /**
    111. * 获取订阅计划详情
    112. * @param $subscription_id
    113. * @param false $is_expires 是否强制获取token
    114. * https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_get
    115. * @return mixed
    116. */
    117. public function getSubscriptionDetails($subscription_id, $is_expires=false)
    118. {
    119. if(!$this->access_token) $this->getToken();
    120. $ch1 = curl_init();
    121. curl_setopt($ch1, CURLOPT_URL, $this->baseUrl . "/v1/billing/subscriptions/" . $subscription_id);
    122. curl_setopt($ch1, CURLOPT_SSL_VERIFYPEER, false);
    123. curl_setopt($ch1, CURLOPT_RETURNTRANSFER, true);
    124. curl_setopt($ch1, CURLOPT_HTTPHEADER, [
    125. 'Authorization: Bearer ' . $this->access_token,
    126. 'Accept: application/json',
    127. 'Content-Type: application/json',
    128. ]);
    129. $result = json_decode(curl_exec($ch1));
    130. curl_close($ch1);
    131. if($result->error == 'invalid_token') {
    132. // token 失效时重新获取token
    133. $this->getToken(true);
    134. return $this->getSubscriptionDetails($subscription_id, true);
    135. }
    136. return json_decode(json_encode($result), true);
    137. }
    138. }

    3、订阅支付

    1. $subscription = $this->createSubscriptions('计划id');
    2. if($subscription['code'] === false) {
    3. return [
    4. 'code' => ErrorCode::EC_UNKNOWN,
    5. 'msg' => 'Paypal Subscriptions Error: ' . $subscription['msg'],
    6. 'data' => [],
    7. ];
    8. }
    9. // 购买商品下单流程...
    10. // 成功返回的结果
    11. // 'url' => $subscription['url'],
    12. // 'agreement_id' => $subscription['agreement_id'],

    返回结果:

    id:paypal生成的订阅ID

    links[0]['href']:去支付的链接

    1. object(stdClass)#189 (4) {
    2. ["status"]=>
    3. string(16) "APPROVAL_PENDING"
    4. ["id"]=>
    5. string(14) "I-BU64KX8FTG3C"
    6. ["create_time"]=>
    7. string(20) "2022-08-09T05:32:29Z"
    8. ["links"]=>
    9. array(3) {
    10. [0]=>
    11. object(stdClass)#219 (3) {
    12. ["href"]=>
    13. string(90) "https://www.sandbox.paypal.com/webapps/billing/subscriptions?ba_token=BA-3P5459826W8205613"
    14. ["rel"]=>
    15. string(7) "approve"
    16. ["method"]=>
    17. string(3) "GET"
    18. }
    19. [1]=>
    20. object(stdClass)#220 (3) {
    21. ["href"]=>
    22. string(70) "https://api.sandbox.paypal.com/v1/billing/subscriptions/I-BU64KX8FTG3C"
    23. ["rel"]=>
    24. string(4) "edit"
    25. ["method"]=>
    26. string(5) "PATCH"
    27. }
    28. [2]=>
    29. object(stdClass)#221 (3) {
    30. ["href"]=>
    31. string(70) "https://api.sandbox.paypal.com/v1/billing/subscriptions/I-BU64KX8FTG3C"
    32. ["rel"]=>
    33. string(4) "self"
    34. ["method"]=>
    35. string(3) "GET"
    36. }
    37. }
    38. }

    4、订阅回调

    回调需要配置webhook及回调地址

     事件说明:Webhook event names

    1. public function actionPaypal()
    2. {
    3. $payload = @file_get_contents('php://input');
    4. $content = "=========".date('Y-m-d H:i:s',time())."==========\r\n";
    5. file_put_contents('paypal_success.log', $content . $payload . "\r\n",FILE_APPEND);
    6. $content = json_decode($payload);
    7. }

    首次订阅成功会回调两个事件:

    1.BILLING.SUBSCRIPTION.ACTIVATED

    1. =========2022-08-02 05:58:58==========
    2. {"id":"WH-2JK13221SH228131A-85M888883CF992091V","event_version":"1.0","create_time":"2022-08-02T09:58:37.896Z","resource_type":"subscription","resource_version":"2.0","event_type":"BILLING.SUBSCRIPTION.ACTIVATED","summary":"Subscription activated","resource":{"quantity":"1","subscriber":{"email_address":"wwww@personal.example.com","payer_id":"BNMR2YZQJ9Q2L","name":{"given_name":"John","surname":"Doe"}},"create_time":"2022-08-02T09:58:05Z","plan_overridden":false,"shipping_amount":{"currency_code":"USD","value":"0.0"},"start_time":"2022-08-02T09:57:17Z","update_time":"2022-08-02T09:58:06Z","billing_info":{"outstanding_balance":{"currency_code":"USD","value":"0.0"},"cycle_executions":[{"tenure_type":"TRIAL","sequence":1,"cycles_completed":1,"cycles_remaining":0,"current_pricing_scheme_version":1,"total_cycles":1},{"tenure_type":"REGULAR","sequence":2,"cycles_completed":0,"cycles_remaining":0,"current_pricing_scheme_version":1,"total_cycles":0}],"last_payment":{"amount":{"currency_code":"USD","value":"0.99"},"time":"2022-08-02T09:58:05Z"},"next_billing_time":"2022-08-03T10:00:00Z","failed_payments_count":0},"links":[{"href":"https://api.sandbox.paypal.com/v1/billing/subscriptions/I-DL5L53KM3N/cancel","rel":"cancel","method":"POST","encType":"application/json"},{"href":"https://api.sandbox.paypal.com/v1/billing/subscriptions/I-DL5L53KM3N","rel":"edit","method":"PATCH","encType":"application/json"},{"href":"https://api.sandbox.paypal.com/v1/billing/subscriptions/I-DL5L53KM3N","rel":"self","method":"GET","encType":"application/json"},{"href":"https://api.sandbox.paypal.com/v1/billing/subscriptions/I-DL5L53KM3N/suspend","rel":"suspend","method":"POST","encType":"application/json"},{"href":"https://api.sandbox.paypal.com/v1/billing/subscriptions/I-DL5L53KM3N/capture","rel":"capture","method":"POST","encType":"application/json"}],"id":"I-DL5L53KM3N","plan_id":"P-4G238388SD824MLUPJNY","status":"ACTIVE","status_update_time":"2022-08-02T09:58:06Z"},"links":[{"href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-2JK13221SH228131A-85M80963CF992091V","rel":"self","method":"GET"},{"href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-2JK13221SH228131A-85M80963CF992091V/resend","rel":"resend","method":"POST"}]}

    2.  PAYMENT.SALE.COMPLETED

    1. =========2022-08-02 05:58:58==========
    2. {"id":"WH-4AU27166UL181633V-3BU78225VU943751T","event_version":"1.0","create_time":"2022-08-02T09:58:43.670Z","resource_type":"sale","event_type":"PAYMENT.SALE.COMPLETED","summary":"Payment completed for $ 0.99 USD","resource":{"billing_agreement_id":"I-DL5L53KM3N","amount":{"total":"0.99","currency":"USD","details":{"subtotal":"0.99"}},"payment_mode":"INSTANT_TRANSFER","update_time":"2022-08-02T09:58:05Z","create_time":"2022-08-02T09:58:05Z","protection_eligibility_type":"ITEM_NOT_RECEIVED_ELIGIBLE,UNAUTHORIZED_PAYMENT_ELIGIBLE","transaction_fee":{"currency":"USD","value":"0.33"},"protection_eligibility":"ELIGIBLE","links":[{"method":"GET","rel":"self","href":"https://api.sandbox.paypal.com/v1/payments/sale/4DN9460xxx61205Y"},{"method":"POST","rel":"refund","href":"https://api.sandbox.paypal.com/v1/payments/sale/4DN9460xxx61205Y/refund"}],"id":"4DN9460xxx61205Y","state":"completed","invoice_number":""},"links":[{"href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-4AU27166UL181633V-3BU78225VU943751T","rel":"self","method":"GET"},{"href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-4AU27166UL181633V-3BU78225VU943751T/resend","rel":"resend","method":"POST"}]}

    续订时只会回调:PAYMENT.SALE.COMPLETED

    1. =========2022-08-03 06:25:06==========
    2. {"id":"WH-4AU27166UL181633V-3BU78225VU943751T","event_version":"1.0","create_time":"2022-08-02T09:58:43.670Z","resource_type":"sale","event_type":"PAYMENT.SALE.COMPLETED","summary":"Payment completed for $ 0.99 USD","resource":{"billing_agreement_id":"I-DL5L53KM3N","amount":{"total":"0.99","currency":"USD","details":{"subtotal":"0.99"}},"payment_mode":"INSTANT_TRANSFER","update_time":"2022-08-02T09:58:05Z","create_time":"2022-08-02T09:58:05Z","protection_eligibility_type":"ITEM_NOT_RECEIVED_ELIGIBLE,UNAUTHORIZED_PAYMENT_ELIGIBLE","transaction_fee":{"currency":"USD","value":"0.33"},"protection_eligibility":"ELIGIBLE","links":[{"method":"GET","rel":"self","href":"https://api.sandbox.paypal.com/v1/payments/sale/4DN9460xxx61205Y"},{"method":"POST","rel":"refund","href":"https://api.sandbox.paypal.com/v1/payments/sale/4DN9460xxx61205Y/refund"}],"id":"4DN9460xxx61205Y","state":"completed","invoice_number":""},"links":[{"href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-4AU27166UL181633V-3BU78225VU943751T","rel":"self","method":"GET"},{"href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-4AU27166UL181633V-3BU78225VU943751T/resend","rel":"resend","method":"POST"}]}

    4.1 订阅创建回调

    事件:BILLING.SUBSCRIPTION.ACTIVATED

    1. if($content->resource_type == 'subscription') {
    2. // 新建订阅
    3. if($content->event_type == 'BILLING.SUBSCRIPTION.ACTIVATED') {
    4. // 验证订单信息
    5. $agreement_id = $content->resource->id;
    6. $goodId = $content->resource->plan_id;
    7. // 支付价格
    8. $price = $content->resource->billing_info->last_payment->amount->value;
    9. // 创建时间
    10. $start_time = strtotime(str_replace('T', ' ', trim($content->resource->create_time, 'Z')));
    11. // 状态更新时间
    12. $update_time = strtotime(str_replace('T', ' ', trim($content->resource->update_time, 'Z')));
    13. // todo 验证并记录用户订阅时间...
    14. }
    15. }

    4.2 支付成功或续订成功回调

    事件:PAYMENT.SALE.COMPLETED

    因订阅回调与支付成功回调不知道哪个先后,所以此处订单表内记录了两个订阅的字段,agreement_id:创建订阅时返回的id,out_trade_no:订单编号

    如果此支付成功回调与订阅回调时间小于1小时,证明此支付成功回调与订阅回调为同一订单,我再处理默认首次订阅处理回调走BILLING.SUBSCRIPTION.ACTIVATED

    1. if($content->resource_type == 'sale') {
    2. if($content->event_type == 'PAYMENT.SALE.COMPLETED') {
    3. $agreement_id = $content->resource->billing_agreement_id; // 若是订阅,此字段为同意协议id,
    4. $payment_intent_new = $content->resource->id; // 订单id
    5. $update_time = strtotime(str_replace('T', ' ', trim($content->resource->update_time, 'Z')));
    6. // 支付价格
    7. $price = $content->resource->amount->total;
    8. if($agreement_id) {
    9. // 因订阅回调与支付成功回调不知道哪个先后,根据agreement_id查询首订阅订单,如果存在后续验证,不存在说明此回调在前,订阅成功回调在后,首订阅默认执行订阅成功回调
    10. $objTradeInfoOld = VipBuy::find()->andWhere(['agreement_id' => $agreement_id, 'status' => VipBuy::STATUS_SUCCESS])->one();
    11. if(objTradeInfoOld) {
    12. if($update_time - $objTradeInfoOld->notify_time <= 3600) {
    13. // 如果此支付成功回调与订阅回调时间小于1小时,证明此支付成功回调与订阅回调为同一订单
    14. return true;
    15. }
    16. $objTradeInfo = VipBuy::find()->andWhere(['out_trade_no' => $payment_intent_new, 'goods_id' => $objTradeInfoOld->goods_id, 'product' => $product])->one();
    17. if($objTradeInfo->status == VipBuy::STATUS_SUCCESS) return true;
    18. if(!$objTradeInfo) {
    19. // 此为续订订单,生成新订单并执行其他逻辑
    20. }
    21. }
    22. }
    23. }
    24. }

    4.3 取消订阅回调

    取消订阅流程:

    事件:BILLING.SUBSCRIPTION.CANCELLED

    1. if($content->resource_type == 'subscription') {
    2. // 取消订阅
    3. if($content->event_type == 'BILLING.SUBSCRIPTION.CANCELLED' && $content->resource->status == 'CANCELLED') {
    4. $payment_intent = $content->resource->id;
    5. $goodId = $content->resource->plan_id;
    6. // 状态更新时间,可记录为用户取消订阅时间
    7. $update_time = strtotime(str_replace('T', ' ', trim($content->resource->status_update_time, 'Z')));
    8. // todo 取消订阅后续操作...
    9. }
    10. }

    订阅完成

  • 相关阅读:
    unity操作_刚体 c#
    【how2j练习题】HTML部分综合练习
    基于Java毕业设计休闲网络宾馆管理源码+系统+mysql+lw文档+部署软件
    php比特教务选排课系统的设计与实现毕业设计源码301826
    LeetCode-345. 反转字符串中的元音字母-Java-easy
    商业化之路怎么走,一家开源分布式数据库厂商的答案|爱分析调研
    Linux字符设备驱动
    实现数组去重的其中三种方法
    【图像加密】基于离散小波变换结合Schur分解的双重加密零水印算法附matlab代码
    Android ThreadLocal
  • 原文地址:https://blog.csdn.net/weixin_39461487/article/details/126244489