刷 OpenWRT 韌體刷機記錄 – 華碩 ASUS RT-N16 路由器

查查部落格文章,原來我曾經有過 Asus RT-N10+Asus RT-N16 …等路由器。
這幾台其實可以相容 DD-WRT 與 Tomato 第三方韌體,
但現今,我還是會推薦使用 OpenWRT,功能多太多了。

因為華碩原廠開始用各種方式阻擋置換第三方韌體,熟悉的手感不見了,故記錄一下。

注意:刷機有風險,刷機前請詳閱說明書,刷壞了屬於個人行為,小弟也不負責。
刷機後很有可能失去保固,如果害怕弄壞就按左上角關閉本文吧!

開始之前,記得看文件

OpenWRT 的刷機指南
https://openwrt.org/toh/asus/rt-n16
找到對應的型號就開始吧,很多招都從這來的。

硬體概述

  • CPU: Broadcom BCM4718 480MHz
  • Flash: 32MB
  • RAM: 128MB
  • 2.4GHz Wi-Fi (b/g/n): 300Mbps
  • 5 ports Gigabit ethernet 10/100/1000 Mbps
  • USB 2.0 x 2

材料準備

  1. 去華碩 ASUS 官網下載 Firmware Restoration 韌體更新程式
    ASUS Firmware Restoration 版本 2.1.0.2

https://www.asus.com/tw/supportonly/rtn16/helpdesk_bios/

並在 Windows 電腦安裝起來。

  1. 下載該型號最初版的韌體 (Firmware)

ASUS RT-N16韌體版本 3.0.0.4.374.979 (2013/10/09)
https://www.asus.com/tw/supportonly/rtn16/helpdesk_bios/

這是該型號最初版的韌體。

  1. DD-WRT 的 INITIAL 中繼過渡韌體 (Firmware)

依據 DD-WRT 官方文件所說需要下載這一份 22118 K2.6_mini_RT-N16.trx 過渡韌體
https://download1.dd-wrt.com/dd-wrtv2/downloads/betas/2013/07-24-2013-r22118/broadcom_K26/dd-wrt.v24-22118_NEWD-2_K2.6_mini_RT-N16.trx

(過渡韌體檔名為 dd-wrt.v24-22118_NEWD-2_K2.6_mini_RT-N16.trx 供參考)

  1. OpenWRT 該型號的最新版韌體 (Firmware)

https://downloads.openwrt.org/releases/22.03.5/targets/bcm47xx/mips74k/openwrt-22.03.5-bcm47xx-mips74k-asus_rt-n16-squashfs.trx

(截稿至今,檔名為: openwrt-22.03.5-bcm47xx-mips74k-asus_rt-n16-squashfs.trx 僅供參考。)

安裝步驟

相關步驟筆記如下,可能寫得有點雜,請見諒。

因為新版韌體「 3.0.0.4.380.7378 (2017/04/26) 」的網頁介面有鎖非原廠韌體更新,故要先退版。

  1. 設定好電腦本機 IP: 192.168.1.2 子網路遮罩: 255.255.255.0
  2. 進入「救援模式 (Rescue mode)」(等下會敘述)
  3. 使用剛剛下載的 ASUS Firmware Restoration 韌體更新程式 刷機

這裡有二種選擇,

刷入最初版的韌體 (Firmware):3.0.0.4.374.979 (2013/10/09)

完成後會進入 原廠韌體介面,確認版號之後,直接從路由器管理介面,直接更新韌體,刷最新版 OpenWRT 韌體即可。

(截稿至今,檔名為: openwrt-22.03.5-bcm47xx-mips74k-asus_rt-n16-squashfs.trx 僅供參考。)

或者

刷入 DD-WRT 的 INITIAL 過渡韌體
dd-wrt.v24-22118_NEWD-2_K2.6_mini_RT-N16.trx 供參考)

完成後會進入 DD-WRT,但這個版本不能直接使用(所以才稱做中繼過渡韌體)
使用電腦 Firefox,透過這個過渡版本的路由器網頁管理介面
,刷最新版 OpenWRT 韌體即可。

(截稿至今,檔名為: openwrt-22.03.5-bcm47xx-mips74k-asus_rt-n16-squashfs.trx 僅供參考。)

救援模式 (Rescue mode)

新版韌體「 3.0.0.4.380.7378 (2017/04/26) 」的救援模式進入方式有改版,但如果你刷入了舊版韌體「3.0.0.4.374.979 (2013/10/09)」,救援模式進入方式會變成舊方式。

我二種方式都列出來供大家參考。

  • 舊版:路由器斷電後,按住 restore 鍵不放並插電,持續 5 秒,電源燈會變成慢閃 (2 秒亮一下)
  • 新版:路由器斷電後,然後按住紅色 WPS 按紐,然後插電,等到看到電源燈亮了 (大約 1 秒) 之後再按住 restore 直到電源燈慢閃 (2 秒亮一下)

如何判斷是否有進入救援模式?

如何判斷是否有進入救援模式?
我覺得電腦連接網路線,用 ping 最準。

設定好電腦本機 IP: 192.168.1.2 子網路遮罩: 255.255.255.0

用 ping 加上 -t 參數,變成連續 ping 模式

ping 192.168.1.1 -t

你會發現 TTL=64 (正常模式) 變為 TTL=100 (救援模式)

Reply from 192.168.1.1: bytes=32 time<1ms TTL=64
Reply from 192.168.1.1: bytes=32 time<1ms TTL=100

有變成 TTL=100 就代表成功進入救援模式了,有時候可能要多試幾次才會成功進去,就算進入救援模式了

幾個須注意的重點 (雷點)

  • 如果你的電腦處於複雜的網路環境,例如有裝 Hyper-V 虛擬機、 VMWare 虛擬機
    又有 WiFi 等等多個網路裝置
    把這些網路裝置先停用,把網路環境單純化

  • 救援模式的方式有偷偷改過,但如果刷舊版韌體會被改回來
    總而言之,可以先試試看舊版,如果不行再試試看新版方式

  • 過渡版 DD-WRT 網頁介面刷機的時候,Google Chrome 會出現 ERROR_EMPTY_RESPONSE 的離奇問題
    實測後用火狐 Firefox 就不會出現此問題,在這裡使用 Firefox。

希望整個流程對你有幫助。

參考資料

刷 OpenWRT 韌體刷機記錄 – 華碩 ASUS RT-AC58U 路由器

剛好拿到一台華碩 ASUS RT-AC58U AC1300 路由器,華碩硬體做得還不錯,很耐用。
雖然不是 WiFi 6,但 WiFi 5 AC1300 (400 + 867 Mbps) 可以勇一陣子,再戰十年。

這算是我第五台刷機的機器了,該有步驟的還是會講一下。

我個人很愛開源第三方韌體,從 DD-WRT 玩到 Tomato,直到 OpenWRT (LEDE) 橫空出世,可玩性就在這裡了。
OpenWRT 可以突破原廠限制,很多商用進階功能,直接下放到家用機種,穩定性也比原廠韌體高,這就是我刷機的原因。

注意:刷機有風險,刷機前請詳閱說明書,刷壞了屬於個人行為,小弟也不負責。
刷機後很有可能失去保固,如果害怕弄壞就按左上角關閉本文吧!

開始之前,記得看文件

OpenWRT 的刷機指南
https://openwrt.org/toh/asus/rt-ac58u
很多沒提到的細節都在這裡

硬體概述

  • CPU: Qualcomm Atheros IPQ4018 或 IPQ4019 717Mhz
  • Flash: 128MB
  • RAM: 128MB
  • 2.4GHz Wi-Fi (b/g/n): 400Mbps
  • 5GHz Wi-Fi (a/n/ac): 867Mbps
  • 5 ports Gigabit ethernet
  • USB 3.0 x 1

安裝方式

注意:刷機有風險,刷機前請詳閱說明書,刷壞了屬於個人行為,小弟也不負責。
刷機後很有可能失去保固,如果害怕弄壞就按左上角關閉本文吧!

刷機前請再三確認型號、硬體版號、國際版還是地區版,刷錯型號會直接變磚。

參考步驟為 OpenWRT 的刷機步驟

  1. 到 Zyxmon’s AC58U 網頁,下載我們的中繼韌體 (Firmware) XXX-squashfs-flash-factory.trx 檔案。

(截稿至今,檔名為: openwrt-r1834-0f04829-ipq806x-asus_rt-ac58u-squashfs-flash-factory.trx 僅供參考。)

  1. 瀏覽器鍵入 http://192.168.1.1/ 登入華碩的路由器管理介面

  2. 在 系統管理 Administration → 韌體更新 Firmware Upgrade
    (可能的頁面網址是 http://192.168.1.1/Advanced_FirmwareUpgrade_Content.asp
    上傳 XXX-squashfs-flash-factory.trx 檔案,重開機後會進入 OpenWRT (LEDE),不過這不要太高興,這個中繼版本有一些 Bug,沒辦法使用。

  3. 在終端機用指令打開 SSH
    (以下是用 Mac 做為連線,理論上 Windows 的 Powershell 也適用,或者使用 putty ,SSH 的連線方式就不贅述了。)

ssh [email protected]

依序鍵入以下指令:

# umount /mnt/ubi0_5
# ubirmvol /dev/ubi0 -N jffs2

意思大致為:刪除 jffs2 分割區,解除其保護機制。

  1. OpenWRT 網站 下載最新版更新版 OpenWRT 韌體,
    檔名 XXX-squashfs-sysupgrade.bin

(截稿至今,檔名為: openwrt-22.03.2-ipq40xx-generic-asus_rt-ac58u-squashfs-sysupgrade.bin 僅供參考。)

  1. 瀏覽器鍵入 http://192.168.1.1/ 到這個半殘中繼韌體的路由器管理介面,
    上傳韌體(可能的頁面網址是 http://192.168.1.1/cgi-bin/luci/admin/system/flashops),記得 Keep settings 的勾勾要拿掉,不要保留設定。

  2. 重開機就可以使用完整版 OpenWRT 了。

希望這篇文章對大家有幫助。

參考資料

關於 Kubernetes (K8S) 二個 Pod 與 Service 連線的一些細節

在 Kubernetes 的微服務架構中,了解 Pod 之間的連線細節是至關重要的一環。透過這篇文章的例子,深度探討 Kubernetes 的工作原理,讓讀者不僅理解 Pod 與 Service 之間的基本關聯,還能掌握其背後的機制與細節。我們將從 Pod 的概念和建立開始,進一步講解 Service 的角色和功能,並討論如何實現兩者間的連線。希望通過這篇文章,讀者能夠更具信心地運用 Kubernetes,無論是管理現有的微服務,還是設計新的應用架構。我們將嘗試將這些概念以最簡潔明了的方式呈現,使初學者和專業人士都能從中獲益。本文章帶你深入淺出,一窺 Kubernetes 的核心,理解與掌握 Pod 連線的關鍵知識。

範例需求

  • 建置二個 Deployment 而讓他們能夠內網互相溝通
  • 用一個 LoadBalancer 對應到其中一個 Deployment

配置範例

以下是一個 Kubernetes 配置範例,建立兩個 Deployment 並讓它們能夠內網互相溝通,以及一個 LoadBalancer 服務對應到其中一個 Deployment:

deployment.yml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-1
spec:
  replicas: 2
  selector:
    matchLabels:
      app: app-1
  template:
    metadata:
      labels:
        app: app-1
    spec:
      containers:
      - name: container-1
        image: j796160836/simple-test-http:latest
        ports:
        - containerPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-2
spec:
  replicas: 2
  selector:
    matchLabels:
      app: app-2
  template:
    metadata:
      labels:
        app: app-2
    spec:
      containers:
      - name: container-2
        image: j796160836/simple-test-http:latest
        ports:
        - containerPort: 80

service.yml

apiVersion: v1
kind: Service
metadata:
  name: service-1
spec:
  selector:
    app: app-1
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
  name: service-2
spec:
  selector:
    app: app-2
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  type: LoadBalancer

這個範例中:

  1. 建立了兩個 Deployment,分別名為 deployment-1deployment-2。每個 Deployment 都有 2 個副本,分別使用標籤 app: app-1app: app-2

  2. deployment-1deployment-2 建立了兩個對應的 ClusterIP 服務,分別名為 service-1service-2。這兩個 ClusterIP 服務會將流量轉發到標籤為 app: app-1app: app-2 的 Pod。

  3. deployment-2 建立了一個名為 service-2 的 LoadBalancer 服務,將外部流量轉發到標籤為 app: app-2 的 Pod。

通過這個配置,兩個 Deployment 的 Pod 可以通過 ClusterIP 服務在內網進行通信,而 LoadBalancer 服務則允許外部流量來存取其中一個 Deployment 的 Pod。

備註:type: LoadBalancer 這個設定值只會在雲端服務
(例如:GCP (Google Cloud Platform) 裡面的 Google Kubernetes Engine (GKE) 、
AWS (Amazon Web Services) 的 Amazon Elastic Kubernetes Service (EKS)、
Microsoft 的 Azure Kubernetes Service (AKS))才會生效,
自行架設 on-premise 的 Kubernetes 叢集不會有動作,除非有另外做一些設定。

準備就緒,我們把它部署上來

$ kubectl apply -f deployment.yaml 

然後可以用 kubectl get pods 來查看 Pod 運作情形

$ kubectl get pods -n default
NAME                            READY   STATUS    RESTARTS   AGE
deployment-1-79c659f4ff-kkvgx   1/1     Running   0          112s
deployment-1-79c659f4ff-wf4kk   1/1     Running   0          112s
deployment-2-76d567869f-cgts7   1/1     Running   0          112s
deployment-2-76d567869f-fpqsm   1/1     Running   0          111s

別忘記部署 service

$ kubectl apply -f service.yaml 

我們使用 kubectl get services 來查看 services 的運作情況

$ kubectl get services -n default
NAME         TYPE           CLUSTER-IP    EXTERNAL-IP     PORT(S)          AGE
service-1    ClusterIP      10.54.3.115   <none>          80/TCP           48s
service-2    LoadBalancer   10.54.3.33    34.xxx.xxx.123   80:32103/TCP     48s

筆記備註:
deployment 為一個部署計劃,裡面定義了 Pod spec,意指這個 Pod 樣板裡面有什麼 container。
replicas 標籤來定義這個樣板要跑幾個副本

通常一個 Pod 裡面只會有一個 container,在其他情況(例如需要 sidecar 的時候)才會一個 Pod 裡面有不只一個 container。


我的 app-2 要怎麼 ping 到 app-1 ?

想必你也跟我有一樣的問題, app-2 要怎麼 ping 到 app-1?讓我們一一解釋。

在 Kubernetes 集群中,你可以使用內部的 Service DNS 名稱來讓 app-2 的 Pod 連接到 app-1 的 Pod。在本例中,app-2 可以透過 service-1 服務名稱來存取 app-1

假設你的應用程式支持從環境變數讀取目標服務的 DNS 名稱,你可以在 deployment-2 的 Pod 模板中添加一個環境變數,指向 service-1 的 DNS 名稱,例如:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-2
spec:
  replicas: 2
  selector:
    matchLabels:
      app: app-2
  template:
    metadata:
      labels:
        app: app-2
    spec:
      containers:
      - name: container-2
        image: your-image-repo/image-2:latest
        ports:
        - containerPort: 80
        env:
        - name: APP_1_SERVICE_URL
          value: "http://service-1.default.svc.cluster.local:80"

這裡,我們為 container-2 添加了一個環境變數 APP_1_SERVICE_URL,其值為 http://service-1.default.svc.cluster.local:80。這個環境變數將被傳遞給 app-2 的應用程式,讓它可以連接到 app-1

在你的 app-2 應用程式中,你需要使用這個環境變數(例如,APP_1_SERVICE_URL)作為 app-1 服務的基礎 URL 進行連接。根據你的應用程式語言和框架,讀取環境變數的方法可能會有所不同。

例如,如果你的應用程式是用 Python 編寫的,你可以使用以下方式讀取環境變數:

import os

app_1_service_url = os.environ['APP_1_SERVICE_URL']

之後,你可以使用 app_1_service_url 作為 app-1 服務的基礎 URL 進行連接。

我們來做一個測試,嘗試把 container 裡面 console 掛進去看看

先列出 Pod,找到你要的 Pod

$ kubectl get pods -n default
NAME                            READY   STATUS    RESTARTS   AGE
deployment-1-79c659f4ff-kkvgx   1/1     Running   0          112s
deployment-1-79c659f4ff-wf4kk   1/1     Running   0          112s
deployment-2-76d567869f-cgts7   1/1     Running   0          112s
deployment-2-76d567869f-fpqsm   1/1     Running   0          111s

將 console 掛進去

$ kubectl exec -it -n my-namespace deployment-1-79c659f4ff-kkvgx -- /bin/bash

然後做 curl 瀏覽看看

root@deployment-1-79c659f4ff-kkvgx:/# curl service-2.my-namespace.svc.cluster.local
<!DOCTYPE html>
<html>
<head>
....(後略)

可以成功連線!

關於內部 DNS 名稱

你或許會問: service-1.default.svc.cluster.local 是固定值嗎? 每次 deploy 會不會變更呢?

service-1.default.svc.cluster.local 是一個 Kubernetes 服務的內部 DNS 名稱。這個名稱是根據你的服務名稱和命名空間生成的。在本例中,服務名稱是 service-1,命名空間是 default

DNS 名稱的規則為 <service-name>.<namespace>.svc.cluster.local

這個 DNS 名稱在 Kubernetes 集群中是固定的,只要你不更改相應的服務名稱和命名空間。每次部署時,只要保持相同的服務名稱和命名空間,這個 DNS 名稱就不會變更。

在本例中,每次部署時,只要你保持服務名稱為 service-1 和命名空間為 default
那這個 service-1.default.svc.cluster.local 的 DNS 名稱就不會變更。

當然,如果你將服務名稱或命名空間更改為其他值,則對應的 DNS 名稱也會相應更改。在這種情況下,你需要在應用程式配置或部署文件中更新相應的 DNS 名稱。


Cleanup

做完實驗了,我們把剛剛建的這些東西都清掉(刪除),避免在雲端服務產生不必要的費用,這一步是很重要的。

$ kubectl delete deployments -n my-namespace deployment-1
deployment.apps "deployment-1" deleted

$ kubectl delete deployments -n my-namespace deployment-2
deployment.apps "deployment-2" deleted

$ kubectl delete services -n my-namespace service-1
service "service-1" deleted

$ kubectl delete services -n my-namespace service-2
service "service-2" deleted

依序把建立出來的 deployment、 service 給刪除

kubectl delete deployments <deployment>
kubectl delete services <services>
kubectl delete pods <pods>
kubectl delete daemonset <daemonset>

參考資料

https://sharegpt.com/c/feBhUAr

[教學] 用 Docker 的 buildx 輕鬆多架構編譯 (multi-architecture build)

Docker 可以在程序層級上做為封裝系統環境的技術,並方便部署(或移轉)到別的機器上,
比起可以做到相同目標的 虛擬機 (VM) 比它輕量快速,近期成為熱門技術之一。
這麼好的技術還是有其相關限制的。例如: Docker image (映像檔) 仍受限於 CPU 架構 (architecture),
但標準的 Docker 的指令預設只能編譯一個 CPU 架構 (architecture)。

接下來要介紹算是某種新功能,新的 buildx 指令,讓 Docker 編譯製作 image (映像檔) 時,
能一次編譯各種你要的 CPU 架構的 image。


不知道您有沒有自己做過 Docker image 過?
我們複習一下標準的 Docker 編譯

原有的 docker build 流程

標準的 Docker 編譯如下:

$ docker build . -t MY_ACCOUNT/my-awesome-image:latest

相關說明:

  • . 點 (dot):代表當前目錄,也是預設名稱 Dockerfile 檔案名稱
  • -t 參數:給予 image 名字,如果要上傳到 Registry 倉庫的話,要改成對應名字
    例如: dockerHub 的格式 帳號 / 名稱,這個例子為: MY_ACCOUNT 帳號下的 my-awesome-image 套件,版本為 latest

再來就是用指令登入你的 dockerHub

$ docker login

輸入帳號與密碼

最後,上傳你的 image 到 Registry 倉庫

$ docker push MY_ACCOUNT/my-awesome-image:latest

但如今,宿主 (Host) 的 CPU 架構會因為你的使用伺服器環境的不同而不同
例如:你買了 Apple silicon (M1) 晶片,預設 CPU 架構跑 arm64
又例如:你想把你的 Docker 環境跑在樹莓派 (Raspberry Pi), CPU 架構跑 armv7 或者 arm64

各種情境,你可能會需要把你的 Docker 編譯模組換成能一次編輯多架構的。

新的 buildx 指令

要達成此目的,做法有二種,只講其中一種, docker buildx 指令

docker buildx 算是全新的指令,可以在官網去下載設定。

$ docker buildx version

查看是否有安裝,執行結果如下(我列出我的環境,你的可能會跟我不一樣):

$ docker version
Client:
 Cloud integration: v1.0.22
 Version:           20.10.13
 API version:       1.41
 Go version:        go1.16.15
 Git commit:        a224086
 Built:             Thu Mar 10 14:08:43 2022
 OS/Arch:           darwin/arm64
 Context:           default
 Experimental:      true

Server: Docker Desktop 4.6.0 (75818)
 Engine:
  Version:          20.10.13
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.16.15
  Git commit:       906f57f
  Built:            Thu Mar 10 14:05:37 2022
  OS/Arch:          linux/arm64
  Experimental:     false
 containerd:
  Version:          1.5.10
  GitCommit:        2a1d4dbdb2a1030dc5b01e96fb110a9d9f150ecc
 runc:
  Version:          1.0.3
  GitCommit:        v1.0.3-0-gf46b6ba
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

查看有哪些架構可用

$ docker buildx inspect --bootstrap
Name:   mybuilder
Driver: docker-container

Nodes:
Name:      mybuilder0
Endpoint:  unix:///var/run/docker.sock
Status:    running
Platforms: linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/mips64le, linux/mips64, linux/arm/v7, linux/arm/v6

以小弟的電腦來說,有這麼多,你也不一定要全部都使用。

我選了幾個常用的

  • linux/amd64:適合 x64 的環境(不管宿主主機是 linux 還是 Mac )
  • linux/arm64:適合 arm64 的環境(例如:Apple silicon (M1) 晶片)
  • linux/arm/v7:適合 arm 環境(例如:樹莓派)

接下來,我們建立一個 builder 並使用(初次安裝執行一次即可)

$ docker buildx create --use

(其實他是建立 builder 與 使用 builder 的簡化版,相關指令我列在下方)

建立 builder

$ docker buildx create --name mybuilder

使用該 builder

$ docker buildx use mybuilder

列出有哪些 builder

docker buildx ls

更新原有 build 指令

我們更新原有 build 指令

$ docker build . -t MY_ACCOUNT/my-awesome-image:latest

變成這樣

$ docker buildx build --load -t MY_ACCOUNT/my-awesome-image:latest --platform linux/amd64 .

(為了開發方便,我們先編譯一個平台架構)

效果二者一樣。

如果 Dockerfile 編好了,準備編譯並打包上傳 dockerhub 可以用以下指令:

$ docker buildx build --push -t MY_ACCOUNT/my-awesome-image:latest --platform linux/amd64,linux/arm64,linux/arm/v7 .

加個 --push 參數就可以在編譯完成的時候,一併做上傳。

瀏覽你的 dockerHub 帳號,會看到多種你選取的平台出現在 dockerHub 中。

小提醒:別使用 docker push 指令,截稿至今,它還不支援多架構映像檔上傳,它只會幫你上傳單一架構而已,
你也可以測試看看。

以上就是這次的內容,希望對大家有幫助。

參考資料

解決 MySQL 資料庫備份還原錯誤 ERROR: ASCII ‘\0’ appeared in the statement

最近遇到一個情境,在 Windows 使用 PowerShell 使用 mysqldump 把 MySQL / MariaDB 資料庫匯出 SQL 檔案,然後在 Linux 環境底下用 mysql 指令匯入,

卻出現一個很莫名錯誤:

$ mysql -u root -p myDatabase < /backup.sql
Enter password: 
ERROR: ASCII '\0' appeared in the statement, but this is not allowed unless option --binary-mode is enabled and mysql is run in non-interactive mode. Set --binary-mode to 1 if ASCII '\0' is expected. Query: '-'.

這原因出現在 PowerShell,的 > 資料流重導向 (I/O Redirection) 語句,
會使用 UTF-16LE 和 CR/LF 換行符號來建立檔案。

這些在 mysql 指令中不認得,他目前只認 UTF-8。
所以要做一些修改。

從 MySQL / MariaDB 資料庫匯出

mysqldump 匯出指令有修改

原本為:(此在 Windows (PowerShell) 環境下執行)

PS> .\mysqldump -h localhost -u root -p myDatabase > backup.sql

改為新指令

PS> .\mysqldump -h localhost -u root -p myDatabase -r backup.sql

改用 -r 指令,讓 mysqldump 「直接操作檔案」,這樣就不會出現這個問題了。

匯入新的 MySQL / MariaDB 資料庫

匯入指令相同:(此在 Linux 環境下執行)

$ mysql -u root -p myDatabase < /backup.sql

這樣就可以正確匯入了。

參考資料

[教學] Google 表單 製作多選複選選項 與限制總量數量(團購表單好用)

因為 Choice Eliminator 2 這個第三方外掛已經不能用了(第三方外掛的廠商已經下線了)
這陣子就在找替代方案,發現這一招還是可用,分享給大家。

本教學會帶入部分 Google 表單與 Google 試算表的使用,
看不懂也不用擔心,會一步一步帶大家建立。

教學步驟


(這是範例成品)

我們在這舉一個例子,假設是一個蛋糕團購表單,有六個選項,
在六個選項中,選擇二個:

  • 原味
  • 抹茶
  • 起司
  • 草莓
  • 芋頭
  • 巧克力

在這團購表單很常見。

Step1. 建立表單

首先先建立一個 Google 表單,

網址:
https://docs.google.com/forms/

按新增

這邊用打入標題、 等聯絡資訊。
用簡答功能打入 姓名、電話、地址 等聯絡資訊。
旁邊有加號可以新增題目,這功能就不贅述。

再來就是重頭戲,新增一個「核取方塊」,
打入 原味、抹茶、起司、草莓、芋頭、巧克力 六個選項

在右下角的點點點選項,選擇「回應驗證」

就會多出一行選項

選擇「選取剛好」,數量打入 2,最後輸入提示訊息

再來點到「回覆」頁籤,右上角一個小按鈕「建立試算表」

選取回應目的地的視窗中
選建立新試算表,輸入名字,然後按「確定」

動畫就像這樣

你就會看到打開了一個 Google 試算表。

Step2. 製作回應表單的試算表

在試算表下方按下「 + 」,建立試算表

我們建立二個新的試算表,名字分別叫做:

  • 選項拆解
  • 統計

像這樣

Step3.「選項拆解」的試算表設計

在「選項拆解」的試算表中,

A1 打入「選項」
B2 ~ G2 依序打入「原味、抹茶、起司、草莓、芋頭、巧克力」等選項

接下來這個騷操作,要一步完成
先選擇 A2 這格,先打一個「 = 」(等號)
然後直接選擇「表單回應 1」,的第一格選項欄位

你會看到公式跳出來

='表單回應 1'!E2

動畫在這:

未來填單有資料的時候就會這樣

意思是說,單引號括起來的是試算表的名字,
意指「表單回應 1」試算表裡的 E2 欄位。

如果你是直接複製公式的話,
要注意單引號與雙引號,還有要注意欄位格子是否正確。

按下 Enter 之後,他會顯示空白的是正常現象,選取上去就會該格子就會有公式出現。

Step3-1.「選項拆解」試算表套用搜尋公式

選到 B2 這格,打上公式:

=IF(IFERROR(FIND(B$1, $A2), 0) > 0, 1, 0)

然後選上 B2 這格,直接拖拉到 G2,讓這列的選項都填滿了公式。

動畫:


如果有興趣的朋友,這邊小小解釋一下這個公式的原理:

FIND() 輸入三個參數:

  • 要找的文字
  • 原始文字
  • 從第幾個字開始找(可忽略不填)

白話文就是在第二個參數中,找第一個參數的文字,在哪個位置,
從第三個參數開始找,第三個參數可忽略,就是從第一個字開始找。

輸出就是在第幾個字出現的位置。

FIND() 的說明文件:
https://support.google.com/docs/answer/3094126?hl=zh-Hant

IFERROR() 輸入二個參數:

  • 要執行的公式
  • 出錯時要顯示的字

第一個參數是執行公式,如果它出錯了,就顯示第二個參數值。

IFERROR() 的說明文件:
https://support.google.com/docs/answer/3093304?hl=zh-Hant

IF() 就是判斷,輸入三個參數:

  • 判斷的公式
  • true 時顯示文字
  • false 時顯示文字

算蠻好理解的。

IF() 的說明文件:
https://support.google.com/docs/answer/3093364?hl=zh-Hant

整段白話文就是:我們把選項拆解,有搜尋到字的欄位標成 1 否則是 0


選取 要估算一下會填單的人數,整列選起來往下拉,讓每列都填上公式。
至於要拉幾列,當然 越多越好

這表格意旨在拆解選項並填到各個對應的格子裡。

Step4.「統計」的試算表設計

切到「統計」的試算表

A1 ~ E1 欄位分別打上「顯示、選項、額滿提示、已達數量、數量上限」
然後 B2 ~ B7 依序分別打上總共有的「原味、抹茶、起司、草莓、芋頭、巧克力」選項,
額滿提示可以打上你想打的額滿提示,範例是在最後加上(完售)字樣。

如果你想隱藏該選項,在額滿提示直接留空白。

然後在 E2 ~ E7 打入你要的數量上限。

Step4-1.「統計」試算表套加總公式

選擇 D2 套入加總公式

=SUM('選項拆解'!B:B)

這句 SUM() 的意思是:做 「選項拆解」試算表裡的 B 欄所有列的加總,

然後如法炮製,依序在 D3 套入公式

=SUM('選項拆解'!C:C)

在 D4 套入公式

=SUM('選項拆解'!D:D)

以此類推,套到 D7

=SUM('選項拆解'!G:G)

這邊重點要注意統計欄位是不是正確

Step4-2.「統計」試算表套顯示判斷公式

選擇 A2 這格,套入公式

=IF(D2<E2,B2,C2)

這個應該很好理解,就是數量統計超過上限時,顯示額滿提示,否則顯示原本的選項名

然後往下拉,填滿到 A7

為何要這樣設計呢?為了配合等下要提到的 Form Ranger 外掛

Step5. 安裝 Form Ranger 表單外掛

在右上方點點點點開「外掛程式」,搜尋「Form Ranger」

也就是這個外掛:
https://workspace.google.com/u/5/marketplace/app/form_ranger/387838027286?hl=zh&pann=forms_addon_widget

安裝,

選擇帳號,

瀏覽授權權限,按下「允許」,

允許這個第三方外掛。

Step6. 使用 Form Ranger 表單外掛

然後回到剛剛做的表單,在右上角多一個小積木
選擇「formRanger — PROD」

點「Start」

會看到你的多選問題

勾選「Populate from range」,然後旁邊按「 + 」(New Range)

在 Select sheet 的頁籤中,

選擇剛剛的回應試算表「蛋糕訂購表單 (回覆)」,按「Select」。

在 Select Ranage 的頁籤中,

Sheet name 選「統計」
Column header 選「顯示」

左側會出現預覽,按下「Next」

最後在 Name range 頁籤中,

Range name 打入一個名稱(名稱辨識用,可隨意填)
然後按下「Save and populate question」

最後的最後,

在 Auto-repopulate questions (自動重新產生問題) 這邊,
記得要把 On form submit (每次表單送出時) 選項給勾起來,
它會在表單送出的時候根據欄位值重新產生表單。

如果資料不同步時,可以手動按下「Update questions list」手動更新表單。

Step7. 發佈連結

表單右上角按下「傳送」,

看到傳送表單的視窗,

選擇「連結」圖示,可以勾「縮短網址」,然後按複製,
這個就是你的表單前台網址。

Step8. 測試!

最後當然是當個客人,來測試一下運作情況啦!

拿剛剛產生的這個網址去瀏覽器真的填看看,也驗證一下會不會有問題,
還是哪裡有做錯的地方。

最後附上筆者隨意填三筆資料的觀察各表單結果。

表單回應

選項拆解

統計

前台表單的樣子

補充

最後的最後,如果你想隱藏額滿的選項,
在額滿提示直接留空白就可以了,選項他會自動消失。

記得時不時注意一下,「選項拆解」試算表套公式的列數,不然會出錯。
這功能不管單選多選都可以適用,選項在對應的地方修改即可,變通一下就有超萬用的表單了。

希望這邊教學對你有幫助。

參考資料

[Android & PHP] 伺服器端 IAP(IAB) 內購通知實作 Real-time Developer Notifications (RTDN)

因為工作需要,研究了這塊,
因為文件有點不連續,又蠻深入的,
左思右想,還是分享出來,給有需要串接 App 內購的朋友,
App 內購在 Android 叫做 In-app Billing (IAB) 在蘋果 iOS 這邊叫做 In-app purchase (IAP),
講得就是一個東西,App 需要購買虛擬商品的時候,必須透過 GooglePlay (Android 平台) 或者蘋果 Apple (iOS 平台) 的金流。

為什麼會需要它?

這邊專注講 Android 平台的 In-app Billing (IAB),

要提到一下 App 內購的流程:

  1. 由使用者按下購買鈕,你的 Android 程式呼叫系統購買頁面
  2. 使用者在系統購買頁面做扣款事宜
    (如果有設定好的話,可能是指紋或者刷臉再次確認,如果第一次使用就會要求你做登入 Google 帳號,輸入信用卡等等的步驟再跳回來)
  3. 你的 Android 程式收到購買狀態(成功/失敗/使用者取消) 並回傳 purchaseToken
  4. 如果購買成功,你的 Android 收到 purchaseToken 之後要 回傳給你的後端伺服器做驗證啟用對應功能給使用者

最後這一步才是關鍵,後端伺服器確認了之後才會開啟對應功能給使用者嘛。
(App 無伺服器的流程是另外一種,而且漏洞很多,先姑且不討論。)

正常流程大致是這樣,但總有一些些例外。

例如:

  • (訂閱型商品常見)每月時間到了系統自動做扣款動作
  • 使用者是在 GooglePlay 商店按的購買而不是在你 App 裡面按
  • Google 那邊刷使用者的卡刷失敗了

等等諸如此類,它並不是發生在你的 App 中,所以你的 App 沒辦法知道那時候訂的商品狀態更新了。

怎麼辦呢?

以前的傳統做法:在 你的 App 啟動時 或者 喚醒 的時候,向 Google 再次確認一次有沒有未處理的購買訂單。

如果訂單不同步怎麼辦?請使用者再次開一次 App 吧!訂單就同步了。

如果你只有一個 Android 平台,
這沒問題,使用者的所有流程都是在你的 App 下進行的,
但如今你的服務可能會有 iOS 平台、 Android 平台、網站…等等的平台。
如果使用者買了之後沒有開 App ,很容易會造成訂單不一致問題。
這個時候就需要內購伺服器通知了。

總結來說,這個 App 內購伺服器通知就是為了解決訂單不同步的問題,
透過伺服器與伺服器對傳通知確認,即時補齊 App 外交易的訂單。

這個 App 內購伺服器通知叫做 Real-time Developer Notifications (RTDN)。

怎麼做?

在使用這個之前,必須先知道 Google 出的 Pub/Sub 模組。
他會透過 Pub/Sub 模組發通知出來,我們可以用 Push (常用) 或者 Pull 方式實作。

Pub/Sub 模組

Pub/Sub 模組它是一個類似廣播模組,如果有需要的話,就設定註冊它,你就會收到來自程式的廣播通知。
一個頻道稱為 Topic
一個訂閱人叫做 Subscription

其中 Subscription 分為 Pull 跟 Push 二種。

  • Pull: 為一個 Queue (佇列) ,幫你 Queue 住所有的訊息直到你打 API 去拉它,
  • Push: 要指定一個 API endpoint 給它,網址就是填你的伺服器 API 位址,他會用文件定義的 API 格式,主動去呼叫你的伺服器。
    (這部分就跟 Apple 概念接近了,這也是最常用的方式。)

官方文件說,一個 app 要獨立一個 Google Cloud project 這點要注意。
在有限額度下,多開 Google Cloud project 帳號應該也無仿。

操作步驟

等等的步驟大致是,
建立一個 Topic 與其對應的 Subscription,然後 Subscription 設定好連接你的伺服器,
設定 app 變更通知要發送到哪一個 Topic。
設定有點小複雜,可能要慢慢研究一下。

到 Google Cloud Console 新增一個 project
(如果有的話就選擇你的 project,假設叫做 my_app_project

Step1. 建立 Topic

到 BigData > Pub/Sub 的頁籤中:
https://console.cloud.google.com/cloudpubsub/topic/list

先建立一個 Topic,
名字自訂,假設叫 my_app_topic

有勾 Add a default subscription 的話,它會自動幫你建立一個 Pull 的 Subscription 叫做 my_app_topic-sub
我們等等會使用 Push 的 Subscription,這個 my_app_topic-sub 的 Pull Subscription 可以先留著,
到時候可以直接在 Cloud Console 這邊直接驗證。

Step2. 測試發送訊息到 Topic

可以先試試看 Topic 發通知跟接收通知的感覺,
在 Pub/Sub > Topic 頁籤中,

點開剛剛建立的 my_app_topic
在 MESSAGES 頁籤,有一個 PUBLISH MESSAGE 按鈕可以按,

按下後看到發送測試訊息的畫面,可以在 Message body 的地方隨意打點字上去,
然後按 publish 發送。

在 Pub/Sub > Subscription 頁籤中,點開自動建立的 my_app_topic-sub

點開 MESSAGES 頁籤,看到列表是空白是正常的,

按下 PULL 更新通知,剛剛發送的訊息就會出現,如果沒有出現,可能要稍等個 1-2分鐘 再按一次(因為它發送跟接收是非同步的)。

Step3. 通知連接你的 app

這邊就是它文件寫的最奇妙的地方了,

在 Pub/Sub > Topic 的頁籤列表中,按下三顆點點 > View premissions 按鈕,

按下 ADD PRINCIPAL 按鈕,

在 New principals 的地方手動輸入 [email protected]
Select a role 的地方手動選擇 Pub/Sub Publisher ,
(這個值是 Google 指定的 Service account 名字,寫在文件裡的,大家都一樣)

然後進入 Google play console,
https://play.google.com/console
並選擇你的 app。

在 Monetization setup 的頁面中,
Real-time developer notifications 區域,
Topic name 的地方, 按照格式 打入剛剛的 Topic 名字:

projects/my_app_project/topics/my_app_topic

(這邊要依照你實際情況來打,每個人不太一樣)

按下 Save changes 然後按下 Send test notification 試試。

Step4. 測試通知發送連接情況

在剛剛 Real-time developer notifications 區域,
按下 Send test notification 試試,
流程跟單獨測試 Topic 類似,
回到剛剛 Pub/Sub > Topic 頁籤中,
在 Pub/Sub > Subscription 頁籤中,點開剛剛的 my_app_topic-sub
按下 PULL 更新通知,會出現測試類似以下的訊息,如果沒有出現,可能要稍等個 1-2分鐘 再按一次(因為它發送跟接收是非同步的)

{"version":"1.0","packageName":"com.myawesome.app","eventTimeMillis":"1638346194077","testNotification":{"version":"1.0"}}

實作伺服器接收端 (PHP)

接收端分成二個方式 Pull 跟 Push 的方式,二個擇一實作即可。
個人比較推薦使用 Push 的方式。

Pull 的方式

以下是 PHP 實作,
如果你不是是用這語言開發,可以參考他其他的 Client library 來實作
https://cloud.google.com/pubsub/docs/reference/libraries#cloud-console

PHP 做法就是使用 composer 引用 cloud-pubsubgoogle/apiclient 套件。

$ composer require google/apiclient
$ composer require google/cloud-pubsub

或者手動在 composer.jsonrequire 區域加上關聯

{
  "config": {
    // ....
  },
  "require": {
    "google/apiclient": "^2.7"
    "google/cloud-pubsub": "^1.34"
  }
}

再執行

$ composer update

你可能需要建立一個 Service account 並給予 Pub/Sub Subscriber 權限
在 Key 的地方按下 Add Key 選擇 json 得到 credentials.json

credentials.json 文件大概長這樣:

{
  "type": "service_account",
  "project_id": "my_app_project",
  "private_key_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBA.......N4M8zLc\n-----END PRIVATE KEY-----\n",
  "client_email": "my-service-account@my_app_project.iam.gserviceaccount.com",
  "client_id": "xxxxxxxxxxxxxxxxxxxxx",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/my-service-account%40my_app_project.iam.gserviceaccount.com"
}

credentials.json 放到伺服器能存取到的地方,假設是放在跟 PHP 相同的目錄
就可以新增一個 PubSubClient 物件來抓取 Pull 的通知

<?php
require __DIR__ . '/vendor/autoload.php';

use Google\Cloud\PubSub\PubSubClient;

$pubSub = new PubSubClient([
    'keyFilePath' => __DIR__ . '/credentials.json',
    'projectId' => 'my_app_project'
]);

$subscription = $pubSub->subscription('my_app_topic-sub');

$messages = $subscription->pull();

foreach ($messages as $message) {
    echo $message->data();
}

這個範例就能跟網頁介面一樣,一次拉一些一次拉一些,拉到你想要的訊息,
可能用一個 cronjob 排程讓它固定跑,就可以使用它。

你就可以得到類似這樣的一筆資料:

{
  "version": "1.0",
  "packageName": "com.myawesome.app",
  "eventTimeMillis": "1638375316338",
  "subscriptionNotification": {
    "version": "1.0",
    "notificationType": 13,
    "purchaseToken": "df................CnPIQ",
    "subscriptionId": "com.myawesome.app.one_month_subscription"
  }
}

這個是一個訂閱通知的範例,
根據查表 notificationType = 13 是 SUBSCRIPTION_EXPIRED 也就是訂閱過期了。

根據文件,除了 subscriptionNotification(訂閱通知)之外,它還有 oneTimeProductNotification(單次購賣通知)
細節就不多講了,格式很接近

{
  "version":"1.0",
  "packageName":"com.myawesome.app",
  "eventTimeMillis":"1638375316338",
  "oneTimeProductNotification":
  {
    "version":"1.0",
    "notificationType":1,
    "purchaseToken":"fg................HBbID",
    "sku":"com.myawesome.app.coin"
  }
}

單次購買的 notificationType 與訂閱的 notificationType 定義不同,需注意。

Push 的方式(常用)

你要設計一個 API endpoint 給他,每當訂單狀態有變更時,Google 伺服器它會用 POST 把資料回傳給你

這邊分成二部分,有認證跟沒認證,先解釋乾淨版不需認證的方式。
(這邊的認證是指認證是否真的由 Google 伺服器來的通知,而不是別人假冒的)

格式參考如下:
https://developer.android.com/google/play/billing/rtdn-reference

接收的文件路徑:
https://cloud.google.com/pubsub/docs/push

你會收到來自 Google 伺服器經由 POST 打來的資料,類似這樣:

{
  "message": {
    "attributes": {
      "key": "value"
    },
    "data": "eyAidmVx................GlvbiB9",
    "messageId": "136969346945"
  },
  "subscription": "projects/my_app_project/subscriptions/mysubscription"
}

其中這個 data 欄位是一個 base64 編碼後的字串,解碼 data 欄位可以得到類似以下的資料:

{
  "version": "1.0",
  "packageName": "com.myawesome.app",
  "eventTimeMillis": "1638375316338",
  "subscriptionNotification": {
    "version": "1.0",
    "notificationType": 13,
    "purchaseToken": "df................CnPIQ",
    "subscriptionId": "com.myawesome.app.one_month_subscription"
  }
}

這個是一個訂閱通知的範例,
根據查表 notificationType = 13 是 SUBSCRIPTION_EXPIRED 也就是訂閱過期了

根據文件,除了 subscriptionNotification(訂閱通知)之外,它還有 oneTimeProductNotification(單次購賣通知)
細節就不多講了,格式很接近

{
  "version":"1.0",
  "packageName":"com.myawesome.app",
  "eventTimeMillis":"1638375316338",
  "oneTimeProductNotification":
  {
    "version":"1.0",
    "notificationType":1,
    "purchaseToken":"fg................HBbID",
    "sku":"com.myawesome.app.coin"
  }
}

單次購買的 notificationType 與訂閱的 notificationType 定義不同,需注意。

查找訂單細節(訂閱型資料)

伺服器訂購通知只給你很粗糙的內容,實際內容還需要你另外打 API 達成。

PHP 需 composer 引用 apiclient 套件

$ composer require google/apiclient

或者手動在 composer.jsonrequire 區域加上關聯

{
  "config": {
    // ....
  },
  "require": {
    "google/apiclient": "^2.7"
  }
}

再執行

$ composer update

以下程式碼是一個抓取訂閱訂單的範例:
需要填入 $packageName, $subscriptionId$purchaseToken

$packageName = 'com.myawesome.app';
$subscriptionId = 'com.myawesome.app.one_month_subscription';
$purchaseToken = 'df................CnPIQ';

$client = new \Google_Client();
try {
    $client->setAuthConfig(dirname(__FILE__) . '/credentials.json');
} catch (\Google\Exception $e) {
    throw $e;
}
$client->addScope('https://www.googleapis.com/auth/androidpublisher');
$service = new \Google_Service_AndroidPublisher($client);
$purchase = $service->purchases_subscriptions->get($packageName, $subscriptionId, $purchaseToken);

var_dump($purchase);

大致的資料格式如下(引用官方文件):

{
  "kind": string,
  "startTimeMillis": string,
  "expiryTimeMillis": string,
  "autoResumeTimeMillis": string,
  "autoRenewing": boolean,
  "priceCurrencyCode": string,
  "priceAmountMicros": string,
  "introductoryPriceInfo": {
    object (IntroductoryPriceInfo)
  },
  "countryCode": string,
  "developerPayload": string,
  "paymentState": integer,
  "cancelReason": integer,
  "userCancellationTimeMillis": string,
  "cancelSurveyResult": {
    object (SubscriptionCancelSurveyResult)
  },
  "orderId": string,
  "linkedPurchaseToken": string,
  "purchaseType": integer,
  "priceChange": {
    object (SubscriptionPriceChange)
  },
  "profileName": string,
  "emailAddress": string,
  "givenName": string,
  "familyName": string,
  "profileId": string,
  "acknowledgementState": integer,
  "externalAccountId": string,
  "promotionType": integer,
  "promotionCode": string,
  "obfuscatedExternalAccountId": string,
  "obfuscatedExternalProfileId": string
}

REST 文件在這:
https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.subscriptions
PHP 的資料格式參考(很難看懂,還是參照 REST 文件吧!):
https://developers.google.com/resources/api-libraries/documentation/androidpublisher/v3/php/latest/class-Google_Service_AndroidPublisher_SubscriptionPurchase.html

基本上對應的資料格式會有對應的 Method 可以呼叫。

查找訂單細節(單次購買型資料)

伺服器訂購通知只給你很粗糙的內容,實際內容還需要你另外打 API 達成。

PHP 需 composer 引用 apiclient 套件

$ composer require google/apiclient

或者手動在 composer.jsonrequire 區域加上關聯

{
  "config": {
    // ....
  },
  "require": {
    "google/apiclient": "^2.7"
  }
}

再執行

$ composer update

以下程式碼是一個抓取訂閱訂單的範例:
需要填入 $packageName, $sku$purchaseToken

$packageName = 'com.myawesome.app';
$sku = 'com.myawesome.app.coin';
$purchaseToken = 'fg................HBbID';

$client = new \Google_Client();
try {
    $client->setAuthConfig(dirname(__FILE__) . '/credentials.json');
} catch (\Google\Exception $e) {
    throw $e;
}
$client->addScope('https://www.googleapis.com/auth/androidpublisher');
$service = new \Google_Service_AndroidPublisher($client);
$purchase = $service->purchases_products->get($packageName, $sku, $purchaseToken);

var_dump($purchase);

大致的資料格式如下(引用官方文件):

{
  "kind": string,
  "purchaseTimeMillis": string,
  "purchaseState": integer,
  "consumptionState": integer,
  "developerPayload": string,
  "orderId": string,
  "purchaseType": integer,
  "acknowledgementState": integer,
  "purchaseToken": string,
  "productId": string,
  "quantity": integer,
  "obfuscatedExternalAccountId": string,
  "obfuscatedExternalProfileId": string,
  "regionCode": string
}

REST 文件在這:
https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.products
PHP 的資料格式參考(很難看懂,還是參照 REST 文件吧!):
https://developers.google.com/resources/api-libraries/documentation/androidpublisher/v3/php/latest/class-Google_Service_AndroidPublisher_ProductPurchase.html

基本上對應的資料格式會有對應的 Method 可以呼叫。

回應 (Acknowledge) 訂單已處理(訂閱型)

有一個很重要的就是,伺服器處理完訂單要打 API 跟 Google 伺服器講:商品已送達。
這個行為稱為 Acknowledge

不然它會認為沒有開對應功能給使用者,計時三天後,自動做退費,所以這功能很重要。

$packageName = 'com.myawesome.app';
$gpaOrderId = 'GPA.xxxx-xxxx-xxxx-xxxxx';
$purchaseToken = 'df................CnPIQ';

try {
  $client = new \Google_Client();
  $client->setAuthConfig(dirname(__FILE__) . '/credentials.json');
  $client->addScope('https://www.googleapis.com/auth/androidpublisher'); 
  $service = new \Google_Service_AndroidPublisher($client);

  $postBody = new Google_Service_AndroidPublisher_SubscriptionPurchasesAcknowledgeRequest();
  $postBody->setDeveloperPayload("");
  $response = $service->purchases_subscriptions->acknowledge($packageName, $gpaOrderId, $purchaseToken, $postBody);

  var_dump($response);
} catch (\Google\Exception $e) {
    throw $e;
}

以上是一個簡單範例。

回應 (Acknowledge) 訂單已處理(單次購買型)

伺服器處理完訂單要打 API 跟 Google 伺服器講:商品已送達。
這個行為稱為 Acknowledge

不然它會認為沒有開對應功能給使用者,計時三天後,自動做退費,所以這功能很重要。
原理類似,就不贅述了。

$packageName = 'com.myawesome.app';
$sku = 'com.myawesome.app.coin';
$purchaseToken = 'fg................HBbID';

try {
  $client = new \Google_Client();
  $client->setAuthConfig(dirname(__FILE__) . '/credentials.json');
  $client->addScope('https://www.googleapis.com/auth/androidpublisher'); 
  $service = new \Google_Service_AndroidPublisher($client);

  $postBody = new Google_Service_AndroidPublisher_ProductPurchasesAcknowledgeRequest();
  $postBody->setDeveloperPayload("");
  $response = $service->purchases_products->acknowledge($packageName, $sku, $purchaseToken, $postBody);

  var_dump($response);
} catch (\Google\Exception $e) {
    throw $e;
}

以上就是各種實作細節,細節很多,文件很長,可能要多看幾次。
祝串接愉快。

參考資料

Spring boot 學習筆記 – 使用 Kotlin 做後端開發

學了這麼厲害的 Kotlin 可不可以拿它來開發後端(Backend),答案是當然可以的!
我們使用 Kotlin 跟 Gradle 來做開發
先做一個非常簡單的 API 再來慢慢進階

前端?後端?前台?後台?有差嗎?

在這邊簡單解釋一下 Web 技術這些詞彙的定義,大家也常常搞錯。

  • 前端 (Frontend):由後端 (Backend) 收到資料,透過瀏覽器渲染出來跟使用者互動的介面 (UI) 與程式都稱前端。

  • 後端 (Backend):也就是伺服器 (Server) 的部分,專門處理由使用者的要求,提供對應資料的程式。

  • 前台:大家比較通俗的講法,使用者看得到的操作得到的區域,如同舞台一般。

  • 後台:大家比較通俗的講法,通常是指管理者介面,如同舞台的後台。

那前台、後台跟前端、後端有關係嗎?
答案是:沒有什麼關係。
前台需要有前端(介面),也需要後端(伺服器程式)幫忙,後台也是。

工作上可以細分成:

  • 前台的前端
  • 前台的後端
  • 後台的前端
  • 後台的後端

分別有前端工程師與後端工程師負責開發。
今天的主題是後端開發的主題,所以沒有介面只有資料。

學習目標

  • 手把手製作一個 REST API
  • 熟悉 Spring boot 的 Annotation 寫法
  • 調整 404 頁面

準備開發環境

這邊有三種做法,好壞都不一

  • 直接推薦:InteliJ IDEA Ultimate 付費版
  • Visual studio code 外掛 Spring Initializr plug-ins
  • Spring Tools 4 for Eclipse (前身為 STS3)

💡 小提醒:這個需要付費版 InteliJ IDEA Ultimate, 免費的 Community 會無法正確 Compile,小弟已經親身試過了。

小弟建議使用 InteliJ,因為他的優異程式碼提示、搜尋與重構等功能好過其他的 IDE 編輯器

小提醒:因為 InteliJ IDEA 有可能會小改版,介面可能會稍微長的不太一樣,如果真有找不到選項、或者文章失效歡迎聯繫小弟我。

開新專案

新增SpringBoot專案

New Project,選擇 Spring Initializr

  • Group: <自己填>
  • Artifact: <自己填>
  • Type: Gradle Project (Generate a Gradle based project archive.)
  • Language: Kotlin
  • Packaging: Jar
  • Java Version: 11 (選更新的版本也可以)
  • Version: <自己填>
  • Name: <自己填>
  • Description: <自己填>
  • Package: <自己填>

然後按「Next」。

新增SpringBoot專案

這邊選擇二個:

  • Web > Spring Web
  • Developer Tools > Spring Boot DevTools

不選也沒關係,後面可以自行新增。
按下「Finish」完成建立專案。

如果直接新增完跑的話,Log 大概會長這樣。

瀏覽會長這樣:

資料夾結構

如果都是預設值的話,會看到以下資料夾結構:

  • src/main/kotlin/\<packageName>/
    主要程式碼的目錄
  • src/test/kotlin/\<packageName>/
    測試程式碼的目錄
  • build.gradle.kts
    專案的 gradle 檔案
  • src/main/resources/static
    存放一些靜態資源(例如:JavaScript、css、圖片…等)
  • src/main/resources/templates
    存放一些樣板檔案
  • src/main/resources/application.properties
    參數設定配置檔,存放一些資料連線資訊、雜項設定等等

修改啟動的連接埠 (Port)

找到 application.properties 檔案,加入這句即可

server.port=8086

啟動 Port 就改成 8086 了

其他參數請見:
https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html

增加 Dependencies

打開 build.gradle.kts 找到 dependencies { } 的區塊

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    developmentOnly("org.springframework.boot:spring-boot-devtools")
}

即是 Spring Web 跟 Spring Boot DevTools


觀察檔案

DemoApplication.kt (如果沒改名字的話)
或者有 @SpringBootApplication 標明字樣的檔案
即主要程式,程式的進入點
可以找到一個

fun main(args: Array<String>) {
    runApplication<DemoApplication>(*args)
}

可以找到 main 即程式進入點


寫第一個 Controller 寫一隻 API

著手來寫第一個 Controller

定義資料 Model

我們先定義幾個的 model class,

GeneralMessageResponse 是用來顯示範例 API 資料用的

GeneralMessageResponse.kt

package com.example.demo.model

data class GeneralMessageResponse(val message:String)

就這樣二句,就可以定義好資料 Model。

GeneralErrorResponse 用來顯示錯誤訊息的

GeneralErrorResponse.kt

package com.example.demo.model

data class GeneralErrorResponse(var message:String)

寫一個 Controller

製作一個 class 名叫 HelloController 使用 @RestController 標示,內容如下:

HelloController.kt

package com.example.demo

import com.example.demo.model.GeneralMessageResponse
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class HelloController{

    @RequestMapping("/")
    fun hello(): GeneralMessageResponse {
        return GeneralMessageResponse("Hello, world!")
    }
}

做一個方法 (Method),方法名稱可以自訂,標上 @RequestMapping 標示,回傳一個物件
即回傳的資料

@RequestMapping 上面需標明綁定的 API 路徑(不可重複)
可綁定何種傳送方式 (GET, POST, PUT, DELETE)

可編譯此專案,並瀏覽之
預設會是 http://localhost:8080/

會得到以下結果:

{"message":"Hello, world!"}

新增另外一個 Method

在剛剛的 HelloController 在新增一個方法 (Method),名叫 getHello()
程式碼片段如下:

@RequestMapping("/hello", method = [RequestMethod.GET])
fun getHello(@RequestParam("name", defaultValue = "World") name: String): GeneralMessageResponse {
    return GeneralMessageResponse("Hello, $name")
}

比起剛剛的 Method 多了參數,只接受 GET 方法(使用其他方式存取會錯誤,因為沒定義)
不標明等於接收所有的方式

參數部分使用 @RequestParam 修飾字標明,標上參數名稱,
defaultValue 如果沒填資料的話的預設值為何
然後像剛才一樣,回傳一個 JSON 物件

方式+名稱 的組合不可重複,但可接受同名但不同的方式的 Method
例如這樣是合法的:

@RequestMapping("/hello")
fun hello(): GeneralMessageResponse {
    return GeneralMessageResponse("Hello, world (from default method)!")
}

@RequestMapping("/hello", method = [RequestMethod.GET])
fun getHello(@RequestParam("name", defaultValue = "World") name: String): GeneralMessageResponse {
    return GeneralMessageResponse("Hello, $name (from get)")
}

@RequestMapping("/hello", method = [RequestMethod.POST])
fun postHello(@RequestParam("name", defaultValue = "World") name: String): GeneralMessageResponse {
    return GeneralMessageResponse("Hello, $name (from post)")
}

後面 Restful API 的時候會再提到。


錯誤處理

錯誤處理有二個部分,不太一樣:

  • 執行時出現 Exception 時,可自訂回傳的錯誤訊息
  • 當存取一個不存在的 API 時,預設回的訊息( 404 Not found 頁面)

處理 Exception 錯誤

先新增一個 GeneralErrorResponse 做為錯誤訊息的回應

GeneralErrorResponse.kt

package com.example.demo.model

import com.fasterxml.jackson.annotation.JsonInclude

@JsonInclude(JsonInclude.Include.NON_NULL)
data class GeneralErrorResponse(
    val message:String, 
    val requestUrl: String? = null, 
    val errorStackStace: String? = null
)

新增一個 ThrowableExtension.kt 新增一個 stackTraceAsString() 拿到 stackTrace 的字串

ThrowableExtension.kt

package com.example.demo.utils

import java.io.PrintWriter
import java.io.StringWriter

fun Throwable.stackTraceAsString(): String {
    val errors = StringWriter()
    this.printStackTrace(PrintWriter(errors))
    return errors.toString()
}

最後新增 GlobalExceptionHandler 用來處理全域的 API 錯誤問題

GlobalExceptionHandler.kt

package com.example.demo

import com.example.demo.model.GeneralErrorResponse
import com.example.demo.utils.stackTraceAsString
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.ResponseBody
import org.springframework.web.bind.annotation.ResponseStatus
import javax.servlet.http.HttpServletRequest

@ControllerAdvice
class GlobalExceptionHandler {
    @ExceptionHandler(Throwable::class)
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    @ResponseBody
    fun handleException(request: HttpServletRequest, error: Exception): GeneralErrorResponse {
        return GeneralErrorResponse("Error occurred!", request.requestURL.toString(), error.stackTraceAsString())
    }
}

這邊使用了 @ControllerAdvice@ExceptionHandler 標示
當然不同情境有不同的用法,這邊示範回傳一個 JSON

最後在 Controller 新增一個測試用的 Exception 方法

@RequestMapping("/testErr")
fun testErr() {
    throw Exception("test exception")
}

然後瀏覽測試之。

處理 404 Not found 頁面

這裡處理當存取一個不存在的 API 時,預設回的資料。
我們再新增一個 Controller 名叫 MyErrorController 實作 ErrorController 介面,
範例如下:

MyErrorController.kt

package com.example.demo

import com.example.demo.model.GeneralErrorResponse
import org.springframework.boot.web.servlet.error.ErrorController
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class MyErrorController : ErrorController {
    override fun getErrorPath(): String {
        return "/error"
    }

    @RequestMapping("/error")
    fun handleError(): GeneralErrorResponse {
        return GeneralErrorResponse("404 Error")
    }
}

這裡需實作 ErrorController 介面,定義錯誤發生的預設 Path 將走去哪裡
這裡使用 "/error"
然後綁定前述的值,回應一個預設訊息。


參考資料