在台灣通過 eplus 與 PIA 抽票認證:ifmobile 門號使用心得

最近因為要登入 PIA 和 eplus 購票網站,臨時需要一組可以收簡訊的日本門號。

研究一番後,我選擇了 ifmobile 而不是 hanacell 或樂天迴線 SIM 卡。
這篇文章簡單記錄我購買、使用的流程與心得,希望對也有類似需求的朋友有幫助。


為什麼選 ifmobile?

雖然 ifmobile 是中資企業,對隱私有疑慮的朋友可能要多考慮一下,但它幾個優點讓我還是決定嘗試:

  • 最關鍵的原因:
    hanacell 必須肉身飛往日本才能使用門號,所以雖然便宜但無法馬上使用。
    據 Plurk 網友回報 2025 年已經無法使用樂天迴線 SIM 卡驗證 PIA 或 eplus。
  • ifmobile 同 hanacell 也支援實體 SIM 和 eSIM(ifmobile 的 eSIM 要透過客服額外申請)。
  • ifmobile 同 hanacell 網站上也可直接用信用卡購買。
  • ifmobile 購買流程只需上傳「個人與護照內頁」的合照。
  • ifmobile 客服效率高,甚至日本黃金週也上班。
  • ifmobile 門號申請後就能立刻收簡訊或撥打電話,日本網站登錄驗證完全沒問題(PIA, eplus, LINE)。

網路上有人提到需要透過客服後台開啟漫遊等待 30 分鐘,實測與問客服都是不用、馬上可用。


申請流程與客服互動

申請流程大致如下:

  1. 在官網填寫資料,日文名字不用太嚴格(漢字筆畫不同也沒關係),英文名字按護照填寫就好。

  2. 地址寫英文即可,國籍寫「Taiwan」OK,不需要寫 ROC。

  3. 上傳與護照的自拍照,以及護照內頁。背景不用如證件照嚴謹,只要不戴口罩包括護照內頁清楚即可。

  4. 完成付款(會收首月費用 + 開卡費 5500 日圓)。

  5. 需要主動聯絡客服(建議用微信,回覆速度遠勝 LINE),可以要求改寄 eSIM 退運費。

    黃金周期間 LINE 詢問週三 (4/30) ,ifmobile LINE 周六 (5/03) 才回。

    同樣黃金周期間,微信週三 (4/30) 當天就回覆,且週六 (5/03) 也積極協助處理。

  6. 寄送方式有 EMS,海外運費為 2200 日圓。

    小提醒:eSIM 每換一次手機要再付 1100 日圓。

客服有時用語音訊息回覆,有點像在微信語音聊天,不確定是不是因為人在家或戶外總之很隨興 XD

不過服務態度很好,問題也都能解決。


包裹寄送速度

我的經驗如下:

  • 4/30 下單詢問
  • 5/1 當天下午交寄,EMS 追蹤碼要自己問客服拿
  • 5/2 早上到東京國際交換局,下午就到台灣
  • 5/3 上午 11 點就送到我家(我希望配送時段選擇下午,但配送員提前電話詢問提早配達)

對於從日本寄來台灣的速度來說,我覺得這樣算非常快了。


費用整理

ifmobile 的月費會依據你選的流量方案而不同。以下是一些基本費率資訊:

  • 開卡費:5500 日圓
  • 緊急通話服務費:每月 4 日圓(110/119/120)
  • 海外接收簡訊:免費
  • 海外發簡訊:220 日圓/則
  • 日本國內通話:24 日圓/30 秒
  • 日本→海外:275 日圓/30 秒
  • 海外接聽/撥打:275 日圓/30 秒
  • eSIM 每換一次手機:1100 日圓
  • 海外郵費:2200 日圓(日本境內 880 日圓)

我選擇的是每月 1GB 與無額外通話包月方案。


實際使用測試

收到的是一張實體 SIM 和一份日本語的使用說明書,裝在很大的日本 EMS 制式硬紙卡信封袋中。

我實測的幾支手機:

  • ASUS Zenfone 8:只能收簡訊,VoLTE 無法開啟 → 無法撥號。
  • Apple iPhone 6s (2017):無訊號,可能是個別裝置問題。
  • Apple iPhone 6+ (2015):重開機即自動連上中華電信,訊號滿格,可撥打日本與台灣電話,簡訊收發正常。

去日本才需要設定 APN,人在台灣不用動任何設定。

特別測試:購票驗證

  • 成功完成 PIA 和 eplus 演唱會網站手機認證,撥號記得在台灣要把第一位數的 0 省略,並且輸入 +81 國碼。
  • 日本版 LINE 綁定成功,但年齡認證仍需親跑東京的 ifmobile 辦公室才能解鎖 ID 搜尋功能,目前只能用 QR Code 加好友。

總結:值得嗎?

雖然以純粹「養門號」來看,ifmobile 不算便宜。
但以速度快、申請簡單、客服積極、可收簡訊與撥打電話來說,非常適合有緊急需求(例如登入購票網站、接收日本簡訊驗證)的使用者。

尤其是在一週內就能搞定,還能選擇 eSIM 或實體卡,對我來說根本是救星。唯一缺點就是貴,但急用的時候真的沒得選。
如果只是要養門號,不趕時間、可以親自飛日本、不用應對 PIA 潛在的臨時登入驗證,會比較推薦 hanacell,價錢便宜太多。


有需要收日本簡訊或要綁定日本 LINE 的朋友,也許可以把 ifmobile 納入考慮範圍囉!

[歌詞假名表示] 神さま、バカ

BulkInsert 懶人福音:從 Access MDB 高效同步資料到 SQL Server

緣由

  1. 目標電腦系統老舊,Windows Server 2003 (可以理解為 Windows XP),最新版只能運行 .NET Framework 4.0.3。
  2. 希望製作成可以打開就執行的 headless,搭配一些簡易的檔案輸出提供其他老舊程式語言簡單的存取。

前置作業

Visual Studio 必須安裝以下 Nuget 套件,你也可以視需要斟酌。
至於詳細安裝方法就不贅述,請自行上網了解 Nuget 套件安裝方法。

  1. CommandLineParser [v2.9.1]
  2. NLog [v5.4.0]
  3. Dapper [v1.50.2]
  4. DataBooster.SqlServer [v1.8.4]
    此外,為了讓 Windows XP 也能運行我們創建專案時選擇 .Net Framework 4 的 Console Application。

Log 紀錄輸出

在專案根目錄建立 nlog.config

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

        <!-- 定義 NLog 使用的 Targets -->
        <targets>
                <!-- Console 輸出 -->
                <target name="console" xsi:type="Console" layout="${longdate} | ${level:uppercase=true} | ${message}" />

                <!-- 檔案輸出 -->
                <target name="file" xsi:type="File"
                fileName="logs/SyncDemox.log"
                archiveFileName="logs/SyncDemox.{#}.log"
                archiveNumbering="Rolling"
                archiveEvery="Day"
                maxArchiveFiles="7"
                layout="${longdate} | ${level:uppercase=true} | ${message}" />

                <!-- LastError.txt (只保留最後一個錯誤) -->
                <target name="lastError" xsi:type="File"
                fileName="user/LastError.txt"
                writeMode="Overwrite"
                layout="${longdate} | ${level:uppercase=true} | ${message}" />
        </targets>

        <!-- 定義 Logger 規則 -->
        <rules>
                <!-- Console 和 檔案都要接收 INFO 以上的 Log -->
                <logger name="*" minlevel="Info" writeTo="console, file" />
                <logger name="*" minlevel="Error" writeTo="lastError" />
        </rules>
</nlog>

如此設定將可以~
最後的錯誤訊息輸出到 LastError.txt;
全部的訊息輸出到 Console 和 logs/SyncDemo.log,並自動依照天數管控不超過 7 份。

實際運用起來如下:

internal class Program
{
    private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

    static void Main(string[] args)
    {
        // ...
        try 
        {
            Logger.Info("Start doing work!")
        }
        catch (Exception ex)
        {
            Logger.Error("SyncMdbToSQLServer Error: " + ex.Message);
        }
        // ...
    }
}

主要部分: 將 Mdb 資料複製 SQL Server

Program.cs

try
{
    using (var mdbconn = new OleDbConnection(mdbConnStr))
    {
        mdbconn.Open();
        Logger.Info("資料庫未被鎖定,準備開始讀取 MDB。");

        var result = SyncMdbToSQLServer(a, ini, mdbconn, sqlConnStr, targetTableName);

        WriteLastResult(a, result);
    }
}
catch (OleDbException ex)
{
    Logger.Error("無法開啟 MDB,可能被鎖定! ErrorMsg: " + ex.Message);
    WriteLastResult(a, false);
}
private static bool SyncMdbToSQLServer(ParserResult<CLOptions> a, IniReader ini, OleDbConnection mdbConn, string sqlConnStr, string targetTableName)
{
    try
    {
        var clo = a.Value;

        if (!int.TryParse(ini.Read("MaxRecentMonths", "Sync"), out int maxRecentMonths))
        {
            maxRecentMonths = -1; // All
        }
        if (clo.Verbose)
        {
            Logger.Info($"MaxRecentMonths: {maxRecentMonths}");
        }
        int currentMonth = DateTime.Now.Month;

        var cardDataLookup = mdbConn.Query<CardData>("SELECT EmNo, CardID, SiteID FROM CardData")
            .ToDictionary(x => new Tuple<int, int>(x.CardID, x.SiteID), x => x.EmNo);

        string whereDateClause = maxRecentMonths <= -1 ?
            string.Empty :
            $" AND DateT >= DateAdd('m', {-1 * (maxRecentMonths-1)}, Date())";

        // 查詢 AttRecords,並修正 EmNo 為 0 的情況
        string sSQL = "SELECT NoTe,DateT,NodeID,CardID,Events,SiteID,AddrID,AttRecords.EmNo,SetID,Name,Left " +
            "FROM [AttRecords] " +
            "LEFT JOIN EmData ON AttRecords.EmNo = EmData.EmNo " +
            "WHERE 1 = 1 And (Events = 3 OR Events = 11)" + whereDateClause;
        var attJoinEmRecords = mdbConn.Query<SimpleAttRecord>(
            sSQL
        ).Select(record =>
        {
            if (record.EmNo == 0) // 只有當 EmNo 為 0 時才修正
            {
                var key = new Tuple<int, int>(record.CardID, record.SiteID);
                if (cardDataLookup.TryGetValue(key, out int correctEmNo))
                {
                    record.EmNo = correctEmNo; // 替換為正確的 EmNo
                }
            }
            return record;
        }).ToList();
        Logger.Info($"Mdb has total {attJoinEmRecords.Count()} items.");

        SqlConnection sqlConn = new SqlConnection(sqlConnStr);
        sqlConn.Open();
        // Download all data from SQL Server to compare later
        var existedSqlItems = sqlConn.Query<MhPCard>($"SELECT * FROM [{targetTableName}]");
        var existedSqlHashItems = new HashSet<Tuple<int?, DateTime>>(existedSqlItems.Select(item => new Tuple<int?, DateTime>(item.Code, item.Date)));
        sqlConn.Dispose();

        // 找出不存在於 SQL Server 的資料
        var newRecords = attJoinEmRecords
            .Where(item => !existedSqlHashItems.Contains(new Tuple<int?, DateTime>(item.EmNo, item.DateT)))
            .ToList();
        Logger.Info($"New records: {newRecords.Count()}");

        using (SqlLauncher sqlLaun = new SqlLauncher(sqlConnStr, "dbo." + targetTableName, map=>
        { 
            map.Add(0,0);
            map.Add(1,2);
            map.Add(2,5); // Test
            // map.Add(2,6); //Real
        })
            )
        {
            foreach (var item in newRecords)
            {
                sqlLaun.Post(item.EmNo, item.DateT, item.NoTe);
            }
        }

        Logger.Info("BulkInsert succeed!");

        return true;
    }
    catch (Exception ex)
    {
        Logger.Error("SyncMdbToSQLServer Error: " + ex.Message);
        return false;
    }
}

主要步驟

  1. 讀取設定:從 ini 檔讀取同步範圍 (MaxRecentMonths)
  2. 讀取 MDB (Access) 資料庫
    var items = mdbConn.Query<CardData>("SELECT EmNo, CardID, SiteID FROM CardData")
  3. 較為個人特殊需求的步驟,讀者可以略過。
    運用 Dapper 取得 CardData 資料,建立 Dictionary<CardID, SiteID, EmNo>
    運用 Dapper 查詢 AttRecords,並根據 CardData 修正 EmNo
    運用 Dapper 讀取 SQL Server 現有資料:
  4. 從 SQL Server 下載 targetTableName 所有資料
    SqlConnection sqlConn = new SqlConnection(sqlConnStr);
    sqlConn.Open();
    var existedSqlItems = sqlConn.Query<MhPCard>($"SELECT * FROM [{targetTableName}]");
    sqlConn.Dispose();
    // 你也可以用 using 包起來,我單純是因為不想要太多巢狀括號不利閱讀。
  5. 建立 HashSet<Tuple<int?, DateTime>> 快速比對
    因為我每筆資料都要看有沒有存在的,如果不一次做好查表,就得不斷查詢 SQL Select,且查表可以將複雜度降低為 BigO(1)。

    var existedSqlHashItems = new HashSet<Tuple<int?, DateTime>>(
    existedSqlItems.Select(item => new Tuple<int?, DateTime>(item.Code, item.Date))
    );
  6. 比對找出新資料(MDB 但不在 SQL Server)
    
    var newRecords = attJoinEmRecords
    .Where(item => !existedSqlHashItems.Contains(new Tuple<int?, DateTime>(item.EmNo, item.DateT)))
    .ToList();

Logger.Info($"New records: {newRecords.Count()}");

7. 使用 SqlLauncher 進行批次插入
```csharp
using (SqlLauncher sqlLaun = new SqlLauncher(sqlConnStr, "dbo." + targetTableName, map=>
{ 
    map.Add(0,0);
    map.Add(1,2);
    map.Add(2,6);
}))
{
    foreach (var item in newRecords)
    {
        sqlLaun.Post(item.EmNo, item.DateT, item.NoTe);
    }
}
Logger.Info("BulkInsert succeed!");

將 Mdb 第 0 個欄位對應到 SqlServer 的第 0 個欄位;1對2;2對6。
※ 若報錯 NonNullable 注意你是不是以為已經設 Identity 但其實還沒設呢?或是你單純直接插入 Null 到該欄。

補充:其他程式個別實作

方便地讀取程式啟動參數

有了 CommandLineParser 便能創建一個物件來方便閱讀:

CLOptions.cs

public class CLOptions
{
    [Option('v', "verbose", Required = false, HelpText = "Set output to verbose messages.")]
    public bool Verbose { get; set; }

    [Option('j', "jobid", Required = false, HelpText = "Job ID.")]
    public string JobId { get; set; }

    [Option('t', "textoutputresultpath", Required = false, HelpText = "Text output result path. (Override Config's one)")]
    public string TextOutputResultPath { get; set; }

    [Option('p', "pause", Required = false, HelpText = "Pause before exit.")]
    public bool Pause { get; set; }
}

Program.cs

在程式中隨時可以讀寫這些屬性,不是唯讀的所以也可以像我另外實作 Ini 設定檔案牘取。

//...
var a = Parser.Default.ParseArguments<CLOptions>(args)
.WithParsed<CLOptions>(o =>
{

    if (o.JobId == null)
    {
        o.JobId = Guid.NewGuid().ToString();
    }
    if (o.Verbose)
    {
        Logger.Info($"{o.JobId} | Verbose output enabled (include sensitive info)");
    }
});

var clo = a.Value;
//...

IniReader.cs - Ini 檔案讀取

Ref: https://stackoverflow.com/a/14906422/3939608

/// <summary>
/// An INI file handling class.
/// </summary>
/// <see cref="https://stackoverflow.com/a/14906422/3939608"/>
public class IniReader
{
    string Path;
    string EXE = Assembly.GetExecutingAssembly().GetName().Name;

    [DllImport("kernel32", CharSet = CharSet.Unicode)]
    static extern long WritePrivateProfileString(string Section, string Key, string Value, string FilePath);

    [DllImport("kernel32", CharSet = CharSet.Unicode)]
    static extern int GetPrivateProfileString(string Section, string Key, string Default, StringBuilder RetVal, int Size, string FilePath);

    public IniReader(string IniPath = null)
    {
        Path = new FileInfo(IniPath ?? EXE + ".ini").FullName;
    }

    public string Read(string Key, string Section = null)
    {
        var RetVal = new StringBuilder(255);
        GetPrivateProfileString(Section ?? EXE, Key, "", RetVal, 255, Path);
        return RetVal.ToString();
    }

    public void Write(string Key, string Value, string Section = null)
    {
        WritePrivateProfileString(Section ?? EXE, Key, Value, Path);
    }

    public void DeleteKey(string Key, string Section = null)
    {
        Write(Key, null, Section ?? EXE);
    }

    public void DeleteSection(string Section = null)
    {
        Write(null, null, Section ?? EXE);
    }

    public bool KeyExists(string Key, string Section = null)
    {
        return Read(Key, Section).Length > 0;
    }
}

結論

我們可以 讓舊系統繼續運作,同時避免在 SQL Server 建立不必要的 Stored Procedure。

運用 Dapper 和 SqlLauncher,我們能夠:
✅ 有效讀取 MDB 資料
✅ 使用 HashSet 避免重複插入資料
✅ 透過 批次插入 提高效能

對我來說,SQL Server 就應該乾乾淨淨,專門存資料就好!

利用 AutoHotkey 在 Windows 模仿 macOS 的 Cmd + ` 切換應用視窗

身為一個長年使用 Windows 的使用者,自從入手 MacBook Air M1 之後,我深深體會到 Cmd+ 的便利性,特別是在同一個應用程式內切換不同視窗的流暢體驗。對於 Windows 使用者來說,通常需要 Alt+Tab 來回切換所有應用程式,無法限制在單一應用內。而這篇文章將教你如何透過 AutoHotkey 1.0 實現類似的快捷操作,讓你在同一個程式中快速切換不同的視窗。繼續閱讀...

Home Assistant 的 Yaml 處理換行字元發送 Line Push Message (Restful Request)

本篇並無一步一步地教學如何設定 Home Assistant 以將 Gmail 通知發送到 LINE,而是特別關注如何正確處理換行字元(“\n”)。這個看似簡單的需求,可能會由於 YAML、JSON 和各種 API 之間的複雜性而帶來許多頭痛的問題。

篇幅將專注在如何確保 LINE 訊息中的換行字元號正確顯示的問題。繼續閱讀...