php 修改zen-cart下单和付款流程以防止漏单


Posted in PHP onMarch 08, 2010

用过zen-cart的人都知道,zen-cart中下单步骤是下面这样的(其中[]中的表示不是必须的):

   1. 购物车(shopping cart)

   2. [货运方式(delivery method)]

   3. 支付方式(payment method)

   4. 订单确认(confirmation)

   5. [第三方网站支付]

   6. 订单处理(checkout process)——这一步比较重要,因为会在这里将购物车中的信息写入订单

   7. 下单成功(checkout success)

   这样的流程在正常情况下是没有任何问题的。但是,从第5步到第6部的过程中,用户可能以为付款成功就直接关闭掉网页了,或者由于网络原因造成不能正常跳转到checkout_process页面,这样造成的后果是很严重的,因为订单不能被正常的创建。

   基于上述的分析, 我们希望稍微地改变一下流程,即在支付之前订单已经创建好了,这样就算在支付时不能从第三方支付网站跳转回来,我们也不会存在用户付款成功却在后台没有订单的情况了。经过修改后的蓝图基本是下面这样的:

   1. 在checkour_confirmation页面确认订单后,都会直接proccess,并且进入checkour_success页面,可以在这里进入付款页面。如下图所示:

     php 修改zen-cart下单和付款流程以防止漏单

   2. 如果当时客户没能付款,也可进入自己的后台对历史订单进行付款。如下图所示:

     php 修改zen-cart下单和付款流程以防止漏单

   下面我们就来看看如何一步一步来实现上述的功能。

  1. 首先我们需要对现有的支付模块进行一个改造。需要对支付方式的class增加一个字段paynow_action_url,用来表示进行支付的页面url,另外还需要增加一个函数,paynow_button($order_id),来获取支付表单的参数隐藏域代码。
要增加paynow_action_url字段,请在类payment的构造函数中最后加上下面的代码:

if ( (zen_not_null($module)) && (in_array($module.'.php', $this->modules)) && (isset($GLOBALS[$module]->paynow_action_url)) ) { 
$this->paynow_action_url = $GLOBALS[$module]->paynow_action_url; 
}

要增加paynow_button($order_id)函数,请在payment类的最后一个函数之后加上如下的代码:
function paynow_button($order_id){ 
if (is_array($this->modules)) { 
if (is_object($GLOBALS[$this->selected_module])) { 
return $GLOBALS[$this->selected_module]->paynow_button($order_id); 
} 
} 
}

2. 以paypal支付方式为例子,说明如何具体实现。为了不破坏paypal原有的代码,我们将paypal.php文件拷贝一个副本出来,并命名为paypalsimple.php,并对里面的代码做适当的修改。代码如下所示,可以看到,这里去掉了对form_action_url的指定,并给定了paynow_action_url,因为我们希望用户点击“确认订单”后直接进入checkout_process,所以如果不指定form_action_url,那么确认订单的表单就会直接提交到checkout_process页面了,而paynow_action_url就是以前的form_action_url的值。paynow_button函数的实现也很简单,这里只是将原先的process_button()函数的内容剪切过来而已,只不过我们没有使用全局的$order变量,而是使用$order = new order($order_id),来重新构造的一个对象,这样做是为在历史订单中显示pay now按钮做准备的。
paypalsimple.php
<?php 
/** 
* @package paypalsimple payment module 
* @copyright Copyright 2003-2006 Zen Cart Development Team 
* @copyright Portions Copyright 2003 osCommerce 
* @license http://www.zen-cart.com/license/2_0.txt GNU Public License V2.0 
* @version $Id: paypalsimple.php 4960 2009-12-29 11:46:46Z gary $ 
*/ 
// ensure dependencies are loaded 
include_once((IS_ADMIN_FLAG === true ? DIR_FS_CATALOG_MODULES : DIR_WS_MODULES) . 'payment/paypal/paypal_functions.php'); 
class paypalsimple { 
var $code, $title, $description, $enabled; 
// class constructor 
function paypalsimple() { 
global $order; 
$this->code = 'paypalsimple'; 
$this->title = MODULE_PAYMENT_PAYPAL_SIMPLE_TEXT_TITLE; 
if(IS_ADMIN_FLAG === true){ 
$this->title = MODULE_PAYMENT_PAYPAL_SIMPLE_TEXT_ADMIN_TITLE; 
} 
$this->description = MODULE_PAYMENT_PAYPAL_SIMPLE_TEXT_DESCRIPTION; 
$this->sort_order = MODULE_PAYMENT_PAYPAL_SIMPLE_SORT_ORDER; 
$this->enabled = ((MODULE_PAYMENT_PAYPAL_SIMPLE_STATUS == 'True') ? true : false); 
if ((int)MODULE_PAYMENT_PAYPAL_SIMPLE_ORDER_STATUS_ID > 0) { 
$this->order_status = MODULE_PAYMENT_PAYPAL_SIMPLE_ORDER_STATUS_ID; 
} 
$this->paynow_action_url = 'https://' . MODULE_PAYMENT_PAYPAL_SIMPLE_HANDLER; 
if (is_object($order)) $this->update_status(); 
} 
// class methods 
function update_status() { 
global $order, $db; 
if ( ($this->enabled == true) && ((int)MODULE_PAYMENT_PAYPAL_SIMPLE_ZONE > 0) ) { 
$check_flag = false; 
$check = $db->Execute("select zone_id from " . TABLE_ZONES_TO_GEO_ZONES . " where geo_zone_id = '" . MODULE_PAYMENT_PAYPAL_SIMPLE_ZONE . "' and zone_country_id = '" . $order->billing['country']['id'] . "' order by zone_id"); 
while (!$check->EOF) { 
if ($check->fields['zone_id'] < 1) { 
$check_flag = true; 
break; 
} elseif ($check->fields['zone_id'] == $order->billing['zone_id']) { 
$check_flag = true; 
break; 
} 
$check->MoveNext(); 
} 
if ($check_flag == false) { 
$this->enabled = false; 
} 
} 
} 
function javascript_validation() { 
return false; 
} 
function selection() { 
$text = MODULE_PAYMENT_SIMPLE_PAYPAL_TEXT_CATALOG_LOGO.'  '.MODULE_PAYMENT_PAYPAL_SIMPLE_TEXT_TITLE . '<br/><br/>    <span class="smallText">' . MODULE_PAYMENT_PAYPAL_SIMPLE_ACCEPTANCE_MARK_TEXT . '</span><br/><br/>'; 
return array('id' => $this->code, 
'module' => $text 
); 
} 
function pre_confirmation_check() { 
return false; 
} 
function confirmation() { 
return false; 
} 
function process_button() { 
return false; 
} 
function before_process() { 
return false; 
} 
function after_process() { 
return false; 
} 
function get_error() { 
return false; 
} 
function check() { 
global $db; 
if (!isset($this->_check)) { 
$check_query = $db->Execute("select configuration_value from " . TABLE_CONFIGURATION . " where configuration_key = 'MODULE_PAYMENT_PAYPAL_SIMPLE_STATUS'"); 
$this->_check = $check_query->RecordCount(); 
} 
return $this->_check; 
} 
function install() { 
global $db; 
$db->Execute("insert into " . TABLE_CONFIGURATION . " (configuration_title, configuration_key, configuration_value, configuration_description, configuration_group_id, sort_order, set_function, date_added) values ('Enable PayPal-Simple Module', 'MODULE_PAYMENT_PAYPAL_SIMPLE_STATUS', 'True', 'Do you want to accept PayPal-Simple payments?', '6', '0', 'zen_cfg_select_option(array(\'True\', \'False\'), ', now())"); 
$db->Execute("insert into " . TABLE_CONFIGURATION . " (configuration_title, configuration_key, configuration_value, configuration_description, configuration_group_id, sort_order, date_added) values ('Sort order of display.', 'MODULE_PAYMENT_PAYPAL_SIMPLE_SORT_ORDER', '0', 'Sort order of display. Lowest is displayed first.', '6', '8', now())"); 
$db->Execute("insert into " . TABLE_CONFIGURATION . " (configuration_title, configuration_key, configuration_value, configuration_description, configuration_group_id, sort_order, use_function, set_function, date_added) values ('Payment Zone', 'MODULE_PAYMENT_PAYPAL_SIMPLE_ZONE', '0', 'If a zone is selected, only enable this payment method for that zone.', '6', '2', 'zen_get_zone_class_title', 'zen_cfg_pull_down_zone_classes(', now())"); 
$db->Execute("insert into " . TABLE_CONFIGURATION . " (configuration_title, configuration_key, configuration_value, configuration_description, configuration_group_id, sort_order, set_function, use_function, date_added) values ('Set Order Status', 'MODULE_PAYMENT_PAYPAL_SIMPLE_ORDER_STATUS_ID', '0', 'Set the status of orders made with this payment module to this value', '6', '0', 'zen_cfg_pull_down_order_statuses(', 'zen_get_order_status_name', now())"); 
$db->Execute("insert into " . TABLE_CONFIGURATION . " (configuration_title, configuration_key, configuration_value, configuration_description, configuration_group_id, sort_order, set_function, date_added) values ('Mode for PayPal web services<br /><br />Default:<br /><code>www.paypal.com/cgi-bin/webscr</code><br />or<br /><code>www.paypal.com/us/cgi-bin/webscr</code><br />or for the UK,<br /><code>www.paypal.com/uk/cgi-bin/webscr</code>', 'MODULE_PAYMENT_PAYPAL_SIMPLE_HANDLER', 'www.paypal.com/cgi-bin/webscr', 'Choose the URL for PayPal live processing', '6', '73', '', now())"); 
} 
function remove() { 
global $db; 
$db->Execute("delete from " . TABLE_CONFIGURATION . " where configuration_key in ('" . implode("', '", $this->keys()) . "')"); 
} 
function keys() { 
return array('MODULE_PAYMENT_PAYPAL_SIMPLE_STATUS','MODULE_PAYMENT_PAYPAL_SIMPLE_SORT_ORDER','MODULE_PAYMENT_PAYPAL_SIMPLE_ZONE','MODULE_PAYMENT_PAYPAL_SIMPLE_ORDER_STATUS_ID', 'MODULE_PAYMENT_PAYPAL_SIMPLE_HANDLER'); 
} 
function paynow_button($order_id){ 
global $db, $order, $currencies, $currency; 
require_once(DIR_WS_CLASSES . 'order.php'); 
$order = new order($order_id); 
$options = array(); 
$optionsCore = array(); 
$optionsPhone = array(); 
$optionsShip = array(); 
$optionsLineItems = array(); 
$optionsAggregate = array(); 
$optionsTrans = array(); 
$buttonArray = array(); 
$this->totalsum = $order->info['total']; 
// save the session stuff permanently in case paypal loses the session 
$_SESSION['ppipn_key_to_remove'] = session_id(); 
$db->Execute("delete from " . TABLE_PAYPAL_SESSION . " where session_id = '" . zen_db_input($_SESSION['ppipn_key_to_remove']) . "'"); 
$sql = "insert into " . TABLE_PAYPAL_SESSION . " (session_id, saved_session, expiry) values ( 
'" . zen_db_input($_SESSION['ppipn_key_to_remove']) . "', 
'" . base64_encode(serialize($_SESSION)) . "', 
'" . (time() + (1*60*60*24*2)) . "')"; 
$db->Execute($sql); 
$my_currency = select_pp_currency(); 
$this->transaction_currency = $my_currency; 
$this->transaction_amount = ($this->totalsum * $currencies->get_value($my_currency)); 
$telephone = preg_replace('/\D/', '', $order->customer['telephone']); 
if ($telephone != '') { 
$optionsPhone['H_PhoneNumber'] = $telephone; 
if (in_array($order->customer['country']['iso_code_2'], array('US','CA'))) { 
$optionsPhone['night_phone_a'] = substr($telephone,0,3); 
$optionsPhone['night_phone_b'] = substr($telephone,3,3); 
$optionsPhone['night_phone_c'] = substr($telephone,6,4); 
$optionsPhone['day_phone_a'] = substr($telephone,0,3); 
$optionsPhone['day_phone_b'] = substr($telephone,3,3); 
$optionsPhone['day_phone_c'] = substr($telephone,6,4); 
} else { 
$optionsPhone['night_phone_b'] = $telephone; 
$optionsPhone['day_phone_b'] = $telephone; 
} 
} 
$optionsCore = array( 
'charset' => CHARSET, 
'lc' => $order->customer['country']['iso_code_2'], 
'page_style' => MODULE_PAYMENT_PAYPAL_PAGE_STYLE, 
'custom' => zen_session_name() . '=' . zen_session_id(), 
'business' => MODULE_PAYMENT_PAYPAL_BUSINESS_ID, 
'return' => zen_href_link(FILENAME_PAY_SUCCESS, 'referer=paypal', 'SSL'), 
'cancel_return' => zen_href_link(FILENAME_PAY_FAILED, '', 'SSL'), 
'shopping_url' => zen_href_link(FILENAME_SHOPPING_CART, '', 'SSL'), 
'notify_url' => zen_href_link('ipn_main_handler.php', '', 'SSL',false,false,true), 
'redirect_cmd' => '_xclick', 
'rm' => 2, 
'bn' => 'zencart', 
'mrb' => 'R-6C7952342H795591R', 
'pal' => '9E82WJBKKGPLQ', 
); 
$optionsCust = array( 
'first_name' => replace_accents($order->customer['firstname']), 
'last_name' => replace_accents($order->customer['lastname']), 
'address1' => replace_accents($order->customer['street_address']), 
'city' => replace_accents($order->customer['city']), 
'state' => zen_get_zone_code($order->customer['country']['id'], $order->customer['zone_id'], $order->customer['zone_id']), 
'zip' => $order->customer['postcode'], 
'country' => $order->customer['country']['iso_code_2'], 
'email' => $order->customer['email_address'], 
); 
if ($order->customer['suburb'] != '') $optionsCust['address2'] = $order->customer['suburb']; 
if (MODULE_PAYMENT_PAYPAL_ADDRESS_REQUIRED == 2) $optionsCust = array( 
'address_name' => replace_accents($order->customer['firstname'] . ' ' . $order->customer['lastname']), 
'address_street' => replace_accents($order->customer['street_address']), 
'address_city' => replace_accents($order->customer['city']), 
'address_state' => zen_get_zone_code($order->customer['country']['id'], $order->customer['zone_id'], $order->customer['zone_id']), 
'address_zip' => $order->customer['postcode'], 
'address_country' => $order->customer['country']['title'], 
'address_country_code' => $order->customer['country']['iso_code_2'], 
'payer_email' => $order->customer['email_address'], 
); 
$optionsShip = array( 
//'address_override' => MODULE_PAYMENT_PAYPAL_ADDRESS_OVERRIDE, 
'no_shipping' => MODULE_PAYMENT_PAYPAL_ADDRESS_REQUIRED, 
); 
if (MODULE_PAYMENT_PAYPAL_DETAILED_CART == 'Yes') $optionsLineItems = ipn_getLineItemDetails(); 
if (sizeof($optionsLineItems) > 0) { 
$optionsLineItems['cmd'] = '_cart'; 
// $optionsLineItems['num_cart_items'] = sizeof($order->products); 
if (isset($optionsLineItems['shipping'])) { 
$optionsLineItems['shipping_1'] = $optionsLineItems['shipping']; 
unset($optionsLineItems['shipping']); 
} 
if (isset($optionsLineItems['handling'])) { 
$optionsLineItems['handling_1'] = $optionsLineItems['handling']; 
unset($optionsLineItems['handling']); 
} 
unset($optionsLineItems['subtotal']); 
// if line-item details couldn't be kept due to calculation mismatches or discounts etc, default to aggregate mode 
if (!isset($optionsLineItems['item_name_1'])) $optionsLineItems = array(); 
//if ($optionsLineItems['amount'] != $this->transaction_amount) $optionsLineItems = array(); 
ipn_debug_email('Line Item Details (if blank, this means there was a data mismatch, and thus bypassed): ' . "\n" . print_r($optionsLineItems, true)); 
} 
$products_name_display = ""; 
/* 
for ($i=0, $n=sizeof($order->products); $i<$n; $i++) { 
if(i > 0) { 
$products_name_display.= ', '; 
} 
$products_name_display.= $order->products[$i]['name']. '('. $order->products[$i]['qty'] .','.$order->products[$i]['dhisys_web_order_number'].')'; 
}*/ 
$optionsAggregate = array( 
'cmd' => '_ext-enter', 
'item_name' => $products_name_display, 
'item_number' => $order_id, 
'num_cart_items' => sizeof($order->products), 
'amount' => number_format($this->transaction_amount, $currencies->get_decimal_places($my_currency)), 
'shipping' => '0.00', 
); 
if (MODULE_PAYMENT_PAYPAL_TAX_OVERRIDE == 'true') $optionsAggregate['tax'] = '0.00'; 
if (MODULE_PAYMENT_PAYPAL_TAX_OVERRIDE == 'true') $optionsAggregate['tax_cart'] = '0.00'; 
$optionsTrans = array( 
'upload' => (int)(sizeof($order->products) > 0), 
'currency_code' => $my_currency, 
// 'paypal_order_id' => $paypal_order_id, 
//'no_note' => '1', 
//'invoice' => '', 
); 
// if line-item info is invalid, use aggregate: 
if (sizeof($optionsLineItems) > 0) $optionsAggregate = $optionsLineItems; 
// prepare submission 
$options = array_merge($optionsCore, $optionsCust, $optionsPhone, $optionsShip, $optionsTrans, $optionsAggregate); 
ipn_debug_email('Keys for submission: ' . print_r($options, true)); 
if(sizeof($order->products) > 0){ 
$options['cmd'] = '_cart'; 
for ($i=0, $n=sizeof($order->products); $i<$n; $i++) { 
$options['item_name_'. (string)($i+1)] = $order->products[$i]['name']; 
$options['item_number_'. (string)($i+1)] = $order->products[$i]['dhisys_web_order_number']; 
$options['amount_'. (string)($i+1)] = number_format((float)$order->products[$i]['final_price'],2); 
$options['quantity_'. (string)($i+1)] = $order->products[$i]['qty']; 
} 
} 
// build the button fields 
foreach ($options as $name => $value) { 
// remove quotation marks 
$value = str_replace('"', '', $value); 
// check for invalid chars 
if (preg_match('/[^a-zA-Z_0-9]/', $name)) { 
ipn_debug_email('datacheck - ABORTING - preg_match found invalid submission key: ' . $name . ' (' . $value . ')'); 
break; 
} 
// do we need special handling for & and = symbols? 
//if (strpos($value, '&') !== false || strpos($value, '=') !== false) $value = urlencode($value); 
$buttonArray[] = zen_draw_hidden_field($name, $value); 
} 
$_SESSION['paypal_transaction_info'] = array($this->transaction_amount, $this->transaction_currency); 
$process_button_string = implode("\n", $buttonArray) . "\n"; 
return $process_button_string; 
} 
} 
?>

3. 在checkout_success页面中显示pay now按钮。打开文件"includes/modules/pages/checkout_success/header.php",在文件的末尾添加下面的代码(如果你已经掌握zen-cart中的通知者/观察者模式,并且又不想破坏zen-cart核心代码的话,也可以创建一个观察类来监听NOTIFY_HEADER_END_CHECKOUT_SUCCESS来实现)。
require_once(DIR_WS_CLASSES . 'order.php'); 
require_once(DIR_WS_CLASSES . 'payment.php'); 
$payment_modules = new payment($orders->fields['payment_module_code']);

打开文件"includes/modules/templates/template_default/templates/tpl_checkout_success_default.php",并在适当的位置加上如下的代码,这里对订单的状态进行了一个判断,当只有订单的状态在未付款状态,才显示该按钮,
<div id="pay_now"> 
<?php 
//&& $orders->fields['orders_status'] == '1' 
if(isset($payment_modules->paynow_action_url) && $payment_modules->paynow_action_url != ''&& $orders->fields['orders_status'] == '1'){ 
echo('<fieldset id="csNotifications">'); 
echo('<legend>'.TEXT_PAYNOW.'</legend>'); 
echo zen_draw_form('checkout_paynow', $payment_modules->paynow_action_url, 'post', 'id="checkout_confirmation" onsubmit="submitonce();"'); 
$selection = $payment_modules->selection(); 
echo('<div class="buttonRow payment_method">'.$selection[0]['module'].'</div>'); 
echo('<div class="buttonRow forward paynow">'); 
if (is_array($payment_modules->modules)) { 
echo $payment_modules->paynow_button($orders_id); 
} 
echo(zen_image_submit(BUTTON_IMAGE_PAYNOW, BUTTON_IMAGE_PAYNOW_ALT, 'name="btn_paynow" id="btn_paynow"')); 
echo('</div>'); 
echo('</form>'); 
echo('</fieldset>'); 
} 
?> 
</div>

4. 在历史订单中显示pay now按钮。需要显示pay now按钮的页面有三个:account, account_history,account_history_info,这里的实现和checkout_success页面的实现大同小异,只是传给$payment_modules的函数paynow_button的参数不一样而已,这里就不再赘述。
总结:
经过上面的修改,我们的流程如下:
1. 购物车(shopping cart)
2. [货运方式(delivery method)]
3. 支付方式(payment method)
4. 订单确认(confirmation)
5. 订单处理(checkout process)
6. 下单成功(checkout success)
7. [第三方网站支付]
因为从订单确认到订单处理,都是在我们自己的网站完成的,并且进入支付网站之前,订单已经存在了,这样就不会出现掉单的情况了。
PHP 相关文章推荐
提升PHP执行速度全攻略(下)
Oct 09 PHP
php session 预定义数组
Mar 16 PHP
PHP下操作Linux消息队列完成进程间通信的方法
Jul 24 PHP
深入PHP数据缓存的使用说明
May 10 PHP
ThinkPHP实现事务回滚示例代码
Jun 23 PHP
php抽象类使用要点与注意事项分析
Feb 09 PHP
PHP简单实现生成txt文件到指定目录的方法
Apr 25 PHP
Zend Framework处理Json数据方法详解
Dec 09 PHP
PHP依赖注入(DI)和控制反转(IoC)详解
Jun 12 PHP
基于win2003虚拟机中apache服务器的访问
Aug 01 PHP
PHP调用API接口实现天气查询功能的示例
Sep 21 PHP
PHP底层运行机制与工作原理详解
Jul 31 PHP
PHP 最大运行时间 max_execution_time修改方法
Mar 08 #PHP
php ss7.5的数据调用 (笔记)
Mar 08 #PHP
phpmyadmin 常用选项设置详解版
Mar 07 #PHP
PHPMYADMIN 简明安装教程 推荐
Mar 07 #PHP
THINKPHP+JS实现缩放图片式截图的实现
Mar 07 #PHP
PHP用mysql数据库存储session的代码
Mar 05 #PHP
PHP 采集程序原理分析篇
Mar 05 #PHP
You might like
Excel数据导入Mysql数据库的实现代码
2008/06/05 PHP
php清空(删除)指定目录下的文件,不删除目录文件夹的实现代码
2014/09/04 PHP
PHP中把有符号整型转换为无符号整型方法
2015/05/27 PHP
php使用gd2绘制基本图形示例(直线、圆、正方形)
2017/02/15 PHP
PHP编程快速实现数组去重的方法详解
2017/07/22 PHP
js 深拷贝函数
2008/12/04 Javascript
JavaScript入门教程(10) 认识其他对象
2009/01/31 Javascript
利用jQuery操作对象数组的实现代码
2011/04/27 Javascript
window.open以post方式将内容提交到新窗口
2012/12/26 Javascript
JS性能优化笔记搜索整理
2013/08/21 Javascript
js实现非常棒的弹出div
2016/10/06 Javascript
详解JS中的快速排序与冒泡
2017/01/10 Javascript
微信小程序 数据遍历的实现
2017/04/05 Javascript
详解使用nvm安装node.js
2017/07/18 Javascript
vue项目关闭eslint校验
2018/03/21 Javascript
垃圾回收器的相关知识点总结
2018/05/13 Javascript
详解微信小程序之一键复制到剪切板
2019/04/24 Javascript
Python实现的石头剪子布代码分享
2014/08/22 Python
Python文件处理
2016/02/29 Python
浅谈编码,解码,乱码的问题
2016/12/30 Python
Python简单实现Base64编码和解码的方法
2017/04/29 Python
python使用Matplotlib绘制分段函数
2018/09/25 Python
python进行TCP端口扫描的实现
2018/12/21 Python
python安装读取grib库总结(推荐)
2020/06/24 Python
详解Django关于StreamingHttpResponse与FileResponse文件下载的最优方法
2021/01/07 Python
阿里云:Aliyun.com
2017/02/15 全球购物
用缩写的指针比较"if(p)" 检查空指针是否可靠?如果空指针的内部表达不是0会怎么样?
2014/01/05 面试题
教师应聘个人求职信
2013/12/10 职场文书
教师作风整改措施思想汇报
2014/10/12 职场文书
年会邀请函范文
2015/01/30 职场文书
MBA推荐信怎么写
2015/03/25 职场文书
撤诉申请书法院范本
2015/05/18 职场文书
ThinkPHP5和ThinkPHP6的区别
2021/03/31 PHP
java设计模式--原型模式详解
2021/07/21 Java/Android
一定要知道的 25 个 Vue 技巧
2021/11/02 Vue.js
SQLServer RANK() 排名函数的使用
2022/03/23 SQL Server