2026-05-27

【軟體筆記】群暉(Synology) DS223j 設定 MariaDB + Python3 環境

想要在 DS223j 跑 python 排程,因此摸索寫了這篇筆記;雖然 DS223j 底層也是 Linux,不過算是變形的 Linux,有其獨特性,所以本筆記的做法都是以不影響安全性及效率為前提。

老人家的 DS223j 系統已更新至 DSM7.2


* 登入 DS223j web 管理系統 DSM,NAS 本身也有一些系統程式是以 python2 或 python3 撰寫,所以在【套件中心】會看到 python2 顯示已安裝,而 python3 雖然不是已安裝的狀態,但是若以 ssh 登入 DS223j 查看就會發現出廠時系統就安裝了 Python3.8.15

因為我的程式要寫連線 DS223j 的 MariaDB,所以需要安裝【pymysql】套件,所以要與系統的 Python3.8.15 分開,另外再安裝 套件中心 的 Python3.9

* 點選【套件中心】→【所有套件】→ 找到【Python3.9】並點擊安裝



* 以 ssh 連線進入 DS223j,本筆記採用的軟體為 MobaXterm,注意:NAS 和一般 Linux 不同,只允許系統管理者 ssh 連線。
python3.9 的安裝目錄路徑是 /var/packages/Python3.9/target/usr/bin/,執行檔名稱是 python3.9,在下圖中,可以看到套件中心自動為 python3.9 建立了 2 個捷徑,方便我們呼叫執行 python3.9。 


 
# 切換到家目錄
admin@DS223j: cd ~

# 下載並安裝 pip
admin@DS223j:~$ wget  https://bootstrap.pypa.io/pip/3.9/get-pip.py
admin@DS223j:~$ /var/packages/Python3.9/target/usr/bin/python3.9  get-pip.py

# 安裝 pymysql
admin@DS223j:~$ /var/packages/Python3.9/target/usr/bin/python3.9  -m  pip  install  pymysql
 


test.py
 
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import pymysql


# DS223j 預設關閉了 TCP/IP 連線協定
# 所以改用 socket
def get_db_connection_nas():
    """取得 NAS 資料庫連線"""
    # 請根據您的實際設定修改
    connection = pymysql.connect(
        user='資料庫連線帳號',
        password='密碼',
        database='資料庫名',
        unix_socket='/run/mysqld/mysqld.sock',
        charset='utf8',
        cursorclass=pymysql.cursors.DictCursor
    )
    return connection
    
    
def execute_and_print_query(pdo_nas, sql: str):
    print("\n" + "="*60)
    print(f"執行 SQL: {sql}")
    print("="*60)
    
    try:
        with pdo_nas.cursor() as cursor:
            # 執行 SQL 查詢
            cursor.execute(sql)
            rows = cursor.fetchall()
            if rows and len(rows) > 0:
                field_names = list(rows[0].keys())
                print(f"\n欄位: {', '.join(field_names)}")
                print("-"*60)
            
            # 取得記錄總數
            total_rows = len(rows)
            print(f"\n總共 {total_rows} 筆記錄\n")
            
            # 逐筆 print 出來
            for idx, row in enumerate(rows, 1):
                print(f"第 {idx} 筆記錄:")
                for field, value in row.items():
                    print(f"  {field}: {value}")
                print("-"*40)
            
            # 如果沒有記錄
            if total_rows == 0:
                print("查無任何記錄")
            
            return rows
            
    except Exception as e:
        print(f"執行 SQL 時發生錯誤: {e}")
        return None




def main():
    """主程式"""
    try:
        # 建立 mysql 連線
        pdo_nas = get_db_connection_nas()

        # 查詢記錄
        sql = "SELECT * FROM table1"
        result1 = execute_and_print_query(pdo_nas, sql)

        # 寫入紀錄
        sql_insert = f"""INSERT INTO table1
                        (field1, field2, field3)
                        VALUES
                        ('欄位1值', '欄位2值', '欄位3值')"""
                    
        print(f"sql = {sql_insert}")
        pdo_nas.cursor.execute(sql_insert)
        pdo_nas.commit()
        print("成功寫入")

    except Exception as e:
        print(f"發生錯誤: {e}")
        traceback.print_exc()
    


if __name__ == "__main__":
    main()
 


在儘量不更動 DS223j 的前提下,不改變 NAS 的環境變數,執行 test.py
 
# 須完整路徑,但可以是別名(捷徑),所以下面 3 種呼叫方式都行得通
admin@DS223j:/$ /var/packages/Python3.9/target/usr/bin/python3.9 test.py
admin@DS223j:/$ /var/packages/Python3.9/target/usr/bin/python3 test.py
admin@DS223j:/$ /var/packages/Python3.9/target/usr/bin/python test.py

 


2026-04-06

【Kotlin】自定義 app 結束對話框

本筆記搭配使用 ViewBinding, AdMob, Edge-to-Edge


app  層級 build.gradle
 
android {
    
    compileSdk {
        version = release(36)
    }
...
...

    buildFeatures {
        viewBinding = true
    }
    
    defaultConfig {
        ...
        ...
        minSdk = 26
        targetSdk = 36
    }
}

...
...

dependencies {

    ...
    ...
    
    // adMob
    implementation("com.google.android.gms:play-services-ads:25.1.0")
}
 


strings.xml
 
<resources>
    
    <string name="dialog_title">確定要離開?</string>
    <string name="dialog_message">您確定要關閉應用程式嗎?</string>
    <string name="btn_cancel">取消</string>
    <string name="btn_exit">離開</string>
    
    ...
    ...
 
    <!-- adMob app id 測試 -->
    <string name="admob_app_id">ca-app-pub-3940256099942544~3347511713</string>
    <!-- 橫幅 測試 -->
    <string name="banner_ad_unit_id">ca-app-pub-3940256099942544/6300978111</string>
 
</resources>
 


AndroidManifest.xml
 
<manifest
 
    <!-- 存取網路連線 -->
    <uses-permission android:name="android.permission.INTERNET" />
 
    ...
    ...
 
    <application  ... >
 
        <meta-data
            android:name="com.google.android.gms.version"
            android:value="@integer/google_play_services_version" />
        <meta-data
            android:name="com.google.android.gms.ads.APPLICATION_ID"
            android:value="@string/admob_app_id" />
 
        ...
        ...
 
        <!-- AdMob 的 AdActivity -->
        <activity
            android:name="com.google.android.gms.ads.AdActivity"
            android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize"
            android:theme="@android:style/Theme.Translucent" />
 
        ...
        ...
 
    </application>
 
</manifest>
 


activity_main.xml
 
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"    ← 注意這裡
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
 


自定義對話框 dialog_exit.xml,因為版面很單純,所以採用 LinearLayout
 
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:ads="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="24dp">

    <TextView
        android:id="@+id/tv_dialog_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/dialog_title"
        android:textSize="20sp"
        android:textStyle="bold"
        android:textColor="?attr/colorOnSurface"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:gravity="end"
        android:orientation="horizontal">

        <Button
            android:id="@+id/btn_cancel"
            style="?attr/borderlessButtonStyle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/btn_cancel" />

        <Button
            android:id="@+id/btn_exit"
            style="?attr/borderlessButtonStyle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:text="@string/btn_exit"
            android:textColor="?attr/colorError"/>

    </LinearLayout>

    <!-- 做為 banner 的容器 -->
    <FrameLayout
        android:id="@+id/adContainer"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:minHeight="250dp" />
        
</LinearLayout>
 


MainActivity.kt
 
class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    
    private var preloadedAdView: AdView? = null    // 預載的廣告物件
    

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        // 設定返回鍵攔截(支援新舊所有 Android 版本)
        setupBackPressedListener()
        
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }
        
        // 對話框的 AdMob banner
        preloadBannerAd()
    }
override fun onPause() { preloadedAdView?.pause() super.onPause() } override fun onResume() { super.onResume() preloadedAdView?.resume() } override fun onDestroy() { preloadedAdView?.destroy() super.onDestroy() } private fun setupBackPressedListener() { val callback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { // 當使用者按下返回鍵(或手勢返回)時,顯示對話框 showExitCustomDialog() } } // 將 callback 加到 Dispatcher 中,它會自動處理生命週期 onBackPressedDispatcher.addCallback(this, callback) } private fun showExitCustomDialog() { // 初始化 Dialog 的 View Binding val dialogBinding = DialogExitBinding.inflate(layoutInflater) // 將預載好的 AdView 放入對話框容器中 preloadedAdView?.let { adView -> // 避免 AdView 重複被加入不同的 Parent (adView.parent as? ViewGroup)?.removeView(adView) dialogBinding.adContainer.addView(adView) } val dialog = MaterialAlertDialogBuilder(this) .setView(dialogBinding.root) .setCancelable(true) .create() dialogBinding.btnCancel.setOnClickListener { dialog.dismiss() } dialogBinding.btnExit.setOnClickListener { dialog.dismiss() finish() } dialog.show() } private fun preloadBannerAd() { preloadedAdView = AdView(this).apply { adUnitId = getString(R.string.banner_ad_unit_id) setAdSize(AdSize.MEDIUM_RECTANGLE) // 開始非同步載入 loadAd(AdRequest.Builder().build()) } } }  

實際的對話框效果


2026-03-31

【Metabase】套用香港行政區進行分析 -- 區級行政區

參考資料 ----


到 香港開放數據平台 → 點擊【數據集】→ 輸入【地方行政區分界】→ 下面會跳出【地方行政區分界】和【歷史地方行政區分界】→ 點擊【地方行政區分界】

→ 下面列出查詢結果【數據資源】
→ 我們要的就是【地方行政區分界 (英文/繁體中文) (JSON)】 
→ 點擊【下載】鈕


香港的行政區有 18 個,所以下載的檔名為【hksar_18_district_boundary.json】。


2026-03-22

【Metabase】套用台灣地圖進行分析(二) -- 區、里(村)級行政區

參考資料 ----
github.com/g0v/twgeojson


github.com/g0v/twgeojson/json,有數個 json 檔,其中 twTown1982.geo.json區級行政區,twVillage1982.geo.json 則是 里(村)級

另有 twTown1982.topo.json 及 twVillage1982.topo.json,是 geo.json 的後繼優化格式,簡略記一下主要的差異:就是相鄰的 2 個區中間的界線不重複記錄。
例如:A 區與 B 區相鄰,geo.json 是完整地記錄 A 區的座標點 [1,2,3,4,5] 及 B 區的座標點 [1,6,7,8,9,2],如此 點 1點 2 就被記錄了 2 次,而 topo.json 則透過拓撲方法只記錄 1 次 點1到點2 的弧線,相較之下,topo.json 的檔案就比較小。





Metabase 目前只支援 geojson 格式,所以我們就下載這 2 個檔,以 twTown1982.geo.json 為例
點擊 twTown1982.geo.json
→ 點擊【下載】圖示下載 json

→ 將 twTown1982.geo.json 放置在 /var/www/html/ 下,然後在 Metabase 設定 twTown1982.geo.json 的 url,在本例為 http://localhost/twTown1982.geo.json

瞄一下檔案內容
 
{"type": "FeatureCollection",
 "features": [

 ...
 ...
 
    {"properties":{"TOWNSN":"64000002","TOWNID":"6400003","COUNTYNAME":"高雄市","TOWNNAME":"左營區","name":"高雄市/左營區"},
				   "type":"Feature",
				   "geometry":{"type": "Polygon", 
							   "coordinates": [[[ 120.274766995608132, 22.706593189102705 ], 
                                   ...
                                   ...
                               ]] 
							}
				},
    ...
    ...
 

登入 Metabase,點擊【齒輪】圖示
→ 管理員設定

→ 地圖 → 新增一個地圖


→【URL】輸入 http://localhost/twTown1982.geo.json
→【區域識別符】選擇 TOWNID
→【區域顯示名稱】選擇 TOWNNAME


g0v/twgeojson 的是 2014 年的資料,理論上這幾年並沒有重新劃分區級行政區,所以資料應該仍可信、準確。


g0v/twgeojson/json/twVillage1982.geo.json 則更細, 行政區範圍小至 "村/里",村/里的劃分就可能不定期會變動了,例如老人家居住的里聽說最慢 2027 年就將要拆成 4 個里。


建立區級資料表存放 geojson 的資料,在本筆記,資料表命名為 town_geo
 
CREATE TABLE `town_geo` (
    `id` INT AUTO_INCREMENT PRIMARY KEY,
    `TOWNSN` VARCHAR(20),
    `TOWNID` VARCHAR(20),
    `COUNTYNAME` VARCHAR(50),
    `TOWNNAME` VARCHAR(50),
    `name` VARCHAR(100),
    `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
 


寫一個 php 讀取 twTown1982.geo.json 並存入 town_geo
 
<?php
ini_set('display_errors', 1);
ini_set('memory_limit', '512M');    // 放寬記憶體限制, 才能讀大檔案

// 資料庫連線設定
$host = 'localhost';
$dbname = '資料庫名稱';
$username = '帳號';
$password = '密碼';

try {
    // 1. 建立 PDO 連線
    $pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8mb4", $username, $password);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // 2. 讀取並解析 GeoJSON 檔案
    $jsonFile = 'twTown1982.geo.json';
    if (!file_exists($jsonFile)) {
        die("錯誤:找不到檔案 $jsonFile");
    }

    $jsonContent = file_get_contents($jsonFile);
    $data = json_decode($jsonContent, true);

    if ($data === null) {
        die("錯誤:JSON 解析失敗");
    }

    // 3. 準備 SQL 插入語句
    $sql = "INSERT INTO town_geo (TOWNSN, TOWNID, COUNTYNAME, TOWNNAME, name) 
            VALUES (:townsn, :townid, :countyname, :townname, :name)";
    $stmt = $pdo->prepare($sql);

    // 4. 開始交易 (Transaction) 以提升大量寫入的速度
    $pdo->beginTransaction();

    $count = 0;
    foreach ($data['features'] as $feature) {
        $props = $feature['properties'];

        // 綁定參數並執行
        $stmt->execute([
            ':townsn'     => $props['TOWNSN'] ?? null,
            ':townid'     => $props['TOWNID'] ?? null,
            ':countyname' => $props['COUNTYNAME'] ?? null,
            ':townname'   => $props['TOWNNAME'] ?? null,
            ':name'       => $props['name'] ?? null
        ]);
        
        $count++;
    }

    // 5. 提交交易
    $pdo->commit();

    echo "成功!共匯入 $count 筆資料。";

} catch (PDOException $e) {
    // 發生錯誤時回滾
    if (isset($pdo) && $pdo->inTransaction()) {
        $pdo->rollBack();
    }
    echo "資料庫錯誤:" . $e->getMessage();
} catch (Exception $e) {
    echo "一般錯誤:" . $e->getMessage();
}
?>
 

如此就能結合自己的資料表,建立區級行政區的圖表了。




再建立里(村)級資料表,在本例為 village_geo
 
CREATE TABLE `village_geo` (
    `id` INT AUTO_INCREMENT PRIMARY KEY,
    `VILLAGESN` VARCHAR(20),
    `VILLAGEID` VARCHAR(20),
    `COUNTYNAME` VARCHAR(50),
    `TOWNNAME` VARCHAR(50),
    `VILLAGENAM` VARCHAR(50),
    `name` VARCHAR(150),
    `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
 


寫一個 php 讀取 twVillage1982.geo.json 並存入 village_geo
 
<?php
// 1. 提高記憶體限制(針對較大的村里 GeoJSON 檔案)
ini_set('memory_limit', '512M');
set_time_limit(0); // 取消執行時間限制

// 資料庫連線設定
$host = 'localhost';
$dbname = '資料庫名稱';
$username = '帳號';
$password = '密碼';

try {
    // 2. 建立 PDO 連線
    $pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8mb4", $username, $password);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // 3. 讀取 GeoJSON 檔案
    $jsonFile = 'twVillage1982.geo.json';
    if (!file_exists($jsonFile)) {
        die("錯誤:找不到檔案 $jsonFile");
    }

    echo "正在讀取檔案...\n";
    $jsonContent = file_get_contents($jsonFile);
    
    echo "正在解析 JSON...\n";
    $data = json_decode($jsonContent, true);
    
    // 解析完立刻釋放字串記憶體
    unset($jsonContent); 

    if ($data === null) {
        die("錯誤:JSON 解析失敗,請檢查檔案格式。");
    }

    // 4. 準備 SQL 插入語句
    $sql = "INSERT INTO village_geo (VILLAGESN, VILLAGEID, COUNTYNAME, TOWNNAME, VILLAGENAM, name) 
            VALUES (:v_sn, :v_id, :c_name, :t_name, :v_name, :full_name)";
    $stmt = $pdo->prepare($sql);

    // 5. 開始交易處理 (大量資料寫入必備)
    $pdo->beginTransaction();

    echo "正在匯入資料...\n";
    $count = 0;
    foreach ($data['features'] as $feature) {
        $props = $feature['properties'];

        $stmt->execute([
            ':v_sn'      => $props['VILLAGESN'] ?? null,
            ':v_id'      => $props['VILLAGEID'] ?? null,
            ':c_name'    => $props['COUNTYNAME'] ?? null,
            ':t_name'    => $props['TOWNNAME'] ?? null,
            ':v_name'    => $props['VILLAGENAM'] ?? null,
            ':full_name' => $props['name'] ?? null
        ]);
        
        $count++;
    }

    // 6. 提交交易
    $pdo->commit();
    echo "匯入完成!成功存入 $count 筆村里資料。";

} catch (PDOException $e) {
    if (isset($pdo) && $pdo->inTransaction()) {
        $pdo->rollBack();
    }
    echo "資料庫錯誤:" . $e->getMessage();
} catch (Exception $e) {
    echo "一般錯誤:" . $e->getMessage();
}
?>
 
匯入的記錄如下圖


如果您需要的圖資精細顆粒度要小到村/里國土測繪中心政府資料開放平臺(opendata) 也有提供圖資,感覺有提續在更新,可能會較適合您,但國土測繪中心沒有直接提供 geo.json 格式,我們需要轉換程式幫忙。

到 村里界圖(TWD97經緯度),點擊【資料資源下載網址】,下載的是壓縮檔,找個目錄解壓縮存放,會看到解壓出來的檔案中有副檔名 .shp 的檔案,這是主檔。



到 QGIS 下載格式轉換程式並安裝,藉助程式將 shp 轉換為 geo.json 格式。


選擇 shp 檔,在本例為 VILLAGE_NLSC_1150306.shp



功能表 【Layer】→【Save As】→【Format】欄位選擇 GeoJSON → 輸入檔名,在本例為 town_twd97 → 按下【確定】鈕


開啟檔案,會發現 properties 內的欄位跟上面 g0v 的不同,由此可知,properties 的欄位是各自定義的;所以,若您圖資是選擇國土測繪中心的,需注意匯入資料的欄位也會不同。



相關筆記 ----


2026-03-16

【Metabase】限制/區隔一般 user 的查詢權限

有時,基於工作/職務劃分,公司的資料會進行可視資料權限的控管;例如:行銷部的可以看到有關客戶名單,但不能看業務部有關銷售的業績、人事資料...等,反之亦然。

Metabase 社群版沒有限制使用者可視資料的功能(付費版才有),也就是說,當系統管理員建立了一個資料庫連線,則所有 Metabase 的一般使用者都能看到該連線的所有 table。

不過我們有變通做法 -- 從資料庫分別設定幾個限制權限的帳號,本筆記以 MySQL8 做範例,例如建立帳號 user1(代表業務部),只授予 user1 能查詢檢視 table1,建立帳號 user2(代表行銷部),只能查詢檢視 table2


root 帳號登入 phpMyAdmin 或 登入 Linux 開一個命令列視窗然後建立一個 MySQL 連線,本例示範在 phpMyAdmin 操作
 
-- 建立使用者
CREATE  USER  'user1'@'localhost' IDENTIFIED BY '密碼';

-- 授予 USER1 查詢資料庫 db1 的 table1 的權限
GRANT  SELECT  ON  db1.table1  TO  'user1'@'localhost';

-- 刷新權限(選擇性命令, 通常會自動生效)
FLUSH  PRIVILEGES;
 


驗證:登出 phpMyAdmin,以 user1 登入,就能看到 user1 只能看到 db1table1 的資料。


以管理員帳號登入 Metabase  設定 → 管理員設定

資料庫 → 新增資料庫(以 MySQL8 為例)


輸入在前面為行銷建立的帳號 user1,並在【顯示名稱】輸入 sales(Linuxmint22.2MySQL8 設定安裝可參考相關筆記),重複步驟建立 user2,【顯示名稱】輸入 marketing


到【權限管理】→【所有使用者群組】,這個群組預設是可查詢所有資料庫的,而且會高過其他群組的設定,所以要把 marketing、sales 的【建立提問】取消


設定業務部只能查詢 sales 資料庫


而行銷部只能查詢 marketing 資料庫



相關筆記 ----





2026-01-26

【MySQL8】匯入大容量 .sql 檔

phpMyAdmin 只能匯入容量 2M 以內的 .sql 檔,若要匯入大容量 .sql,則需要以命令列操作
 
# 切換到放置 .sql 檔的目錄下,
# 本例中, aa.sql 置於桌面
user$  cd  桌面

# 進入 su 模式
user$  sudo  su

# 匯入檔案
root#  mysql  -u  root  -p  資料庫名 < aa.sql
 


2026-01-25

【Metabase】設定委託 Gmail 定時發送通知信件

如果您設定的儀表板的數據是每日更新的,或是希望在您關心的某個數據發生較大的變化時(例如某日營業額突然暴量)通知您,就需要有台 mail server 寄送通知信件。

若您也沒有自己架設的 mail server,則可以申請 Gmail,設定 Metabase 委託 Gmail 寄送通知信件;Gmail 這項服務基本上是免費的,不過 Google 為了避免被用來惡意寄發垃圾信,所以有每日 500 封、且單封信件大小不可超過 10M 的限制


登入 Google 帳戶


點擊右上角的帳戶圖示 → 【管理你的 Google 帳戶


確認帳戶已啟用【兩步驟驗證】 → 在上方的搜尋框輸入【應用程式密碼


為應用程式命名,在本例中,就叫做 metabase。


系統產生 16 字的密碼,複製/記下來。


管理員登入 metabase → 點擊右上角的齒輪圖示 → 【管理員設定


點擊【電子郵件設定】 → 【編輯配置


參考下圖,填入各個欄位。


寄送測試信,會寄到管理員的信箱。


由於我的管理員 emailGmail(但與申請 smtp.gmail.com 的帳號不同),所以去 Gmail 看有沒有收到信,若有信寄來,就表示設定成功了。


拿 Metabase 現成自帶的範例儀表板實驗


打開 E-commerce Inshights 後,點擊儀表板右上角的 【分享】圖示 → 【訂閱


可以從 Metabase 的使用者名單選擇要訂閱的人,也可以自己輸入非 Metabase 使用者 → 選擇發送頻率。
若是同一個儀表板,有不同的人在看,在【訂閱】所指定的收信者是各自獨立設定的。
例如:A 已訂閱了 E-commerce Inshights,B 在【訂閱】是看不到有哪些人已經訂閱了 E-commerce Inshights 的。


點擊【現在發送郵件】,確認是否設定正確。


大約過 5 分鐘,去檢查信箱,就可以看到信件內容了。


信件是響應式(RWD) 的,包含了所有分頁的圖表及數據,個人覺得手機看也夠清楚。


2026-01-01

【Metabase】套用台灣地圖進行分析(一) -- 市級行政區

參考資料 ----

simplemaps


Metabase 預設自帶 世界地圖 & 美國地圖,如果需要台灣地圖幫助我們進行資料分析,則要手動新增台灣圖資。


simplemaps 下載台灣的 GeoJSON 的檔案 -- 檔名 tw.json,瞄一下 json 檔的內容,包含了一級行政區,其中屬性 id, name 是我們需要的。

 
{
  "type": "FeatureCollection", 
  "features": [
    {
...
...

  "type": "Feature", 
      "properties": {
        "source": "https://simplemaps.com", 
        "id": "TWKIN", 
        "name": "Kinmen"
      }, 
      
...
...
 



因為在【LinuxMint 22.2 安裝設定 MySQL 8.0 + phpMyadmin】這篇筆記中,我們安裝了 phpMyadmin,所以 LinuxMint 一併安裝了 Apache,其網站根目錄位置為 /var/www/html/

tw.json 放置在 /var/www/html/ 下,然後在 Metabase 設定 tw.jsonurl,在本例為 http://localhost/tw.json

登入 Metabase,點擊【齒輪】圖示

→ 管理員設定


→ 地圖 → 新增一個地圖



要注意的是:通常我們的資料庫中 縣/市 欄位並不是遵循 GeoJSONISO 名稱定義,所以需要做適當的轉換,例如:另外建一個對照用的 table

 
對照資料表:city_geo
+--------+--------+-------+
|   id   |  英文  |  中文  |
+--------+--------+-------+
| TWKIN  | Kinmen | 金門  | 
+--------+--------+-------+
|  ...   |  ...   |  ...  |
+--------+--------+-------+
 




相關筆記 ----

2025-12-31

【Metabase】新增一般使用者

點擊右上角的齒輪圖示

管理員設定



點擊【邀請他人


Metabase 是以 e-mail 做帳號管理,輸入 user 的 e-mail 後,若您 或 貴公司有自己的 mail serverMetabase 就會寄送含該 user 的密碼的信件到 e-mail 的信箱;在本筆記中,因沒有架設 mail server,所以要手動記下 Metabase 自動產生的密碼。

===== 2026.01.25 =====
可參考 設定委託 Gmail 定時發送通知信件 這篇筆記,設定 Gmail 做為 mail server,
不過要注意:一旦設定了 mail server ,之後新建的 Metabase 一般使用者的帳號就必須是真實的 e-mail,因為 Metabase 會將初始密碼寄到該 email

若有設定 mail server,就看不到上圖的初始密碼,Metabase 會將密碼直接寄送到 e-mail

登出管理員,讓剛剛新建的 user 登入


點擊齒輪圖示
帳號設定


輸入剛才 Metabase 產生的密碼,再變更為自己希望的密碼,如果不符合 Metabase 的密碼規則,就會看到提示。