外觀
functions.php
php
<?php
/**
* ============================================================
* woodmart-child / functions.php ── 交接版(含繁中摘要註解)
* ============================================================
* 本檔案為原 functions.php 的「閱讀用副本」(.txt 不會被 WordPress 載入),
* 在每個 function 上方加入 3-5 行繁中說明,方便新團隊快速理解。
* 程式邏輯與原檔 100% 一致;如需修改請改原檔再同步本檔。
*
* 模組分區(行號為原檔行號,本檔含註解後會略有偏移):
* A 1-58 資產載入、版本控制、編輯器字型
* B 60-86 WooCommerce 運費邏輯
* C 88-150 訂單詳情頁追蹤(GA4、affiliate)
* D 152-278 ★ 藍新超商取貨訂單狀態管理
* E 280-328 後台訂單列表「需出貨」欄位
* F 330-490 前端最佳化(emoji/embed 移除、DNS prefetch、defer)
* G 492-727 ★ 結帳欄位客製(地址、條件式顯示)
* H 729-770 6H 配送注意事項彈窗
* I 772-838 追蹤碼(LINE Tag、FB 驗證、GTM)
* J 840-1144 覆寫父主題 woodmart_page_title()
* K 1147-1212 訂單滯留檢查(停用中)
* L 1214-1262 AddToAny / 優惠券 email 修正
* M 1264-1279 SEO:英文網址 noindex
* N 1281-1337 ★ LINE Pay 狀態異常自動修復
* O 1339-1377 結帳性別欄位
* P 1379-1408 Yoast robots.txt 修正
* Q 1410-1482 學生角色機制
* R 1484-1597 GPT 問答 shortcode + AJAX
* S 1599-1667 相關商品排除分類
* T 1669-1802 ★ 自訂超商取貨運送方式類別
* U 1804-2152 ★★★ 藍新 × 運送方式整合 JS(最複雜)
* V 2154-2175 後端阻擋 LINE Pay+超商
* W 2177-2271 ★★ 藍新付款參數補正
* X 2273-2372 ★★ 動態關閉藍新超商付款設定
* Y 2374-2397 藍新自動跳轉 3 秒
* ============================================================
*/
// ========== 模組 A:資產載入、版本控制、編輯器字型 ==========
/**
* 【功能】載入子主題的 style.css 與 main.min.js,並用 filemtime 自動帶版本號(破快取)。
* 【觸發】wp_enqueue_scripts,priority 1000(最後載入,確保覆蓋父主題)。
* 【備註】依賴父主題的 'woodmart-style' handle;JS 依賴 jQuery。
*/
function woodmart_child_enqueue_styles() {
$css_file = get_stylesheet_directory() . '/style.css';
$css_version = file_exists($css_file) ? filemtime($css_file) : '1.0.0';
wp_enqueue_style( 'child-style', get_stylesheet_directory_uri() . '/style.css', array( 'woodmart-style' ), $css_version );
$js_file = get_stylesheet_directory() . '/js/main.min.js';
$js_version = file_exists($js_file) ? filemtime($js_file) : '1.0.0';
wp_enqueue_script( 'custom-script', get_stylesheet_directory_uri() . '/js/main.min.js', array( 'jquery' ), $js_version, true );
}
add_action( 'wp_enqueue_scripts', 'woodmart_child_enqueue_styles', 1000 );
/**
* 【功能】把所有屬於父/子主題的 CSS/JS URL 後面的 ?ver= 版本號,改寫為該檔案的修改時間 (filemtime)。
* 【觸發】script_loader_src / style_loader_src,priority 15。
* 【效果】無須手動 bump 版本,檔案一改、瀏覽器自動破快取。
*/
function crave_script_version_with_filemtime( $src ) {
if ( strpos( $src, get_stylesheet_directory_uri() ) === false && strpos( $src, get_template_directory_uri() ) === false ) {
return $src;
}
$parsed_url = parse_url( $src );
if ( ! isset( $parsed_url['path'] ) ) {
return $src;
}
$file_path = str_replace(
array( get_stylesheet_directory_uri(), get_template_directory_uri() ),
array( get_stylesheet_directory(), get_template_directory() ),
$src
);
$file_path = strtok( $file_path, '?' );
if ( file_exists( $file_path ) ) {
$version = filemtime( $file_path );
$src = strtok( $src, '?' );
$src = add_query_arg( 'ver', $version, $src );
}
return $src;
}
add_filter( 'script_loader_src', 'crave_script_version_with_filemtime', 15, 1 );
add_filter( 'style_loader_src', 'crave_script_version_with_filemtime', 15, 1 );
/**
* 【功能】為 TinyMCE 編輯器擴充中文字型(微軟正黑體、新細明體、標楷體、黑體等)與英文字型清單。
* 【觸發】tiny_mce_before_init filter。
* 【備註】後台貼文/頁面編輯器才會用到。
*/
function add_custum_fontfamily($initArray){
// 中文字型
$initArray['font_formats'] = "微軟正黑體=微軟正黑體,Microsoft JhengHei;新細明體=PMingLiU,新細明體;標楷體=標楷體,DFKai-SB,BiauKai;黑體=黑體,SimHei,Heiti TC;微軟雅黑體Light=微軟雅黑體Light,Microsoft YaHei Light;";
// 英文字型
$initArray['font_formats'] .= "Andale Mono=andale mono,times;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier;Georgia=georgia,palatino;Helvetica=helvetica;Impact=impact,chicago;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco;Times New Roman=times new roman,times;Trebuchet MS=trebuchet ms,geneva;Verdana=v erdana,geneva;Webdings=webdings;Wingdings=wingdings,zapf dingbats;";
return $initArray;
}
add_filter('tiny_mce_before_init', 'add_custum_fontfamily');
// ========== 模組 B:WooCommerce 運費邏輯 ==========
/**
* 【功能】滿額免運顯示規則:當有 free_shipping 時,原本會隱藏其他運送方式;
* 本函式特例保留「超商取貨」(newebpay_cvscom)和「6H 配送」(flat_rate:16),
* 且 6H 配送在免運時改收 150 元(不享免運)。
* 【觸發】woocommerce_package_rates,priority 100。
*/
function my_hide_shipping_when_free_is_available( $rates ) {
$free = array();
foreach ( $rates as $rate_id => $rate ) {
if ( 'free_shipping' === $rate->method_id ) {
$free[ $rate_id ] = $rate;
}
}
if (!empty($free)) {
foreach ( $rates as $rate_id => $rate ) {
// 免運費時 仍顯示超商取貨
if ( 'newebpay_cvscom' === $rate->method_id ){
$free[ $rate_id ] = $rate;
}
//免運時,仍顯示6H配送,改為150
if('flat_rate:16' === $rate->id ){
$rate->cost = 150;
$free[$rate_id] = $rate;
}
}
}
return ! empty( $free ) ? $free : $rates;
}
add_filter( 'woocommerce_package_rates', 'my_hide_shipping_when_free_is_available', 100 );
// ========== 模組 C:訂單詳情頁追蹤(GA4、Affiliate) ==========
/**
* 【功能】在訂單完成頁面(thank-you / 訂單詳情)輸出 GA4 purchase event,回傳訂單金額與幣別 TWD。
* 【觸發】woocommerce_order_details_after_customer_details。
* 【GA4】Measurement ID:G-Z5C8WK0J0F。
*/
function wh_CustomReadOrder($order_id)
{
//getting order object
$order = wc_get_order($order_id);
// 檢查訂單是否存在
if (!$order) {
return;
}
$order_id = $order->get_id();
$order_data = $order->get_data();
$order_total = $order_data['total'];
?>
<script type="text/javascript">
//GA4
gtag('event', 'purchase', {
'send_to': 'G-Z5C8WK0J0F',
'value': '<?php echo esc_js($order_total); ?>',
'currency': 'TWD'
});
</script>
<?php
}
add_action('woocommerce_order_details_after_customer_details', 'wh_CustomReadOrder');
/**
* 【功能】[affiliates] shortcode:在頁面輸出 VBTrax 聯盟追蹤的 conversion script,
* 從 ?order=ID 取訂單金額並回傳給 vbtrax.com。
* 【安全】已修正 XSS:用 absint() 過濾 GET 參數。
* 【用途】放在訂單完成頁,供聯盟行銷追蹤轉換。
*/
function affiliates_trank(){
//getting order object
global $woocommerce;
// 修正 XSS 風險:驗證和清理 GET 參數
$order_id = isset($_GET['order']) ? absint($_GET['order']) : 0;
if($order_id > 0){
$order = wc_get_order($order_id);
// 檢查訂單是否存在
if (!$order) {
return '';
}
$order_data = $order->get_data();
$order_total = $order_data["total"];
$trank_code .= "<script> (function () {var VARemoteLoadOptions = {whiteLabel: { id: 8, siteId: 1784, domain: 'vbtrax.com' }, conversion: true,conversionData: {step: 'sale',orderTotal: '";
$trank_code .= $order_total."',";
$trank_code .= "order:'".$order_id."',";
$trank_code .= "},locale: 'en-US', mkt: true};(function (c, o, n, v, e, r, l, y) {c['VARemoteLoadOptions'] = e; r = o.createElement(n), l = o.getElementsByTagName(n)[0];r.async = 1; r.src = v; l.parentNode.insertBefore(r, l);})(window, document, 'script', 'https://cdn.vbtrax.com/javascripts/va.js', VARemoteLoadOptions);})(); </script>";
return $trank_code;}
}
add_shortcode('affiliates', 'affiliates_trank');
/**
* ========================================
* 藍新金流超商取貨付款訂單狀態管理
* ========================================
* 目標:
* 1. 超商取貨選擇門市後 → pending(等待付款中)
* 2. 顧客實際取貨付款後 → completed(已完成)
* 3. 不修改外掛檔案,用 hook 覆蓋邏輯
*
* 策略:在外掛設定狀態後立即覆蓋
*/
/**
* 主要方法:監控所有狀態變更並立即修正
* 使用最高優先級確保在所有其他 hook 之後執行
*
* ⚠️ 超商取貨付款流程:
* 1. 顧客下單選門市 → pending(等待付款中)
* 2. 門市資訊確認(藍新第一次回傳)→ 強制改回 pending(等待顧客取貨付款)
* 3. 顧客到超商取貨付款(藍新第二次回傳,有「付款時間」)→ 維持 completed
*/
// ========== 模組 D:藍新超商取貨訂單狀態管理(核心,最易出錯) ==========
/**
* 【功能】★ 攔截藍新超商取貨付款(CVSCOMPayed)訂單的狀態變更:
* - 顧客只是「選了門市」(藍新第一次回傳,備註只有店名)→ 強制改回 pending
* - 顧客「實際取貨付款」(藍新第二次回傳,備註含「付款時間」)→ 維持 completed
* 【觸發】woocommerce_order_status_changed,priority 999(最高,確保在外掛之後執行)。
* 【判斷】訂單備註同時有「藍新金流交易序號」+「付款時間」才算真正付款。
* 【關聯】newebpay_cvscom_manual_update(手動更新時補正)。
*/
add_action('woocommerce_order_status_changed', 'newebpay_cvscom_fix_status', 999, 4);
function newebpay_cvscom_fix_status($order_id, $old_status, $new_status, $order) {
// 只處理藍新金流訂單
if (!$order || $order->get_payment_method() !== 'newebpay') {
return;
}
// 取得訂單資訊
$selected_payment = $order->get_meta('_nwpSelectedPayment');
// 判斷是否為「超商取貨付款」訂單
// 超商取貨付款:顧客到超商才付款,在此之前都保持 pending 狀態
if ($selected_payment !== 'CVSCOMPayed') {
return;
}
// 檢查訂單備註,判斷是選擇門市還是實際付款
$notes = wc_get_order_notes(array(
'order_id' => $order_id,
'limit' => 20,
));
$has_payment_completed = false;
$has_store_note = false;
foreach ($notes as $note) {
// 檢查是否有「真正的」付款完成備註
// 必須同時有「藍新金流交易序號」和「付款時間」才算付款完成
// 因為第一次回傳(選門市)只有交易序號,沒有付款時間
// 第二次回傳(實際付款)才會有付款時間
if (strpos($note->content, '藍新金流交易序號') !== false
&& strpos($note->content, '付款時間') !== false) {
$has_payment_completed = true;
}
// 檢查是否有門市選擇備註
if (strpos($note->content, '超商取貨資訊') !== false || strpos($note->content, '店家:') !== false) {
$has_store_note = true;
}
}
// ⚠️ 核心邏輯:
// 如果已經有「付款時間」,表示顧客真的付款了,不要改回 pending
if ($has_payment_completed) {
return;
}
// 沒有付款時間,表示只是選了門市,強制改回 pending
if ($new_status === 'processing' || $new_status === 'completed') {
remove_action('woocommerce_order_status_changed', 'newebpay_cvscom_fix_status', 999);
if ($has_store_note) {
$order->set_status('pending', '超商取貨付款訂單:門市已選定,等待顧客取貨付款', true);
} else {
$order->set_status('pending', '超商取貨付款:等待顧客選擇門市並取貨付款', true);
}
$order->save();
add_action('woocommerce_order_status_changed', 'newebpay_cvscom_fix_status', 999, 4);
}
}
/**
* 後台手動更新:攔截「至藍新更新交易狀態」按鈕
* ⚠️ 超商取貨付款訂單:只有在尚未付款時才強制保持 pending 狀態
*/
/**
* 【功能】後台點擊「至藍新更新交易狀態」按鈕時的補正:
* 若是超商取貨付款訂單但備註尚無「付款時間」,強制保持 pending。
* 【觸發】woocommerce_payment_complete,priority 20。
* 【關聯】與 newebpay_cvscom_fix_status 同邏輯,但攔截不同 hook。
*/
add_action('woocommerce_payment_complete', 'newebpay_cvscom_manual_update', 20, 1);
function newebpay_cvscom_manual_update($order_id) {
$order = wc_get_order($order_id);
if (!$order || $order->get_payment_method() !== 'newebpay') {
return;
}
// 檢查是否為超商取貨付款訂單
$selected_payment = $order->get_meta('_nwpSelectedPayment');
if ($selected_payment !== 'CVSCOMPayed') {
return;
}
// 檢查訂單備註,判斷是否已經實際付款
$notes = wc_get_order_notes(array(
'order_id' => $order_id,
'limit' => 20,
));
$has_payment_completed = false;
foreach ($notes as $note) {
// 必須同時有「藍新金流交易序號」和「付款時間」才算付款完成
if (strpos($note->content, '藍新金流交易序號') !== false
&& strpos($note->content, '付款時間') !== false) {
$has_payment_completed = true;
break;
}
}
// 如果已經有付款時間,不要改回 pending
if ($has_payment_completed) {
return;
}
// 沒有付款時間,強制保持 pending 狀態
if ($order->get_status() === 'processing' || $order->get_status() === 'completed') {
$order->set_status('pending', '超商取貨付款:等待顧客取貨付款', true);
$order->save();
}
}
// ========== 模組 E:後台訂單列表「需出貨」欄位 ==========
/**
* 【功能】在後台訂單列表插入「需出貨」欄位(緊接在訂單狀態欄之後)。
* 【觸發】manage_edit-shop_order_columns,priority 20。
*/
add_filter( 'manage_edit-shop_order_columns', 'custom_shop_order_column', 20 );
function custom_shop_order_column($columns){
$reordered_columns = array();
// Inserting columns to a specific location
foreach( $columns as $key => $column){
$reordered_columns[$key] = $column;
if( $key == 'order_status' ){
// Inserting after "Status" column
$reordered_columns['shipping_column'] = '需出貨';
}
}
return $reordered_columns;
}
/**
* 【功能】「需出貨」欄位的內容渲染:
* - 超商取貨且已選店 → 紅字「超取需出貨」
* - 6H 配送 → 黃字「6H出貨」
* 【觸發】manage_shop_order_posts_custom_column,priority 20。
*/
add_action( 'manage_shop_order_posts_custom_column' , 'custom_orders_list_column_content', 20, 2 );
function custom_orders_list_column_content( $column, $post_id ){
switch ( $column ){
case 'shipping_column' :
// An instance of
$order = wc_get_order($post_id);
// Iterating through order shipping items
// https://stackoverflow.com/questions/46102428/get-orders-shipping-items-details-in-woocommerce-3
foreach( $order->get_items( 'shipping' ) as $item_id => $shipping_item_obj ){
// Get the data in an unprotected array
$shipping_item_data = $shipping_item_obj->get_data();
$shipping_data_name = $shipping_item_data['name'];
$shipping_data_method_title = $shipping_item_data['method_title'];
$shipping_data_method_id = $shipping_item_data['method_id'];
// shipping_data_name: 超商取貨付款
// shipping_data_method_title: 超商取貨付款
// shipping_data_method_id: newebpay_cvscom
if( $shipping_data_method_title == '超商取貨付款' || $shipping_data_method_title == '超商取貨' ){
$store_name = $order->get_meta('_newebpayStoreName');
if ($store_name !== '') {
echo '<span style="color: #ca4a1f">超取需出貨</span>';
}
}else if($shipping_data_method_title == '6H配送'){
echo "<span style='color:#FDAF01;'>6H出貨</span>";
}else{
echo '';
}
}
break;
}
}
// ========== 模組 F:前端最佳化 ==========
/**
* 【功能】幫除了 jquery.js 之外的所有 script 標籤加上 defer 屬性,提升前端載入速度。
* 【觸發】script_loader_tag(目前已被註解停用,line 348)。
* 【備註】因相容性問題暫時關閉;要啟用時請充分測試。
*/
function crave_js_defer_attr($tag) {
if (is_admin()) {
return $tag;
}
// Do not add defer attribute to these scripts
$scripts_to_exclude = array('jquery.js'); // add a string of js file e.g. script.js
foreach($scripts_to_exclude as $exclude_script) {
if (true == strpos($tag, $exclude_script ) )
return $tag;
}
// Defer all remaining scripts not excluded above
return str_replace( ' src', ' defer src', $tag );
}
// jQuery issue fix CTKpro
// add_filter( 'script_loader_tag', 'crave_js_defer_attr', 10);
/**
* Remove junk from head
*/
/**
* 【功能】移除 WordPress 在 head 輸出的版本號(generator meta),避免被探測攻擊面。
* 【觸發】the_generator filter。
* 【備註】緊接著的 remove_action 也清掉一系列 wp_head 雜訊(RSD、wlwmanifest、feed、shortlink 等)。
*/
// remove WordPress version number
function crave_remove_version() {
return '';
}
add_filter('the_generator', 'crave_remove_version');
remove_action('wp_head', 'wp_generator');
remove_action('wp_head', 'rsd_link'); // remove really simple discovery (RSD) link
remove_action('wp_head', 'wlwmanifest_link'); // remove wlwmanifest.xml (needed to support windows live writer)
remove_action('wp_head', 'feed_links', 2); // remove rss feed links (if you don't use rss)
remove_action('wp_head', 'feed_links_extra', 3); // removes all extra rss feed links
remove_action('wp_head', 'index_rel_link'); // remove link to index page
remove_action('wp_head', 'start_post_rel_link', 10, 0); // remove random post link
remove_action('wp_head', 'parent_post_rel_link', 10, 0); // remove parent post link
remove_action('wp_head', 'adjacent_posts_rel_link', 10, 0); // remove the next and previous post links
remove_action('wp_head', 'adjacent_posts_rel_link_wp_head', 10, 0 );
remove_action('wp_head', 'wp_shortlink_wp_head', 10, 0 ); // remove shortlink
/**
* 【功能】關閉 WordPress 的 emoji 載入(移除 emoji JS、CSS、TinyMCE plugin、DNS prefetch)。
* 【觸發】init action。
* 【效果】減少前端 HTTP 請求,加快載入。
*/
function crave_disable_emojis() {
remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
remove_action( 'wp_print_styles', 'print_emoji_styles' );
remove_action( 'admin_print_styles', 'print_emoji_styles' );
remove_filter( 'the_content_feed', 'wp_staticize_emoji' );
remove_filter( 'comment_text_rss', 'wp_staticize_emoji' );
remove_filter( 'wp_mail', 'wp_staticize_emoji_for_email' );
add_filter( 'tiny_mce_plugins', 'crave_disable_emojis_tinymce' );
add_filter( 'wp_resource_hints', 'crave_disable_emojis_remove_dns_prefetch', 10, 2 );
}
add_action( 'init', 'crave_disable_emojis' );
/**
* Filter function used to remove the tinymce emoji plugin.
*
* @param array $plugins
* @return array Difference betwen the two arrays
*/
/**
* 【功能】從 TinyMCE 外掛清單移除 wpemoji,配合 crave_disable_emojis()。
*/
function crave_disable_emojis_tinymce( $plugins ) {
if ( is_array( $plugins ) ) {
return array_diff( $plugins, array( 'wpemoji' ) );
} else {
return array();
}
}
/**
* Remove emoji CDN hostname from DNS prefetching hints.
*
* @param array $urls URLs to print for resource hints.
* @param string $relation_type The relation type the URLs are printed for.
* @return array Difference betwen the two arrays.
*/
/**
* 【功能】從 DNS prefetch hints 中移除 emoji CDN(s.w.org),配合 crave_disable_emojis()。
*/
function crave_disable_emojis_remove_dns_prefetch( $urls, $relation_type ) {
if ( 'dns-prefetch' == $relation_type ) {
/** This filter is documented in wp-includes/formatting.php */
$emoji_svg_url = apply_filters( 'emoji_svg_url', 'https://s.w.org/images/core/emoji/2/svg/' );
$urls = array_diff( $urls, array( $emoji_svg_url ) );
}
return $urls;
}
/**
* 【功能】全面關閉 WordPress 的 oEmbed 功能(REST 端點、TinyMCE、rewrite 規則、JS 等)。
* 【觸發】init action,priority 9999(晚於外掛註冊)。
*/
function crave_disable_embeds() {
// Remove the REST API endpoint.
remove_action( 'rest_api_init', 'wp_oembed_register_route' );
// Turn off oEmbed auto discovery.
add_filter( 'embed_oembed_discover', '__return_false' );
// Don't filter oEmbed results.
remove_filter( 'oembed_dataparse', 'wp_filter_oembed_result', 10 );
// Remove oEmbed discovery links.
remove_action( 'wp_head', 'wp_oembed_add_discovery_links' );
// Remove oEmbed-specific JavaScript from the front-end and back-end.
remove_action( 'wp_head', 'wp_oembed_add_host_js' );
add_filter( 'tiny_mce_plugins', 'crave_disable_embeds_tiny_mce_plugin' );
// Remove all embeds rewrite rules.
add_filter( 'rewrite_rules_array', 'crave_disable_embeds_rewrites' );
// Remove filter of the oEmbed result before any HTTP requests are made.
remove_filter( 'pre_oembed_result', 'wp_filter_pre_oembed_result', 10 );
}
add_action( 'init', 'crave_disable_embeds', 9999 );
/**
* 【功能】從 TinyMCE 外掛清單移除 wpembed,配合 crave_disable_embeds()。
*/
function crave_disable_embeds_tiny_mce_plugin($plugins) {
return array_diff($plugins, array('wpembed'));
}
/**
* 【功能】移除所有 oEmbed 相關的 rewrite 規則。
*/
function crave_disable_embeds_rewrites($rules) {
foreach($rules as $rule => $rewrite) {
if(false !== strpos($rewrite, 'embed=true')) {
unset($rules[$rule]);
}
}
return $rules;
}
/**
* 【功能】前台移除 jQuery Migrate(避免舊版 plugin 的相容性負擔)。
* 【觸發】wp_default_scripts action。
* 【備註】後台仍保留以避免破壞 admin UI;jQuery 版本鎖在 1.12.4。
*/
function crave_remove_jquery_migrate( &$scripts) {
if(!is_admin()) {
$scripts->remove('jquery');
$scripts->add('jquery', false, array( 'jquery-core' ), '1.12.4');
}
}
add_action( 'wp_default_scripts', 'crave_remove_jquery_migrate' );
/**
* 【功能】在 wp_head 最前端輸出 DNS prefetch hints,預先解析常用第三方域名(typekit、cdnjs、googleapis、GA 等)。
* 【觸發】wp_head action,priority 0(最先輸出)。
*/
function dns_prefetch() {
// 修正邏輯:預設啟用 DNS prefetch
$prefetch = 'on';
echo '<meta http-equiv="x-dns-prefetch-control" content="' . esc_attr($prefetch) . '">';
// 只有當 DNS prefetch 啟用時才加入預連接的域名
if ($prefetch == 'on') {
$dns_domains = array(
"//use.typekit.net",
"//netdna.bootstrapcdn.com",
"//cdnjs.cloudflare.com",
"//ajax.googleapis.com",
"//s0.wp.com",
"//s.gravatar.com",
"//stats.wordpress.com",
"//www.google-analytics.com"
);
foreach ($dns_domains as $domain) {
if (!empty($domain)) {
echo '<link rel="dns-prefetch" href="' . esc_url($domain) . '" />';
}
}
}
}
add_action( 'wp_head', 'dns_prefetch', 0 );
// ========== 模組 G:結帳欄位客製(地址、條件式顯示) ==========
/**
* 【功能】在結帳頁將所有地址欄位(country/address_1/address_2/city/state/postcode)的 required 設為 false。
* 實際必填驗證改由 wps_select_checkout_field_process() 依運送方式動態判斷。
* 【觸發】woocommerce_default_address_fields,priority 20。
*/
add_filter( 'woocommerce_default_address_fields' , 'filter_default_address_fields', 20, 1 );
function filter_default_address_fields( $address_fields ) {
// Only on checkout page
if( ! is_checkout() ) return $address_fields;
// All field keys in this array
$key_fields = array('country','address_1','address_2','city','state','postcode', 'state', 'city');
// Loop through each address fields (billing and shipping)
foreach( $key_fields as $key_field )
$address_fields[$key_field]['required'] = false;
return $address_fields;
}
/**
* 【功能】在結帳頁注入 jQuery:根據運送方式動態顯示/隱藏地址欄位。
* - 選超商取貨(newebpay_cvscom,含 instance ID 如 newebpay_cvscom:20)→ 隱藏所有地址欄位
* - 選宅配且國家=TW → 顯示所有地址欄位(必填)
* - 選宅配且國家≠TW → 顯示但不要求 address_2/postcode/state/city
* 【觸發】wp_footer action(每頁底部,內含 is_checkout 判斷實際使用情境在結帳頁)。
* 【關聯】wps_select_checkout_field_process()(後端對應驗證)。
*/
add_action( 'wp_footer', 'custom_checkout_field_script' );
function custom_checkout_field_script() {
// HERE your shipping methods rate IDs
$newebpay_cvscom = 'newebpay_cvscom';
$required_text = esc_attr__( 'required', 'woocommerce' );
$required_html = '<abbr class="required" title="' . $required_text . '">*</abbr>';
?>
<script>
jQuery(function($){
var ism = 'input[name^="shipping_method"]',
ismc = ism+':checked',
rq = '-required',
vr = 'validate'+rq,
w = 'woocommerce',
wv = w+'-validated',
iv = '-invalid',
fi = '-field',
wir = w+iv+' '+w+iv+rq+fi,
b = '#billing_',
f = '_field',
a1 = 'country',
a2 = 'address_1',
a3 = 'address_2',
a4 = 'postcode',
a5 = 'state',
a6 = 'city',
b1 = b+a1+f,
b2 = b+a2+f,
b3 = b+a3+f,
b4 = b+a4+f,
b5 = b+a5+f,
b6 = b+a6+f,
newebpay = '<?php echo $newebpay_cvscom; ?>';
// Utility function to shows or hide checkout fields
function showHide( action='show', selector='' ){
if( action == 'show' ){
$(selector).show(function(){
// console.log('show');
// console.log($(selector));
$(this).addClass(vr);
$(this).removeClass(wv);
$(this).removeClass(wir);
// console.log($(selector+' > label > abbr').html());
// console.log( $(selector+' label') );
if( $(selector+' > label > abbr').html() == undefined ){
$(selector+' > label span.optional').remove();
$(selector+' label').append('<?php echo $required_html; ?>');
}
});
}else if( action == 'requiredfalse'){
$(selector).show(function(){
// console.log('requiredfalse');
// console.log($(selector));
$(this).removeClass(vr);
$(this).removeClass(wv);
$(this).removeClass(wir);
// console.log($(selector+' > label > abbr').html());
// console.log( $(selector+' label > .required') );
$(selector+' label > .required').remove();
});
}else{
$(selector).hide(function(){
// console.log('hide');
$(this).removeClass(vr);
$(this).removeClass(wv);
$(this).removeClass(wir);
if( $(selector+' > label > abbr').html() != undefined )
$(selector+' label > .required').remove();
});
}
}
// 檢查是否為超商取貨(支援 instance ID,例如 newebpay_cvscom:20)
function isCvscomShipping() {
var selectedShipping = $(ismc).val();
return selectedShipping && selectedShipping.indexOf(newebpay) !== -1;
}
// Initializing at start after checkout init (Based on the chosen shipping method)
setTimeout(function(){
if( isCvscomShipping() ){
showHide('hide',b1);
showHide('hide',b2);
showHide('hide',b3);
showHide('hide',b4);
showHide('hide',b5);
showHide('hide',b6);
}else{
showHide('show',b1);
showHide('show',b2);
showHide('show',b3);
showHide('show',b4);
showHide('show',b5);
showHide('show',b6);
}
}, 100);
// When shipping method is changed (Live event)
$( 'form.checkout' ).on( 'change', ism, function() {
if( isCvscomShipping() ){
showHide('hide',b1);
showHide('hide',b2);
showHide('hide',b3);
showHide('hide',b4);
showHide('hide',b5);
showHide('hide',b6);
}else{
showHide('show',b1);
showHide('show',b2);
showHide('show',b3);
showHide('show',b4);
showHide('show',b5);
showHide('show',b6);
}
});
// 若選擇國外
$( 'form.checkout' ).on( 'change', '#billing_country', function() {
// console.log('enter');
if( $(this).val() == 'TW' ){
console.log('TW');
showHide('show',b1);
showHide('show',b2);
showHide('show',b3);
showHide('show',b4);
showHide('show',b5);
showHide('show',b6);
}else{
// console.log('others');
showHide('show',b1);
showHide('show',b2);
showHide('requiredfalse',b3);
showHide('requiredfalse',b4);
showHide('requiredfalse',b5);
showHide('requiredfalse',b6);
}
});
});
</script>
<?php
}
// add_filter('filter_woocommerce_shipping_packages', function ($r) {
// $cs = WC()->session->get( 'chosen_shipping_methods' );
// error_log('chosen shipping: ' . print_r($cs, true));
// return $r;
// });
// add_filter( 'woocommerce_shipping_packages', 'filter_woocommerce_shipping_packages', 10, 2 );
/****
* checkout field檢查
*/
/**
* 【功能】結帳送出時的後端必填驗證(依運送方式分流):
* - 超商取貨:地址欄位全免
* - flat_rate:4 / flat_rate:6:只驗 address_1
* - flat_rate:16(6H 配送):須完整地址,且縣市須為「台北市/臺北市/新北市」+ 新北限定特定區
* - 其他(含國際):完整地址全部必填
* 【觸發】woocommerce_checkout_process action。
* 【關聯】custom_checkout_field_script()(前端顯示)。
*/
add_action('woocommerce_checkout_process', 'wps_select_checkout_field_process');
function wps_select_checkout_field_process() {
$newebpay_cvscom = 'newebpay_cvscom';
$chosen_shipping_method = WC()->session->get( 'chosen_shipping_methods' )[0];
$billing = '<strong> ' . __('訂購人的', 'woocommerce') . ' ';
$shipping = '<strong> ' . __('訂購人的', 'woocommerce') . ' ';
$country = __('國家', 'woocommerce');
$address1 = __('地址', 'woocommerce');
$postcode = __('郵遞區號', 'woocommerce');
$state = __('縣 / 市', 'woocommerce');
$city = __('鄉鎮市區', 'woocommerce');
$end_text = '</strong> '. __('為必填欄位。', 'woocommerce');
$not_matching = '</strong> '.__('不符合。','woocommerce');
// 使用 strpos 檢查是否為超商取貨(支援 instance ID,如 newebpay_cvscom:20)
if( strpos($chosen_shipping_method, $newebpay_cvscom) !== false ) {
// 超商取貨不需要驗證地址欄位
}elseif( $chosen_shipping_method == 'flat_rate:4' ||
$chosen_shipping_method == 'flat_rate:6'){
if( empty($_POST['billing_address_1']) )
wc_add_notice( $billing . $address1 . $end_text, 'error' );
// if( empty($_POST['billing_state']) )
// wc_add_notice( $billing . $state . $end_text, 'error' );
}elseif($chosen_shipping_method == 'flat_rate:16'){
//6H配送檢查
if( empty($_POST['billing_country']) )
wc_add_notice( $billing . $country . $end_text, 'error' );
if( empty($_POST['billing_address_1']) )
wc_add_notice( $billing . $address1 . $end_text, 'error' );
if( empty($_POST['billing_postcode']) )
wc_add_notice( $billing . $postcode . $end_text, 'error' );
if( empty($_POST['billing_state']) )
wc_add_notice( $billing . $state . $end_text, 'error' );
if( empty($_POST['billing_city']) )
wc_add_notice( $billing . $city . $end_text, 'error' );
if($_POST['billing_state']!='台北市' && $_POST['billing_state']!='臺北市' && $_POST['billing_state']!='新北市'){
wc_add_notice($billing . $postcode . $not_matching, 'error');
}
if($_POST['billing_state']=='新北市'){
if($_POST['billing_city']!='新店區' && $_POST['billing_city']!='永和區' && $_POST['billing_city']!='中和區' && $_POST['billing_city']!='樹林區' && $_POST['billing_city']!='三重區' && $_POST['billing_city']!='蘆洲區' && $_POST['billing_city']!='新莊區' && $_POST['billing_city']!='汐止區' && $_POST['billing_city']!='板橋區'){
wc_add_notice($billing . $postcode . $not_matching, 'error');
}
}
}
else {
if( empty($_POST['billing_country']) )
wc_add_notice( $billing . $country . $end_text, 'error' );
if( empty($_POST['billing_address_1']) )
wc_add_notice( $billing . $address1 . $end_text, 'error' );
if( empty($_POST['billing_postcode']) )
wc_add_notice( $billing . $postcode . $end_text, 'error' );
if( empty($_POST['billing_state']) )
wc_add_notice( $billing . $state . $end_text, 'error' );
if( empty($_POST['billing_city']) )
wc_add_notice( $billing . $city . $end_text, 'error' );
}
}
// ========== 模組 H:6H 配送注意事項彈窗 ==========
/**
* 【功能】結帳頁切換到 flat_rate:16(6H 配送)時,跳出 jquery-confirm 彈窗顯示出貨時段、
* 郵遞區號注意事項、年節期間提醒。
* 【觸發】wp_footer,僅在 is_checkout() 且非 endpoint 時輸出。
* 【依賴】CDN 載入 jquery-confirm 3.3.2。
*/
add_action( 'wp_footer', 'checkout_action_wp_footer', 10, 0 );
function checkout_action_wp_footer(){
if ( is_checkout() && ! is_wc_endpoint_url() ) {
//add js code to check shipping methos choosen
?>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-confirm/3.3.2/jquery-confirm.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-confirm/3.3.2/jquery-confirm.min.js"></script>
<script type="text/javascript">
jQuery(document).ready(function() {
function checkShow6HRAlert(){
var current_method = jQuery('input[name^="shipping_method"]:checked').val();
if(current_method == 'flat_rate:16'){
jQuery.alert({
title: '6H貨運注意事項:',
content: "<ol style='list-style: decimal;padding-left:20px;'>"+
'<li>訂單處理時間為週一至週五於9:30至15:00,當日15:00前下單之訂單可當日配達,若超過15:00下單則於下個工作日完成送件。(週五15:00後下單之訂單則於隔週一開始進行配送)</li>'+
'<li>下單前請確認郵遞區號、住址以及收件人資訊是否填寫正確,請確認收件地址皆有人可收件。</li>'+
'<li>若因天災、年節期間貨運量大等因素影響物流運送時程,將另行通知。</li>'+
"</ol>",
});
}
}
// checkShow6HRAlert();
// On live "change event"
jQuery(document.body ).on( 'change', 'input[name^="shipping_method"]', function() {
checkShow6HRAlert();
});
})
</script>
<!-- <div id="6hdialog">
<p>6H貨運注意事項:</p>
<ol>
<li>最快二小時到貨,最慢六小時到貨。請確認在這段時間內自己都是可以收件的喔~</li>
<li>週一至週五於9:30至16:00前下訂單及當天配達,若超過下午四點則下個工作日中午12:30開始寄送。</li>
<li>統一配送時間為週一至週五 中午 12:30 和下午4:00 (週五下午四點過後訂單則統一於週一開始進行配送喔)</li>
<li>請確認郵遞區號、住址以及收件人資訊是否填寫正確喔!</li>
</ol>
</div> -->
<?php
}
}
// ========== 模組 I:追蹤碼(LINE Tag、FB 驗證、GTM) ==========
/**
* 【功能】輸出 LINE Tag base code(pv 事件),結帳頁額外送 cv(轉換)事件。
* 【觸發】wp_head action。
* 【tagId】7873c94a-88a8-4847-a533-5fff1f881a8b
*/
add_action('wp_head','set_line_tag');
function set_line_tag(){
echo "<!-- LINE Tag Base Code -->
<!-- Do Not Modify -->
<script>
(function(g,d,o){
g._ltq=g._ltq||[];g._lt=g._lt||function(){g._ltq.push(arguments)};
var h=location.protocol==='https:'?'https://d.line-scdn.net':'http://d.line-cdn.net';
var s=d.createElement('script');s.async=1;
s.src=o||h+'/n/line_tag/public/release/v1/lt.js';
var t=d.getElementsByTagName('script')[0];t.parentNode.insertBefore(s,t);
})(window, document);
_lt('init', {
customerType: 'lap',
tagId: '7873c94a-88a8-4847-a533-5fff1f881a8b'
});
_lt('send', 'pv', ['7873c94a-88a8-4847-a533-5fff1f881a8b']);
</script>
<noscript>
<img height='1' width='1' style='display:none'
src='https://tr.line.me/tag.gif?c_t=lap&t_id=7873c94a-88a8-4847-a533-5fff1f881a8b&e=pv&noscript=1' />
</noscript>
<!-- End LINE Tag Base Code -->";
if( is_checkout() ){
echo "
<script>
_lt('send', 'cv', { type: 'Conversion' },['7873c94a-88a8-4847-a533-5fff1f881a8b']);
</script>";
}
}
/**
* 【功能】從 wp_posts 撈出標題含 'facebook_verification' 的文章,將其內容輸出為 FB 域名驗證 meta。
* 【觸發】wp_head action。
* 【安全】已修正 SQL injection:使用 wpdb::prepare() + esc_like()。
* 【設計】用一篇後台文章存驗證碼,方便非工程人員修改。
*/
add_action( 'wp_head', 'set_fb_verification_to_meta');
function set_fb_verification_to_meta(){
global $wpdb;
$title = 'facebook_verification';
// 修正 SQL Injection 風險:正確使用 wpdb::prepare()
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM $wpdb->posts WHERE post_title LIKE %s",
'%' . $wpdb->esc_like( $title ) . '%'
)
);
// 檢查查詢結果是否存在,避免陣列存取錯誤
if (empty($results)) {
return;
}
$p = $results[0];
$fb_verification_str = preg_replace("/\s/","", strip_tags($p->post_content) ) ?: '';
echo "<meta name='facebook-domain-verification' content='$fb_verification_str' />";
}
/**
* 【功能】輸出 Google Tag Manager 標準 snippet(GTM-PC8K325)到 wp_head。
* 【觸發】wp_head action。
* 【備註】GTM body 端的 <noscript> 部分若有需要請在 footer.php 補上。
*/
function set_gtm(){
echo "<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-PC8K325');</script>
<!-- End Google Tag Manager -->";
}
add_action('wp_head','set_gtm');
// **********************************************************************//
// Override Themes function -- themes/woodmart/inc/template-tags.php :line 748
// 增加breadcrumbs
// ! Page title function
// **********************************************************************//
// ========== 模組 J:覆寫父主題 woodmart_page_title(),加入 breadcrumbs ==========
/**
* 【功能】完整覆寫父主題(woodmart/inc/template-tags.php:748)的 woodmart_page_title(),
* 在頁面標題區塊加入 Breadcrumb NavXT 麵包屑(bcn_display),並支援自訂背景圖、顏色、設計風格。
* 【觸發】woodmart_after_header,priority 10;且只有在父主題未先定義時才注入。
* 【依賴】Breadcrumb NavXT 外掛、Woodmart options(page-title-design / size / color)。
* 【注意】父主題若改原始函式,本覆寫可能與新版功能脫鉤,升級時需比對。
*/
if( ! function_exists( 'woodmart_page_title' ) ) {
add_action( 'woodmart_after_header', 'woodmart_page_title', 10 );
function woodmart_page_title() {
global $wp_query, $post;
// Remove page title for dokan store list page
if( function_exists( 'dokan_is_store_page' ) && dokan_is_store_page() ) {
return '';
}
$page_id = 0;
$disable = false;
$page_title = true;
$breadcrumbs = woodmart_get_opt( 'breadcrumbs' );
$image = '';
$style = '';
$page_for_posts = get_option( 'page_for_posts' );
$page_for_shop = get_option( 'woocommerce_shop_page_id' );
$page_for_projects = woodmart_tpl2id( 'portfolio.php' );
$title_class = 'page-title-';
$title_color = $title_type = $title_size = 'default';
// Get default styles from Options Panel
$title_design = woodmart_get_opt( 'page-title-design' );
$title_size = woodmart_get_opt( 'page-title-size' );
$title_color = woodmart_get_opt( 'page-title-color' );
$shop_title = woodmart_get_opt( 'shop_title' );
$shop_categories = woodmart_get_opt( 'shop_categories' );
$single_post_design = woodmart_get_opt( 'single_post_design' );
// Set here page ID. Will be used to get custom value from metabox of specific PAGE | BLOG PAGE | SHOP PAGE.
$page_id = woodmart_page_ID();
if( $page_id != 0 ) {
// Get meta value for specific page id
$disable = get_post_meta( $page_id, '_woodmart_title_off', true );
$image = get_post_meta( $page_id, '_woodmart_title_image', true );
$custom_title_color = get_post_meta( $page_id, '_woodmart_title_color', true );
$custom_title_bg_color = get_post_meta( $page_id, '_woodmart_title_bg_color', true );
if (!empty($image)) {
// 如果是陣列,取第一個值
if (is_array($image)) {
$image = array_shift($image);
}
// 確保是有效的 URL
if (is_string($image) && !empty($image)) {
$style .= "background-image: url(" . esc_url($image) . ");";
}
}
if( $custom_title_bg_color != '' ) {
$style .= "background-color: " . $custom_title_bg_color . ";";
}
if( $custom_title_color != '' && $custom_title_color != 'default' ) {
$title_color = $custom_title_color;
}
}
if ( $title_design == 'disable' ) $page_title = false;
if ( ! $page_title && ! $breadcrumbs ) $disable = true;
if ( is_single() && $single_post_design == 'large_image' ) $disable = false;
if ( $disable ) return;
$title_class .= $title_type;
$title_class .= ' title-size-' . $title_size;
$title_class .= ' title-design-' . $title_design;
if ( $single_post_design == 'large_image' && is_single() ) {
$title_class .= ' color-scheme-light';
}else{
$title_class .= ' color-scheme-' . $title_color;
}
if ( $single_post_design == 'large_image' && is_singular( 'post' ) ) {
$image_url = get_the_post_thumbnail_url( $page_id );
if ( $image_url && ! $style ) $style .= "background-image: url(" . $image_url . ");";
$title_class .= ' post-title-large-image';
?>
<div class="page-title <?php echo esc_attr( $title_class ); ?>" style="<?php echo esc_attr( $style ); ?>">
<div class="container">
<header class="entry-header">
<?php if ( get_the_category_list( ', ' ) ): ?>
<div class="meta-post-categories"><?php echo get_the_category_list( ', ' ); ?></div>
<?php endif ?>
<h1 class="entry-title"><?php the_title(); ?></h1>
<div class="entry-meta woodmart-entry-meta">
<?php woodmart_post_meta(array(
'labels' => 1,
'author' => 1,
'author_ava' => 1,
'date' => 1,
'edit' => 0,
'comments' => 1,
'short_labels' => 0
)); ?>
</div>
<!---CUSTOM CODE START-->
<div class="breadcrumbs grid-container grid-parent">
<?php if ( function_exists( 'bcn_display' ) ) {
bcn_display();
} ?>
</div>
<!---CUSTOM CODE END-->
</header>
</div>
</div>
<?php
return;
}
// Heading for pages
if( is_singular( 'page' ) && ( ! $page_for_posts || ! is_page( $page_for_posts ) ) ):
$title = get_the_title();
?>
<div class="page-title <?php echo esc_attr( $title_class ); ?>" style="<?php echo esc_attr( $style ); ?>">
<div class="container">
<header class="entry-header">
<?php if ( woodmart_woocommerce_installed() && ( is_cart() || is_checkout() ) ): ?>
<?php woodmart_checkout_steps(); ?>
<?php else: ?>
<?php if( $page_title ): ?><h1 class="entry-title"><?php echo esc_html( $title ); ?></h1><?php endif; ?>
<?php if ( $breadcrumbs ) woodmart_current_breadcrumbs( 'pages' ); ?>
<?php endif ?>
</header><!-- .entry-header -->
</div>
</div>
<?php
return;
endif;
// Heading for blog and archives
if( $single_post_design != 'large_image' && is_singular( 'post' ) || woodmart_is_blog_archive() ):
$title = ( ! empty( $page_for_posts ) ) ? get_the_title( $page_for_posts ) : esc_html__( 'Blog', 'woodmart' );
if( is_tag() ) {
$title = esc_html__( 'Tag Archives: ', 'woodmart') . single_tag_title( '', false ) ;
}
if( is_category() ) {
$title = '<span>' . single_cat_title( '', false ) . '</span>';
}
if( is_date() ) {
if ( is_day() ) :
$title = esc_html__( 'Daily Archives: ', 'woodmart') . get_the_date();
elseif ( is_month() ) :
$title = esc_html__( 'Monthly Archives: ', 'woodmart') . get_the_date( _x( 'F Y', 'monthly archives date format', 'woodmart' ) );
elseif ( is_year() ) :
$title = esc_html__( 'Yearly Archives: ', 'woodmart') . get_the_date( _x( 'Y', 'yearly archives date format', 'woodmart' ) );
else :
$title = esc_html__( 'Archives', 'woodmart' );
endif;
}
if ( is_author() ) {
/*
* Queue the first post, that way we know what author
* we're dealing with (if that is the case).
*
* We reset this later so we can run the loop
* properly with a call to rewind_posts().
*/
the_post();
$title = esc_html__( 'Posts by ', 'woodmart' ) . '<span class="vcard"><a class="url fn n" href="' . esc_url( get_author_posts_url( get_the_author_meta( 'ID' ) ) ) . '" title="' . esc_attr( get_the_author() ) . '" rel="me">' . get_the_author() . '</a></span>';
/*
* Since we called the_post() above, we need to
* rewind the loop back to the beginning that way
* we can run the loop properly, in full.
*/
rewind_posts();
}
if( is_search() ) {
$title = esc_html__( 'Search Results for: ', 'woodmart' ) . get_search_query();
}
?>
<div class="page-title <?php echo esc_attr( $title_class ); ?> title-blog" style="<?php echo esc_attr( $style ); ?>">
<div class="container">
<header class="entry-header">
<?php if( $page_title && is_single() ): ?>
<h3 class="entry-title"><?php echo wp_kses( $title, woodmart_get_allowed_html() ); ?></h3>
<?php elseif( $page_title ): ?>
<h1 class="entry-title"><?php echo wp_kses( $title, woodmart_get_allowed_html() ); ?></h1>
<?php endif; ?>
<?php if ( $breadcrumbs && ! is_search() ) woodmart_current_breadcrumbs( 'pages' ); ?>
</header><!-- .entry-header -->
</div>
</div>
<?php
return;
endif;
// Heading for portfolio
if( is_singular( 'portfolio' ) || woodmart_is_portfolio_archive() ):
$title = get_the_title( $page_for_projects );
if( is_tax( 'project-cat' ) ) {
$title = single_term_title( '', false );
}
?>
<div class="page-title <?php echo esc_attr( $title_class ); ?> title-blog" style="<?php echo esc_attr( $style ); ?>">
<div class="container">
<header class="entry-header">
<?php if( $page_title ): ?><h1 class="entry-title"><?php echo esc_html( $title ); ?></h1><?php endif; ?>
<?php if ( $breadcrumbs ) woodmart_current_breadcrumbs( 'pages' ); ?>
</header><!-- .entry-header -->
</div>
</div>
<?php
return;
endif;
// Page heading for shop page
if( woodmart_is_shop_archive()
&& ( $shop_categories || $shop_title )
):
if( is_product_category() ) {
$cat = $wp_query->get_queried_object();
$cat_image = woodmart_get_category_page_title_image( $cat );
if( $cat_image != '') {
$style = "background-image: url(" . $cat_image . ")";
}
}
if( is_product_category() || is_product_tag() ) {
$title_class .= ' with-back-btn';
}
if( ! $shop_title ) {
$title_class .= ' without-title';
}
?>
<?php if ( apply_filters( 'woocommerce_show_page_title', true ) && ! is_singular( "product" ) ) : ?>
<div class="page-title <?php echo esc_attr( $title_class ); ?> title-shop" style="<?php echo esc_attr( $style ); ?>">
<div class="container">
<div class="nav-shop">
<div class="shop-title-wrapper">
<?php if ( is_product_category() || is_product_tag() ): ?>
<?php woodmart_back_btn(); ?>
<?php endif ?>
<?php if ( $shop_title ): ?>
<h1 class="entry-title"><?php woocommerce_page_title(); ?></h1>
<?php endif ?>
</div>
<?php if( ! is_singular( "product" ) && $shop_categories ) woodmart_product_categories_nav(); ?>
</div>
</div>
</div>
<?php endif; ?>
<?php
return;
endif;
}
}
/**
* ⚠️ 此功能目前已停用
* 原功能:每日四次檢查訂單過久未付款,自動取消訂單
* 停用原因:排程設定被註解,功能無法正常運作
*
* 如需啟用此功能,請:
* 1. 取消下方程式碼的註解
* 2. 確認時區設定正確(目前使用 UTC-8)
* 3. 測試排程是否正常執行
*/
// add_action('admin_head','ctk_order_stay_too_long_daily_cron');
// function ctk_order_stay_too_long_daily_cron(){
// // 每日四次排程:檢查訂單過久未付款
// wp_clear_scheduled_hook('ctk_order_stay_too_long_daily_cron_0800');
// wp_clear_scheduled_hook('ctk_order_stay_too_long_daily_cron_1400');
// wp_clear_scheduled_hook('ctk_order_stay_too_long_daily_cron_2000');
// wp_clear_scheduled_hook('ctk_order_stay_too_long_daily_cron_0200');
// // if ( ! wp_next_scheduled( 'ctk_order_stay_too_long_daily_cron_0800' ) ) {
// // wp_schedule_single_event( strtotime('8:00:00') - (8*60*60) , 'ctk_order_stay_too_long_daily_cron_0800' );
// // }
// // if ( ! wp_next_scheduled( 'ctk_order_stay_too_long_daily_cron_1400' ) ){
// // wp_schedule_single_event( strtotime('14:00:00') - (8*60*60) , 'ctk_order_stay_too_long_daily_cron_1400');
// // }
// // if ( ! wp_next_scheduled( 'ctk_order_stay_too_long_daily_cron_2000' ) ){
// // wp_schedule_single_event( strtotime('20:00:00') - (8*60*60) , 'ctk_order_stay_too_long_daily_cron_2000');
// // }
// // if ( ! wp_next_scheduled( 'ctk_order_stay_too_long_daily_cron_0200' ) ){
// // wp_schedule_single_event( strtotime('2:00:00') - (8*60*60) , 'ctk_order_stay_too_long_daily_cron_0200');
// // }
// }
// add_action('ctk_order_stay_too_long_daily_cron_0800','ctk_check_order_stay_too_long',10);
// add_action('ctk_order_stay_too_long_daily_cron_1400','ctk_check_order_stay_too_long',10);
// add_action('ctk_order_stay_too_long_daily_cron_2000','ctk_check_order_stay_too_long',10);
// add_action('ctk_order_stay_too_long_daily_cron_0200','ctk_check_order_stay_too_long',10);
// ========== 模組 K:訂單滯留檢查(目前停用) ==========
/**
* 【功能】掃描過去 24 小時~此刻前 2 小時內仍為 pending 的訂單,
* 非超商取貨者直接取消('cancelled',備註:等待過久未付款)。
* 【狀態】★ 目前停用:上方排程註冊已被註解(line 1158-1184);保留作為未來啟用範本。
* 【啟用方式】取消上方註解,注意時區換算(採 UTC-8)。
*/
function ctk_check_order_stay_too_long(){
// 抓過去24小時內~此刻前兩小時的所有等待付款中訂單
$args = [
'date_created' => ( time() - DAY_IN_SECONDS ).'...'.( time() - 2*60*60 ),
'status' => ['pending'],
];
$orders = wc_get_orders( $args );
foreach ($orders as $order) {
$order_id = $order->get_id();
$is_store_pay = false;
$shipping = $order->get_items( 'shipping' );
foreach ($shipping as $shipping_item_obj) {
$shipping_item_data = $shipping_item_obj->get_data();
$shipping_data_method_title = $shipping_item_data['method_title'];
if( $shipping_data_method_title == '超商取貨付款' ){
$is_store_pay = true;
break;
}
}
if( !$is_store_pay ){
$order->update_status('cancelled', '等待過久未付款');
}
}
}
// ========== 模組 L:AddToAny / 優惠券 email 修正 ==========
/**
* 【功能】在 AddToAny 社群分享外掛中新增「LINE 加好友」服務,使用自訂 SVG 圖示。
* 【觸發】A2A_FOLLOW_services filter,priority 10。
* 【圖示】/wp-content/themes/woodmart-child/img/icons/line_icon.svg
*/
function addtoany_add_follow_services( $services ) {
$services['line_add_friend'] = array(
'name' => '新增 line 好友(超連結)',
'icon_url' => '/wp-content/themes/woodmart-child/img/icons/line_icon.svg',
'icon_width' => 32,
'icon_height' => 32,
'href' => '${id}',
);
return $services;
}
add_filter( 'A2A_FOLLOW_services', 'addtoany_add_follow_services', 10, 1 );
/**
* 【功能】修正 WooCommerce 優惠券 email 限制的大小寫敏感問題:
* 原生比對若大小寫不同會失敗,本函式改用 strcasecmp 比對。
* 【觸發】woocommerce_coupon_is_valid,priority 10。
* 【關聯】array_contains_case_insensitive() 工具函式。
*/
add_filter ( 'woocommerce_coupon_is_valid', 'wc_check_coupon_is_valid', 10, 2 );
function wc_check_coupon_is_valid( $result, $coupon ) {
$user = wp_get_current_user();
$restricted_emails = $coupon->get_email_restrictions();
if ( count ( $restricted_emails ) > 0 ){
if ( array_contains_case_insensitive ( $user->user_email, $restricted_emails ) ) {
return $result;
} else {
return false;
}
} else {
return $result;
}
}
/**
* 【功能】工具:判斷陣列是否包含指定字串(不分大小寫,使用 strcasecmp)。
*/
function array_contains_case_insensitive($needle, $haystack) {
foreach ($haystack as $item) {
if (strcasecmp($needle, $item) === 0) {
return true;
}
}
return false;
}
/*
? 目標: 不讓英文網址(en_us)被機器人爬到(Noindex / Nofollow)
! 限制 / 警告: NA
* 相關: Plugin - Yoast SEO
TODO 擴充功能: NA
普通註解
*/
// ========== 模組 M:SEO 英文版 noindex ==========
/**
* 【功能】當網址含 /en_us/ 時,強制 Yoast 輸出 noindex,nofollow,
* 避免英文版內容被搜尋引擎索引。
* 【觸發】wpseo_robots filter,priority 10。
* 【關聯】Yoast SEO 外掛。
*/
add_action( 'wpseo_robots', 'setNoindexIfEn', 10 );
function setNoindexIfEn( $robots ){
$url_check = $_SERVER['REQUEST_URI'];
if ( str_contains( $url_check, '/en_us/' ) ) {
return 'noindex, nofollow';
} else {
return $robots;
}
}
/*
? 目標: 設定 CRON Job 檢查訂單是否已用 Linepay 付款,若有更改訂單狀態,再用 mail 通知 Admin Linepay 訂單狀態轉換異常(從 處理中 到 失敗)
! 限制 / 警告: NA
* 相關: Plugin - SMTP + WooCommerce LINEPay Gateway
TODO 擴充功能: NA
普通註解
*/
// ========== 模組 N:LINE Pay 訂單狀態異常自動修復 ==========
/**
* 【功能】訂單由 processing → failed 時,註冊一個 5 分鐘後執行的單次 cron(check_linepay_success)。
* 用於攔截 LINE Pay 偶發的狀態誤判。
* 【觸發】woocommerce_order_status_changed,priority 10。
* 【關聯】check_linepay_success_handler() 是實際處理函式。
*/
add_action('woocommerce_order_status_changed', 'log_status', 10, 3);
function log_status($order_id, $old_status, $new_status){
// 若訂單由「處理中」轉為「失敗」
if ( 'processing' == $old_status && 'failed' == $new_status ){
// 計算5分鐘後的時間戳
$scheduled_time = time() + 5 * 60;
// 將訂單ID添加到CRON事件的參數中
$args = array(
'order_id' => $order_id,
);
// 註冊一個新的CRON事件,5分鐘後執行
wp_schedule_single_event($scheduled_time, 'check_linepay_success', $args);
} else {
return;
}
}
add_action('check_linepay_success', 'check_linepay_success_handler');
/**
* 【功能】5 分鐘後檢查 _linepay_payment_status meta:
* 若為 'confirmed'(LINE Pay 端實際已付款成功),把 WC 訂單狀態從 failed 改回 processing,
* 並寄信通知 4 位管理員(hello/joanna/Ruby/DD@chunling.com.tw)+ 寫 error_log。
* 【觸發】check_linepay_success(自訂單次 cron)。
* 【關聯】log_status() 註冊此 cron。
*/
function check_linepay_success_handler($order_id){
$_linepay_payment_status = get_post_meta( $order_id, '_linepay_payment_status', true );
// 若訂單 linepay 付款成功
if ( $_linepay_payment_status && 'confirmed' == $_linepay_payment_status){
// 轉換訂單狀態,由「失敗」改為「處理中」
$order = new WC_Order($order_id);
if (!empty($order)) {
$order->update_status( 'processing' );
}
// 寄送資訊
$sent_to = [
'hello@chunling.com.tw',
'joanna@chunling.com.tw',
'Ruby@chunling.com.tw',
'DD@chunling.com.tw'
];
$title = 'HARU - Linepay 訂單狀態轉換檢查通知';
// $message = '訂單編號 - ' . $order_id . ' 狀態轉換異常(需確認linepay後台狀況後手動調整訂單),請至編輯頁面查看:' . site_url( 'wp-admin/post.php?post='. $order_id .'&action=edit' );
$message = '訂單編號 - ' . $order_id . ' 狀態轉換異常,已確認 linepay 付款狀態,並更改訂單狀態,請至編輯頁面查看:' . site_url( 'wp-admin/post.php?post='. $order_id .'&action=edit' );
// 寄送郵件
wp_mail( $sent_to, $title, $message );
// Log 紀錄
error_log( $message );
}
}
/*
? 目標: 在送出訂單後,更新使用者性別資料
! 限制 / 警告: NA
* 相關: Page - checkout
TODO 擴充功能: NA
普通註解 */
// ========== 模組 O:結帳性別欄位 ==========
/**
* 【功能】結帳送出後,將 posted_data['gender'] 存到當前使用者的 user_meta(key: gender)。
* 【觸發】woocommerce_checkout_update_order_meta,priority 10。
* 【關聯】set_checkout_field_default_value() 下次結帳時帶入預設值。
*/
add_action( 'woocommerce_checkout_update_order_meta', 'update_user_gender_meta', 10, 2 );
function update_user_gender_meta( $order_id, $posted_data ) {
$user_id = get_current_user_id(); // 獲取當前使用者的 ID
if ( $user_id && isset( $posted_data['gender'] ) ) {
$gender = sanitize_text_field( $posted_data['gender'] );
// 更新使用者的 usermeta
update_user_meta( $user_id, 'gender', $gender );
}
}
/*
? 目標: 在 Checkout 頁面設置 gender 欄位預設值
! 限制 / 警告: NA
* 相關: Page - checkout
TODO 擴充功能: NA
普通註解 */
/**
* 【功能】登入用戶結帳時,自動將之前儲存的 gender user_meta 帶入結帳欄位的預設值。
* 【觸發】woocommerce_checkout_fields filter。
*/
add_filter( 'woocommerce_checkout_fields', 'set_checkout_field_default_value' );
function set_checkout_field_default_value( $fields ) {
// 檢查用戶是否已登錄
if ( is_user_logged_in() ) {
// 獲取用戶的 meta 值
$user_id = get_current_user_id();
$gender = get_user_meta( $user_id, 'gender', true );
// 設定預設值
if ( ! empty( $gender ) ) {
$fields['billing']['gender']['default'] = $gender;
}
}
return $fields;
}
/*
? 目標: 讓 Admin 可以修改 Woocommerce export 的 Custom PHP code to modify output
! 限制 / 警告: NA
* 相關: Plugin - Advanced Order Export For WooCommerce
TODO 擴充功能: NA
普通註解 */
add_filter("woe_user_can_add_custom_php", "__return_true");
/**
* Fix Yoast SEO robots.txt changes.
* https://wordpress.org/support/topic/disable-robots-txt-changing-by-yoast-seo/#post-16648736
*/
// ========== 模組 P:Yoast robots.txt 修正、AOE 自訂 PHP ==========
/**
* 【功能】移除 Yoast SEO 對 robots.txt 的覆寫 callback(filter_robots),讓網站使用預設 robots.txt。
* 【觸發】wp_loaded action。
* 【參考】wordpress.org 支援文章 #post-16648736。
*
* 【補充】緊接的 add_filter("woe_user_can_add_custom_php", "__return_true")(line 1385)
* 允許 Advanced Order Export For WooCommerce 後台編輯自訂 PHP code。
*/
function wpwc_fix_yoast_seo_robots_txt() {
global $wp_filter;
if ( isset( $wp_filter['robots_txt']->callbacks ) && is_array( $wp_filter['robots_txt']->callbacks ) ) {
foreach ( $wp_filter['robots_txt']->callbacks as $callback_priority => $callback ) {
foreach ( $callback as $function_key => $function ) {
if ( 'filter_robots' === $function['function'][1] ) {
unset( $wp_filter['robots_txt']->callbacks[ $callback_priority ][ $function_key ] );
}
}
}
}
}
add_action( 'wp_loaded', 'wpwc_fix_yoast_seo_robots_txt' );
// ========== 模組 Q:學生角色機制(st- 優惠券觸發 + 季度清除) ==========
/**
* 【功能】註冊一個自訂 user role:'student',僅有 read 能力(用於識別學生身份、給予折扣)。
* 【觸發】init action。
*/
function add_student_role() {
if (!get_role('student')) {
add_role(
'student', // role 系统名
'Student', // role 顯示名
array(
'read' => true,
)
);
}
}
add_action('init', 'add_student_role');
/**
* 【功能】當登入用戶套用以 'st-' 開頭的優惠券時,自動加上 student role,
* 並寫入 user_meta 'student_role_assigned_date'(用於日後過期檢查)。
* 【觸發】woocommerce_applied_coupon。
*/
function add_student_role_on_coupon_use($coupon_code) {
if (strpos($coupon_code, 'st-') !== false) {
$user = wp_get_current_user();
if ($user && is_user_logged_in()) {
$user->add_role('student');
//# 增加時間戳記
update_user_meta($user->ID, 'student_role_assigned_date', current_time('mysql'));
}
}
}
add_action('woocommerce_applied_coupon', 'add_student_role_on_coupon_use');
/**
* 【功能】掃描所有 student role 用戶,移除 student_role_assigned_date 超過 1 年者。
* 【觸發】quarterly_student_role_check(自訂季度 cron)。
*/
function check_and_remove_expired_student_roles() {
$users = get_users(array(
'role' => 'student',
'meta_key' => 'student_role_assigned_date',
));
foreach ($users as $user) {
$assigned_date = get_user_meta($user->ID, 'student_role_assigned_date', true);
if ($assigned_date) {
$assigned_timestamp = strtotime($assigned_date);
$one_year_ago = strtotime('-1 year');
if ($assigned_timestamp < $one_year_ago) {
$user->remove_role('student');
}
}
}
}
add_action('quarterly_student_role_check', 'check_and_remove_expired_student_roles');
/**
* 【功能】定義自訂 cron 排程頻率 'quarterly'(90 天執行一次)。
* 【觸發】cron_schedules filter。
*/
function add_quarterly_cron_schedule($schedules) {
$schedules['quarterly'] = array(
'interval' => 60 * 60 * 24 * 90, // 每 90 天
'display' => __('Once Quarterly')
);
return $schedules;
}
add_filter('cron_schedules', 'add_quarterly_cron_schedule');
/**
* 【功能】註冊 quarterly_student_role_check cron job(若尚未註冊)。
* 【觸發】init action。
*/
function schedule_quarterly_student_role_check() {
if (!wp_next_scheduled('quarterly_student_role_check')) {
wp_schedule_event(time(), 'quarterly', 'quarterly_student_role_check');
}
}
add_action('init', 'schedule_quarterly_student_role_check');
/**
* 【功能】子主題停用時清除 quarterly_student_role_check cron job,避免孤兒任務。
* 【觸發】register_deactivation_hook。
*/
function deactivate_quarterly_student_role_check() {
$timestamp = wp_next_scheduled('quarterly_student_role_check');
wp_unschedule_event($timestamp, 'quarterly_student_role_check');
}
register_deactivation_hook(__FILE__, 'deactivate_quarterly_student_role_check');
// ========== 模組 R:GPT 問答 shortcode + AJAX + wp_gpt_answers 表 ==========
/**
* 【功能】[gpt_question_button] shortcode:渲染問答按鈕(icon + topic),
* 前端點擊時透過 AJAX 呼叫 OpenAI API 取得答案並顯示。
* 【屬性】icon=圖示 URL、question=要問的問題、topic=按鈕顯示文字。
*/
function gpt_question_button_shortcode($atts) {
$atts = shortcode_atts(array(
'icon' => '',
'question' => '',
'topic' => ''
), $atts);
$icon = esc_url($atts['icon']);
$question = esc_html(strip_tags($atts['question']));
$topic = esc_html(strip_tags($atts['topic']));
$buttons_html = '<div class="gpt-question-button-wrapper">';
$buttons_html .= '<div class="gpt-question-button" data-question="' . $question . '" data-topic="' . $topic . '">';
if ($icon) {
$buttons_html .= '<img src="' . $icon . '" alt="' . $topic . '" class="gpt-question-icon" />';
}
$buttons_html .= wp_trim_words($topic);
$buttons_html .= '</div>';
$buttons_html .= '<div class="gpt-answer"></div>';
$buttons_html .= '</div>';
return $buttons_html;
}
add_shortcode('gpt_question_button', 'gpt_question_button_shortcode');
/**
* 【功能】載入 GPT 問答的 JS(gpt-script.js),並用 wp_localize_script 傳 ajax_url 與 nonce。
* 【狀態】★ 對應的 add_action 目前已被註解(line 1518),shortcode 還可使用,但 AJAX 實際未被前端觸發;
* 若要重新啟用請取消註解。
*/
function gpt_enqueue_scripts() {
wp_enqueue_script('gpt-script', get_template_directory_uri() . '/js/gpt-script.js', array('jquery'), null, true);
wp_localize_script('gpt-script', 'gpt_ajax', array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('gpt_nonce') // 加入 nonce 安全驗證
));
}
//add_action('wp_enqueue_scripts', 'gpt_enqueue_scripts');
/**
* 【功能】GPT AJAX 處理:
* 1) check_ajax_referer 驗 nonce(防 CSRF);
* 2) 先查 wp_gpt_answers 表的快取,命中則直接回傳;
* 3) 沒快取則用 OPENAI_API_KEY(必須在 wp-config.php 定義)呼叫 GPT-3.5-turbo;
* 4) 將回應寫入快取表並回傳給前端。
* 【觸發】wp_ajax_gpt_handle_request / wp_ajax_nopriv_gpt_handle_request。
* 【安全】API Key 必須放在 wp-config.php,不可硬編。
*/
function gpt_handle_ajax_request() {
global $wpdb;
// 修正 CSRF 風險:驗證 nonce
check_ajax_referer('gpt_nonce', 'nonce');
$question = sanitize_text_field($_POST['question']);
// 檢查數據庫中是否已有答案
$table_name = $wpdb->prefix . 'gpt_answers';
$cached_answer = $wpdb->get_var($wpdb->prepare(
"SELECT answer FROM $table_name WHERE question = %s",
$question
));
if ($cached_answer) {
wp_send_json_success($cached_answer);
return;
}
// 如果沒有緩存,調用 API 獲取答案
// ⚠️ 安全性修正:API Key 應該儲存在 wp-config.php 中
// 在 wp-config.php 加入:define('OPENAI_API_KEY', 'your-new-api-key-here');
// ⚠️ 重要:請立即前往 OpenAI 平台撤銷舊的 API Key!
$api_key = defined('OPENAI_API_KEY') ? OPENAI_API_KEY : '';
// 如果沒有設定 API Key,返回錯誤
if (empty($api_key)) {
error_log('OpenAI API Key 未設定,請在 wp-config.php 中加入 define("OPENAI_API_KEY", "your-key");');
wp_send_json_error('API Key 未設定');
return;
}
$response = wp_remote_post('https://api.openai.com/v1/chat/completions', array(
'headers' => array(
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $api_key,
),
'body' => json_encode(array(
'model' => 'gpt-3.5-turbo',
'messages' => array(
array('role' => 'user', 'content' => $question)
),
'max_tokens' => 500,
)),
));
if (is_wp_error($response)) {
error_log('API request failed: ' . $response->get_error_message());
wp_send_json_error('API request failed');
} else {
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
if (isset($data['choices'][0]['message']['content'])) {
$answer = $data['choices'][0]['message']['content'];
// 將答案存儲到數據庫中
$wpdb->insert(
$table_name,
array(
'question' => $question,
'answer' => $answer
),
array(
'%s',
'%s'
)
);
wp_send_json_success($answer);
} else {
error_log('Invalid API response: ' . print_r($body, true));
wp_send_json_error('Invalid API response');
}
}
}
add_action('wp_ajax_gpt_handle_request', 'gpt_handle_ajax_request');
add_action('wp_ajax_nopriv_gpt_handle_request', 'gpt_handle_ajax_request');
// ========== 模組 S:相關商品排除分類 ==========
/**
* 【功能】單一商品頁載入前,掛上「排除 no-show 分類」的相關商品 filter。
* 【觸發】woocommerce_before_single_product action。
*/
add_action( 'woocommerce_before_single_product', 'woodmart_related_products_exclude_init' );
function woodmart_related_products_exclude_init() {
if( class_exists('WooCommerce') && defined('WOODMART_THEME_DIR') ) {
add_filter( 'woocommerce_related_products', 'woodmart_exclude_category_from_related_products', 999, 3 );
add_filter( 'woocommerce_output_related_products_args', 'woodmart_modify_related_products_args', 20, 1 );
}
}
/**
* 【功能】從相關商品列表中排除分類 slug='no-show' 的商品。
* 使用 transient 快取 1 小時(key: woodmart_exclude_cat_*),並用 static $processing 防止無限遞迴。
* 【觸發】woocommerce_related_products,priority 999。
*/
function woodmart_exclude_category_from_related_products( $related_posts, $product_id, $args ){
// 定義要排除的分類 slug
$exclude_categories = array('no-show');
if( empty($exclude_categories) || empty($related_posts) ) {
return $related_posts;
}
// 防止無限遞迴
static $processing = false;
if ($processing) {
return $related_posts;
}
$processing = true;
$exclude_ids = array();
// 使用 transient 快取來避免重複查詢
$cache_key = 'woodmart_exclude_cat_' . md5(implode(',', $exclude_categories));
$cached_ids = get_transient($cache_key);
if ($cached_ids === false) {
// 通過分類 slug 獲取商品 ID(限制數量避免記憶體問題)
foreach($exclude_categories as $category_slug) {
$category = get_term_by('slug', $category_slug, 'product_cat');
if($category) {
$category_products = wc_get_products( array(
'status' => 'publish',
'limit' => 100, // 限制數量,避免查詢過多商品
'category' => array($category_slug),
'return' => 'ids',
));
$exclude_ids = array_merge($exclude_ids, $category_products);
}
}
$exclude_ids = array_unique($exclude_ids);
// 快取 1 小時
set_transient($cache_key, $exclude_ids, HOUR_IN_SECONDS);
} else {
$exclude_ids = $cached_ids;
}
// 從相關商品列表中排除這些商品
$filtered_related_posts = array_diff($related_posts, $exclude_ids);
// 移除了重新獲取相關商品的邏輯,避免無限遞迴
// 如果結果太少,讓 woodmart_modify_related_products_args 在一開始就增加獲取數量
$processing = false;
return $filtered_related_posts;
}
/**
* 【功能】把相關商品的 posts_per_page 提升到至少 8,補償排除後可能不足的數量。
* 【觸發】woocommerce_output_related_products_args,priority 20。
*/
function woodmart_modify_related_products_args( $args ) {
// 確保有足夠的相關商品顯示
if( isset($args['posts_per_page']) && $args['posts_per_page'] < 8 ) {
$args['posts_per_page'] = 8; // 增加獲取數量以補償被排除的商品
}
return $args;
}
/**
* ========================================
* 自定義超商取貨運送方式
* ========================================
* 目標:創建獨立的超商取貨運送方式,不依賴舊版外掛
* 配合新版 newebpay-payment 外掛使用
*/
/**
* 超商取貨運送方式類別
*/
// ========== 模組 T:自訂超商取貨運送方式類別 ==========
/**
* 【功能】自訂超商取貨運送方式(取代舊版藍新外掛的運送方式)。
* - id = 'newebpay_cvscom'(保留沿用,配合既有 hook 邏輯)
* - 支援 shipping zones、instance settings
* - 設定欄位:方法標題、運費、滿額免運門檻
* - calculate_shipping() 內處理免運券與滿額免運
* 【關聯】add_custom_cvscom_shipping_method() 將其註冊到 WC。
*/
if (!class_exists('WC_Custom_CVSCOM_Shipping_Method')) {
class WC_Custom_CVSCOM_Shipping_Method extends WC_Shipping_Method {
/**
* 建構子
*/
public function __construct($instance_id = 0) {
$this->id = 'newebpay_cvscom'; // 使用相同的 ID 以配合現有邏輯
$this->instance_id = absint($instance_id);
$this->method_title = __('超商取貨', 'woocommerce');
$this->method_description = __('藍新金流超商取貨運送方式(配合新版外掛)', 'woocommerce');
$this->supports = array(
'shipping-zones',
'instance-settings',
'instance-settings-modal',
);
$this->init();
}
/**
* 初始化設定
*/
function init() {
// Load the settings API
$this->init_form_fields();
$this->init_settings();
// Define user set variables
$this->title = $this->get_option('title');
$this->cost = $this->get_option('cost');
$this->free_amount = $this->get_option('free_amount');
// Save settings in admin
add_action('woocommerce_update_options_shipping_' . $this->id, array($this, 'process_admin_options'));
}
/**
* 設定表單欄位
*/
function init_form_fields() {
$this->instance_form_fields = array(
'title' => array(
'title' => __('方法標題', 'woocommerce'),
'type' => 'text',
'description' => __('這將顯示在結帳頁面上', 'woocommerce'),
'default' => __('超商取貨', 'woocommerce'),
'desc_tip' => true,
),
'cost' => array(
'title' => __('運費', 'woocommerce'),
'type' => 'number',
'description' => __('輸入固定運費金額', 'woocommerce'),
'default' => '60',
'desc_tip' => true,
'custom_attributes' => array(
'step' => '1',
'min' => '0',
),
),
'free_amount' => array(
'title' => __('滿額免運', 'woocommerce'),
'type' => 'number',
'description' => __('購物車金額達到此金額時免運費(設定 0 關閉此功能)', 'woocommerce'),
'default' => '0',
'desc_tip' => true,
'custom_attributes' => array(
'step' => '1',
'min' => '0',
),
),
);
}
/**
* 計算運費
*/
public function calculate_shipping($package = array()) {
$cart_total = WC()->cart->get_cart_contents_total();
$shipping_cost = $this->cost;
// 滿額免運判斷
if ($this->free_amount > 0 && $cart_total >= $this->free_amount) {
$shipping_cost = 0;
}
// 檢查是否有免運費優惠券
$coupons = WC()->cart->get_coupons();
foreach ($coupons as $coupon) {
if ($coupon->get_free_shipping()) {
$shipping_cost = 0;
break;
}
}
$this->add_rate(array(
'id' => $this->get_rate_id(),
'label' => $this->title,
'cost' => $shipping_cost,
'package' => $package,
));
}
}
}
/**
* 註冊超商取貨運送方式
*/
/**
* 【功能】WC 啟動運送方式時,確認 WC_Custom_CVSCOM_Shipping_Method 類別已存在。
* 【觸發】woocommerce_shipping_init action。
*/
add_action('woocommerce_shipping_init', 'custom_cvscom_shipping_init');
function custom_cvscom_shipping_init() {
if (!class_exists('WC_Custom_CVSCOM_Shipping_Method')) {
return;
}
}
/**
* 將超商取貨運送方式加入 WooCommerce
*/
/**
* 【功能】把 WC_Custom_CVSCOM_Shipping_Method 註冊為 WC 可用的運送方式(id: newebpay_cvscom)。
* 【觸發】woocommerce_shipping_methods filter。
*/
add_filter('woocommerce_shipping_methods', 'add_custom_cvscom_shipping_method');
function add_custom_cvscom_shipping_method($methods) {
$methods['newebpay_cvscom'] = 'WC_Custom_CVSCOM_Shipping_Method';
return $methods;
}
/**
* ========================================
* 藍新金流 x 運送方式整合
* ========================================
* 目標:根據運送方式智能控制超商取貨相關選項
* 方案:前端 + 後端雙重驗證
*/
/**
* 前端:JavaScript 控制顯示邏輯
*/
// ========== 模組 U:藍新 × 運送方式整合 JS(最複雜,~330 行) ==========
/**
* 【功能】★★★ 結帳頁注入大段 jQuery,依運送方式動態控制付款選項:
*
* 情境 A(宅配 / 6H 配送):
* - 隱藏藍新「nwp_selected_payments」下拉中的 CVSCOMPayed/CVSCOMNotPayed 選項
* - 完全隱藏 #CVSCOMNotPayed checkbox 並清空 value
* - 顯示 LINE Pay 付款選項
*
* 情境 B(超商取貨):
* - 顯示所有藍新付款選項(含 CVSCOM)
* - #CVSCOMNotPayed checkbox 視覺隱藏(absolute -9999px)但保留 DOM
* - 根據選擇的付款方式自動切換 checkbox:
* CVSCOMPayed(取貨付款)→ unchecked(CVSCOM=2)
* 其他(信用卡等)→ checked(CVSCOM=1,超商取貨不付款)
* - 隱藏 LINE Pay
* - 提交前 (checkout_place_order) 阻擋 LINE Pay+超商組合並再次補正 checkbox
*
* 並監聽 updated_checkout、change shipping_method、change payment_method、update_checkout
* 確保 AJAX 重新渲染後 UI 維持正確狀態,updateCheckoutButtonText() 同步結帳按鈕文字。
*
* 【觸發】wp_footer,僅在 is_checkout() 時注入。
* 【除錯】內含大量 console.log,正式環境保留方便排查超商取貨問題。
*/
add_action('wp_footer', 'newebpay_shipping_payment_integration');
function newebpay_shipping_payment_integration() {
if (!is_checkout()) return;
?>
<script>
jQuery(function($){
var newebpay_cvscom = 'newebpay_cvscom'; // 超商取貨運送方式 ID
/**
* 檢查是否選擇超商取貨運送方式
*/
function isStorePickingShipping() {
var selectedShipping = $('input[name^="shipping_method"]:checked').val();
// 修正:如果沒有選中的運送方式,檢查是否只有超商取貨可用
if (!selectedShipping) {
var $shippingOptions = $('input[name^="shipping_method"]');
if ($shippingOptions.length === 1) {
selectedShipping = $shippingOptions.val();
console.log('🔍 只有一個運送方式:' + selectedShipping);
}
}
// 檢查是否包含 newebpay_cvscom(因為實際值可能是 newebpay_cvscom:20)
var isStorePicking = selectedShipping && selectedShipping.indexOf(newebpay_cvscom) !== -1;
console.log('🔍 檢查運送方式:' + selectedShipping + ' → 是否超商取貨:' + isStorePicking);
return isStorePicking;
}
/**
* 主控制函數:根據運送方式控制付款選項
*/
function controlPaymentOptions() {
if (isStorePickingShipping()) {
// 情況 B:選擇超商取貨運送方式
handleStorePickingMode();
} else {
// 情況 A:選擇宅配/6H配送
handleNormalShippingMode();
}
}
/**
* 情況 A:宅配/6H配送
* - 隱藏「超商取貨付款」下拉選項
* - 隱藏「超商取貨不付款」checkbox
* - 顯示 LINE Pay 付款選項
*/
function handleNormalShippingMode() {
var $select = $('select[name="nwp_selected_payments"]');
var $checkbox = $('#CVSCOMNotPayed');
var $checkboxLabel = $('label[for="CVSCOMNotPayed"]');
var $linepay = $('.payment_method_linepay, #payment_method_linepay');
if ($select.length > 0) {
// 隱藏「超商取貨付款」選項
$select.find('option[value="CVSCOMPayed"]').hide();
$select.find('option[value="CVSCOMNotPayed"]').hide(); // 也隱藏這個選項
// 如果當前選中的是超商取貨相關選項,切換到信用卡
var currentVal = $select.val();
if (currentVal === 'CVSCOMPayed' || currentVal === 'CVSCOMNotPayed') {
// 優先選擇信用卡,如果沒有則選第一個可見選項
if ($select.find('option[value="Credit"]').length > 0) {
$select.val('Credit');
} else {
var firstVisibleOption = $select.find('option:visible:first').val();
$select.val(firstVisibleOption);
}
$select.trigger('change');
}
}
// 完全隱藏並取消勾選 checkbox(使用 .hide() 是安全的,因為我們不需要它的值)
$checkbox.hide();
$checkboxLabel.hide();
$checkbox.prop('checked', false);
$checkbox.val(''); // 清空 value
// 顯示 LINE Pay 付款選項
if ($linepay.length > 0) {
$linepay.show();
console.log(' ✅ 顯示 LINE Pay 付款選項');
}
// 🔥 修正:確保按鈕文字正確
setTimeout(function() {
updateCheckoutButtonText();
}, 50);
console.log('🚚 宅配/6H模式:已隱藏超商取貨選項');
}
/**
* 情況 B:超商取貨運送方式
* - 顯示所有付款選項
* - 視覺隱藏 checkbox(但保留功能)
* - 根據下拉選擇自動控制 checkbox
* - 隱藏 LINE Pay 付款選項
*/
function handleStorePickingMode() {
var $select = $('select[name="nwp_selected_payments"]');
var $checkbox = $('#CVSCOMNotPayed');
var $checkboxLabel = $('label[for="CVSCOMNotPayed"]');
var $linepay = $('.payment_method_linepay, #payment_method_linepay');
console.log('🏪 進入超商取貨模式');
if ($select.length > 0) {
// 顯示所有選項(包括超商取貨付款)
$select.find('option').each(function() {
$(this).show();
console.log(' ✅ 顯示選項:' + $(this).val() + ' - ' + $(this).text());
});
// 確保下拉選單本身是可見的
$select.show();
} else {
console.log(' ⚠️ 找不到付款方式下拉選單');
}
if ($checkbox.length > 0) {
// ✨ 視覺隱藏但保留功能(不使用 display:none)
$checkbox.show(); // 確保不是 display:none
$checkbox.css({
'position': 'absolute',
'left': '-9999px',
'opacity': '0',
'pointer-events': 'none'
});
$checkboxLabel.css({
'position': 'absolute',
'left': '-9999px',
'opacity': '0',
'pointer-events': 'none'
});
console.log(' ✅ Checkbox 已視覺隱藏(功能正常)');
// 根據當前選擇的付款方式,自動設定 checkbox
autoToggleCheckbox();
} else {
console.log(' ⚠️ 找不到 checkbox');
}
// 隱藏 LINE Pay 付款選項
if ($linepay.length > 0) {
$linepay.hide();
// 如果 LINE Pay 被選中,切換到其他付款方式
if ($linepay.find('input[type="radio"]').is(':checked')) {
// 優先選擇藍新金流,如果沒有則選第一個可見的付款方式
var $newebpay = $('.payment_method_newebpay, #payment_method_newebpay');
if ($newebpay.length > 0) {
$newebpay.find('input[type="radio"]').prop('checked', true).trigger('change');
} else {
$('.payment_method:visible:first').find('input[type="radio"]').prop('checked', true).trigger('change');
}
// 🔥 修正:更新結帳按鈕文字
updateCheckoutButtonText();
}
console.log(' 🚫 已隱藏 LINE Pay 付款選項');
}
console.log('🏪 超商取貨模式設定完成');
}
/**
* 更新結帳按鈕文字
* - 根據當前選中的付款方式,更新按鈕文字
*/
function updateCheckoutButtonText() {
var $checkedPayment = $('input[name="payment_method"]:checked');
var $placeOrderBtn = $('#place_order');
if ($checkedPayment.length > 0 && $placeOrderBtn.length > 0) {
var buttonText = $checkedPayment.data('order_button_text');
if (buttonText) {
$placeOrderBtn.text(buttonText);
console.log(' 🔄 更新結帳按鈕文字為:' + buttonText);
} else {
// 如果沒有自訂文字,使用預設值
var defaultText = $placeOrderBtn.data('value');
if (defaultText) {
$placeOrderBtn.text(defaultText);
console.log(' 🔄 恢復預設結帳按鈕文字:' + defaultText);
}
}
}
}
/**
* 自動控制 checkbox 邏輯
* - 選擇「超商取貨付款」→ 取消勾選(CVSCOM=2)
* - 選擇其他付款方式 → 自動勾選(CVSCOM=1)
*/
function autoToggleCheckbox() {
var $select = $('select[name="nwp_selected_payments"]');
var $checkbox = $('#CVSCOMNotPayed');
if ($checkbox.length === 0) {
console.log('⚠️ checkbox 不存在');
return;
}
var selectedPayment = $select.val();
// 根據付款方式設置 checkbox
if (selectedPayment === 'CVSCOMPayed') {
// 超商取貨付款:CVSCOM=2 → 不勾選
$checkbox.prop('checked', false);
console.log('💳 超商取貨付款:CVSCOM=2,checkbox 取消勾選');
} else {
// 其他付款方式:CVSCOM=1 → 勾選
$checkbox.prop('checked', true);
console.log('💳 ' + selectedPayment + ':CVSCOM=1,checkbox 已勾選');
}
console.log('🔍 Checkbox 狀態:checked=' + $checkbox.prop('checked') + ', value=' + $checkbox.attr('value'));
}
/**
* 初始化
*/
setTimeout(function(){
controlPaymentOptions();
}, 100);
/**
* 監聽運送方式變更
*/
$(document.body).on('change', 'input[name^="shipping_method"]', function() {
controlPaymentOptions();
});
/**
* 監聽 WooCommerce 更新結帳
*/
$(document.body).on('updated_checkout', function() {
setTimeout(function() {
controlPaymentOptions();
}, 100);
});
/**
* 監聽付款方式下拉變更(僅在超商取貨模式)
*/
$(document.body).on('change', 'select[name="nwp_selected_payments"]', function() {
if (isStorePickingShipping()) {
autoToggleCheckbox();
}
});
/**
* 🔥 修正:監聽付款方式改變,更新結帳按鈕文字
*/
$(document.body).on('change', 'input[name="payment_method"]', function() {
setTimeout(function() {
updateCheckoutButtonText();
}, 50);
});
/**
* 🔥 關鍵:在表單提交前強制確保值被正確設置
* 注意:WooCommerce 在 document.body 觸發此事件,不是 form 本身
*/
$(document.body).on('checkout_place_order', function() {
var selectedShipping = $('input[name^="shipping_method"]:checked').val();
var selectedPayment = $('input[name="payment_method"]:checked').val();
console.log('🚀 表單提交前檢查');
console.log(' 運送方式:' + selectedShipping);
console.log(' 付款方式:' + selectedPayment);
// 🚫 阻止 LINE Pay + 超商取貨的組合
if (isStorePickingShipping() && selectedPayment === 'linepay') {
alert('⚠️ LINE Pay 不支援超商取貨,請選擇其他付款方式或改為宅配運送。');
console.log(' ❌ 阻止提交:LINE Pay 不支援超商取貨');
return false; // 阻止表單提交
}
if (isStorePickingShipping()) {
var $checkbox = $('#CVSCOMNotPayed');
var $select = $('select[name="nwp_selected_payments"]');
var selectedNewebpayMethod = $select.val();
console.log(' 藍新付款方式:' + selectedNewebpayMethod);
// 根據付款方式設置 checkbox
if (selectedNewebpayMethod === 'CVSCOMPayed') {
// CVSCOM=2:超商取貨付款 → 不勾選
if ($checkbox.length > 0) {
$checkbox.prop('checked', false);
}
console.log(' ✅ CVSCOM=2(超商取貨付款),checkbox 不勾選');
} else {
// CVSCOM=1:超商取貨不付款 → 勾選
if ($checkbox.length > 0) {
$checkbox.prop('checked', true);
}
console.log(' ✅ CVSCOM=1(超商取貨不付款),checkbox 已勾選');
}
console.log(' 最終 checkbox 狀態:checked=' + ($checkbox.length > 0 ? $checkbox.prop('checked') : 'N/A'));
}
// 允許表單提交
return true;
});
/**
* 🔥 超級關鍵:攔截 AJAX 數據,手動添加 checkbox 值
*/
$(document.body).on('checkout_error', function() {
console.log('⚠️ 結帳發生錯誤');
});
// 在 WooCommerce 更新結帳前,確保 checkbox 值正確
$(document.body).on('update_checkout', function() {
if (isStorePickingShipping()) {
var $checkbox = $('#CVSCOMNotPayed');
var $select = $('select[name="nwp_selected_payments"]');
var selectedPayment = $select.val();
if ($checkbox.length > 0) {
if (selectedPayment === 'CVSCOMPayed') {
$checkbox.prop('checked', false);
} else {
$checkbox.prop('checked', true);
}
console.log('🔄 update_checkout:checkbox 已更新,checked=' + $checkbox.prop('checked'));
}
}
});
});
</script>
<?php
}
/**
* ========================================
* 阻止 LINE Pay + 超商取貨的組合(後端驗證)
* ========================================
*/
// ========== 模組 V:後端阻擋 LINE Pay + 超商取貨組合 ==========
/**
* 【功能】後端再次驗證:若付款方式 = linepay 且運送方式含 newebpay_cvscom,
* 加入結帳錯誤「LINE Pay 不支援超商取貨」並寫 error_log。
* 前端 JS 已先攔(模組 U),這裡是雙重保險。
* 【觸發】woocommerce_after_checkout_validation,priority 10。
*/
add_action('woocommerce_after_checkout_validation', 'prevent_linepay_with_store_pickup', 10, 2);
function prevent_linepay_with_store_pickup($data, $errors) {
// 檢查是否選擇了 LINE Pay
if (isset($data['payment_method']) && $data['payment_method'] === 'linepay') {
// 檢查是否選擇了超商取貨
if (isset($data['shipping_method']) && is_array($data['shipping_method'])) {
foreach ($data['shipping_method'] as $method) {
if (strpos($method, 'newebpay_cvscom') !== false) {
$errors->add('payment_shipping_mismatch',
__('LINE Pay 不支援超商取貨運送方式,請選擇其他付款方式或改為宅配運送。', 'woocommerce'));
error_log('[Checkout Validation] 阻止 LINE Pay + 超商取貨組合');
return;
}
}
}
}
}
/**
* 🎯 簡化策略:只處理非超商取貨的情況,超商取貨讓外掛原生邏輯處理
*/
// ========== 模組 W:藍新付款參數補正(receipt 頁攔截) ==========
/**
* 【功能】當付款方式 = newebpay 但運送方式「不是」超商取貨時,清空所有 CVSCOM 相關 POST/data 欄位,
* 並把 nwp_selected_payments 從 'CVSCOMPayed' 強制改為 'Credit',避免外掛誤帶超商參數。
* 【觸發】woocommerce_checkout_posted_data filter,priority 5(早期介入)。
*/
add_filter('woocommerce_checkout_posted_data', 'newebpay_handle_non_cvscom_shipping', 5);
function newebpay_handle_non_cvscom_shipping($data) {
// 只在使用藍新金流時執行
if (!isset($data['payment_method']) || $data['payment_method'] !== 'newebpay') {
return $data;
}
$chosen_shipping_methods = WC()->session->get('chosen_shipping_methods');
if (empty($chosen_shipping_methods)) {
return $data;
}
$chosen_shipping_method = $chosen_shipping_methods[0];
// 使用 strpos 支持 instance ID(如 newebpay_cvscom:20)
$is_cvscom_shipping = (strpos($chosen_shipping_method, 'newebpay_cvscom') !== false);
// 只處理「非超商取貨」的情況
if (!$is_cvscom_shipping) {
// 確保 cvscom_not_payed 為空,避免 Undefined key 錯誤
if (!isset($_POST['cvscom_not_payed'])) {
$_POST['cvscom_not_payed'] = '';
}
$data['cvscom_not_payed'] = '';
// 如果錯誤地選了超商取貨付款,改為信用卡
if (isset($_POST['nwp_selected_payments']) && $_POST['nwp_selected_payments'] === 'CVSCOMPayed') {
$_POST['nwp_selected_payments'] = 'Credit';
$data['nwp_selected_payments'] = 'Credit';
}
}
return $data;
}
/**
* 🔑 在藍新生成付款表單前清理/補正參數
* woocommerce_receipt_newebpay 在外掛 receipt_page()(priority 10)之前執行(priority 0)
* 這是生成藍新加密參數前最後的攔截機會
*/
/**
* 【功能】★★ 在藍新外掛產生加密表單前最後攔截,依運送方式補正訂單 meta:
* - 非超商運送:刪除 _CVSCOMNotPayed、把 _nwpSelectedPayment 從 CVSCOMPayed/Not 改為 Credit
* - 超商運送 + CVSCOMPayed(取貨付款)→ _CVSCOMNotPayed = 0(CVSCOM=2)
* - 超商運送 + 其他(線上付款)→ _CVSCOMNotPayed = 1(CVSCOM=1,超商不付款)
* 【觸發】woocommerce_receipt_newebpay,priority 0(在外掛 receipt_page 之前)。
* 【關鍵】這是 JS 失靈時的最後一道補正防線。
*/
add_action('woocommerce_receipt_newebpay', 'newebpay_intercept_before_payment_page', 0);
function newebpay_intercept_before_payment_page($order_id) {
$order = wc_get_order($order_id);
if (!$order) return;
// 檢查運送方式
$shipping_methods = $order->get_shipping_methods();
$is_cvscom_shipping = false;
foreach ($shipping_methods as $shipping_method) {
$method_id = $shipping_method->get_method_id();
if ($method_id === 'newebpay_cvscom' || strpos($method_id, 'cvscom') !== false) {
$is_cvscom_shipping = true;
break;
}
}
// 🎯 核心邏輯:如果不是超商取貨運送,清除所有 CVSCOM 相關參數
if (!$is_cvscom_shipping) {
$selected_payment = $order->get_meta('_nwpSelectedPayment', true);
// 清除藍新外掛會讀取的 meta 資料
$order->delete_meta_data('_CVSCOMNotPayed');
$order->update_meta_data('_CVSCOMNotPayed', 0);
// 如果選擇了 CVSCOMPayed,強制改為 Credit
if ($selected_payment === 'CVSCOMPayed' || $selected_payment === 'CVSCOMNotPayed') {
$order->update_meta_data('_nwpSelectedPayment', 'Credit');
}
$order->save();
}
// 如果是超商取貨運送,確保正確設定 CVSCOM 參數
else {
$selected_payment = $order->get_meta('_nwpSelectedPayment', true);
if ($selected_payment === 'CVSCOMPayed') {
// 線上付款取貨 → CVSCOM=2,_CVSCOMNotPayed 必須是 0
$order->update_meta_data('_CVSCOMNotPayed', 0);
$order->save();
} else {
// 超商取貨付款(到店付款)→ CVSCOM=1,_CVSCOMNotPayed 必須是 1
// 不管 JS 是否正確設定 checkbox,後端強制補正
$current_flag = $order->get_meta('_CVSCOMNotPayed', true);
if ($current_flag != '1') {
$order->update_meta_data('_CVSCOMNotPayed', 1);
$order->save();
error_log('[CVSCOM Fix] 訂單 #' . $order->get_id() . ' 補正:_CVSCOMNotPayed 0→1(付款:' . $selected_payment . ')');
}
}
}
}
/**
* 🔑🔑🔑 終極攔截:直接移除外掛的超商取貨付款設定(暫時性)
* 這個方法會在生成藍新參數時,暫時關閉超商取貨付款選項
*/
// ========== 模組 X:動態關閉藍新外掛超商付款設定 ==========
/**
* 【功能】★★ 動態過濾藍新外掛的 settings option:
* - 後台(is_admin)完全不攔截,讓設定能正常儲存
* - 只在前端 order-pay 或 checkout 頁面攔截
* - 若當前訂單/session 非超商運送 → 暫時關閉外掛的
* NwpPaymentMethodCVSCOMPayed / NwpPaymentMethodCVSCOMNotPayed 設定
* 【觸發】option_woocommerce_newebpay_settings filter,priority 9999(最後攔)。
* 【取得訂單來源】依序嘗試:order-pay query var → POST['order_id'] → session 'order_awaiting_payment'。
* 【特殊邏輯】結帳頁無 chosen_shipping_methods 時,掃 packages 判斷是否「只有」超商取貨。
*/
add_filter('option_woocommerce_newebpay_settings', 'newebpay_filter_cvscom_settings', 9999);
function newebpay_filter_cvscom_settings($settings) {
// 🔥 關鍵修正:後台完全不執行攔截,讓設定可以正常儲存
// 只在前端結帳頁面或訂單付款頁面執行攔截
if (is_admin()) {
return $settings; // 後台任何情況都不攔截
}
// 只在付款頁面或結帳頁面執行攔截
if (!is_wc_endpoint_url('order-pay') && !is_checkout()) {
return $settings;
}
// 獲取當前訂單
$order = null;
$order_id = 0;
// 方法 1:從 order-pay 頁面獲取
global $wp;
if (isset($wp->query_vars['order-pay'])) {
$order_id = absint($wp->query_vars['order-pay']);
$order = wc_get_order($order_id);
}
// 方法 2:從 POST 獲取(結帳時)
if (!$order && isset($_POST['order_id'])) {
$order_id = absint($_POST['order_id']);
$order = wc_get_order($order_id);
}
// 方法 3:從 session 獲取最近的訂單(最後手段)
if (!$order && WC()->session) {
$order_id = WC()->session->get('order_awaiting_payment');
if ($order_id) {
$order = wc_get_order($order_id);
}
}
// 判斷運送方式
$is_cvscom_shipping = false;
$method_id = '';
if ($order && $order->get_payment_method() === 'newebpay') {
// 從訂單中檢查運送方式
$shipping_methods = $order->get_shipping_methods();
foreach ($shipping_methods as $shipping_method) {
$method_id = $shipping_method->get_method_id();
if ($method_id === 'newebpay_cvscom' || strpos($method_id, 'cvscom') !== false) {
$is_cvscom_shipping = true;
break;
}
}
} elseif (is_checkout() && WC()->session) {
// 在結帳頁面沒有訂單時,從 session 獲取選擇的運送方式
$chosen_shipping_methods = WC()->session->get('chosen_shipping_methods');
if (!empty($chosen_shipping_methods)) {
$method_id = $chosen_shipping_methods[0];
$is_cvscom_shipping = (strpos($method_id, 'newebpay_cvscom') !== false);
} else {
// 修正:如果 session 中沒有選擇的運送方式,檢查可用的運送方式
// 如果只有超商取貨可用,應該視為選擇了超商取貨
$packages = WC()->shipping()->get_packages();
if (!empty($packages)) {
$package = $packages[0];
$available_methods = WC()->shipping()->calculate_shipping_for_package($package);
if (!empty($available_methods['rates'])) {
$all_rates = $available_methods['rates'];
$cvscom_count = 0;
$total_count = count($all_rates);
foreach ($all_rates as $rate) {
if (strpos($rate->id, 'newebpay_cvscom') !== false) {
$cvscom_count++;
}
}
// 如果所有可用的運送方式都是超商取貨,視為選擇了超商取貨
if ($cvscom_count > 0 && $cvscom_count === $total_count) {
$is_cvscom_shipping = true;
$method_id = 'newebpay_cvscom';
}
}
}
}
}
// 🎯 如果不是超商取貨運送,暫時關閉外掛的超商取貨付款設定
if (!$is_cvscom_shipping && is_array($settings)) {
$settings['NwpPaymentMethodCVSCOMPayed'] = 'no';
$settings['NwpPaymentMethodCVSCOMNotPayed'] = 'no';
}
return $settings;
}
/**
* ========================================
* 修改藍新金流自動跳轉時間
* ========================================
* 將預設的 10 秒改為 3 秒
*/
// ========== 模組 Y:藍新自動跳轉時間(10s → 3s) ==========
/**
* 【功能】在 order-pay 頁面額外注入 JS,3 秒後自動 submit 名為 'newebpay' 的表單,
* 覆蓋藍新外掛預設 10 秒延遲,加快導向藍新付款頁的體驗。
* 【觸發】wp_footer,僅在 is_wc_endpoint_url('order-pay') 時執行。
*/
add_action('wp_footer', 'newebpay_modify_auto_submit_time');
function newebpay_modify_auto_submit_time() {
// 只在訂單支付頁面執行
if (!is_wc_endpoint_url('order-pay')) {
return;
}
?>
<script>
jQuery(function($){
// 覆蓋原有的自動提交,改為 3 秒
setTimeout(function() {
if (document.forms['newebpay']) {
document.forms['newebpay'].submit();
}
}, 3000); // 3000 毫秒 = 3 秒
});
</script>
<?php
}