diff --git a/LINTING.md b/LINTING.md new file mode 100644 index 0000000..fe4debe --- /dev/null +++ b/LINTING.md @@ -0,0 +1,236 @@ +# Go Linting 配置說明 + +本項目使用現代化的 Go linting 工具來確保代碼質量和風格一致性。 + +## 工具介紹 + +### golangci-lint +- **現代化的 Go linter 聚合工具**,整合了多個 linter +- 比傳統的 `golint` 更快、更全面 +- 支持並行執行和緩存 +- 配置文件:`.golangci.yml` + +## 安裝 + +### 安裝 golangci-lint + +```bash +# macOS +brew install golangci-lint + +# Linux +curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.55.2 + +# Windows +go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2 +``` + +### 安裝其他工具 + +```bash +# 格式化工具 +go install mvdan.cc/gofumpt@latest +go install golang.org/x/tools/cmd/goimports@latest +``` + +## 使用方法 + +### Makefile 命令 + +```bash +# 基本代碼檢查 +make lint + +# 自動修復可修復的問題 +make lint-fix + +# 詳細輸出 +make lint-verbose + +# 只檢查新問題(與 main 分支比較) +make lint-new + +# 格式化代碼 +make fmt +``` + +### 直接使用 golangci-lint + +```bash +# 基本檢查 +golangci-lint run + +# 自動修復 +golangci-lint run --fix + +# 檢查特定目錄 +golangci-lint run ./pkg/... + +# 詳細輸出 +golangci-lint run -v + +# 只顯示新問題 +golangci-lint run --new-from-rev=main +``` + +## 配置說明 + +### 啟用的 Linters + +我們的配置啟用了以下 linter 類別: + +#### 核心檢查 +- `errcheck`: 檢查未處理的錯誤 +- `gosimple`: 簡化代碼建議 +- `govet`: 檢查常見錯誤 +- `staticcheck`: 靜態分析 +- `typecheck`: 類型檢查 +- `unused`: 檢查未使用的變量和函數 + +#### 代碼質量 +- `cyclop`: 循環複雜度檢查 +- `dupl`: 代碼重複檢測 +- `funlen`: 函數長度檢查 +- `gocognit`: 認知複雜度檢查 +- `gocyclo`: 循環複雜度檢查 +- `nestif`: 嵌套深度檢查 + +#### 格式化 +- `gofmt`: 格式化檢查 +- `gofumpt`: 更嚴格的格式化 +- `goimports`: 導入排序 + +#### 命名和風格 +- `goconst`: 常量檢查 +- `gocritic`: 代碼評論 +- `gomnd`: 魔術數字檢查 +- `stylecheck`: 風格檢查 +- `varnamelen`: 變量名長度檢查 + +#### 安全 +- `gosec`: 安全檢查 + +#### 錯誤處理 +- `errorlint`: 錯誤處理檢查 +- `nilerr`: nil 錯誤檢查 +- `wrapcheck`: 錯誤包裝檢查 + +### 配置文件結構 + +```yaml +# .golangci.yml +run: + timeout: 5m + skip-dirs: [vendor, .git, bin, build, dist, tmp] + skip-files: [".*\\.pb\\.go$", ".*\\.gen\\.go$"] + +linters: + disable-all: true + enable: [errcheck, gosimple, govet, ...] + +linters-settings: + # 各個 linter 的詳細配置 + +issues: + # 問題排除規則 + exclude-rules: + - path: _test\.go + linters: [gomnd, funlen, dupl] +``` + +## IDE 整合 + +### VS Code +項目包含 `.vscode/settings.json` 配置: +- 自動使用 golangci-lint 進行檢查 +- 保存時自動格式化 +- 使用 gofumpt 作為格式化工具 + +### GoLand/IntelliJ +1. 安裝 golangci-lint 插件 +2. 在設置中指向項目的 `.golangci.yml` 文件 + +## CI/CD 整合 + +### GitHub Actions +項目包含 `.github/workflows/ci.yml`: +- 自動運行測試 +- 執行 golangci-lint 檢查 +- 安全掃描 +- 依賴檢查 + +### 本地 Git Hooks +可以設置 pre-commit hook: + +```bash +#!/bin/sh +# .git/hooks/pre-commit +make lint +``` + +## 常見問題 + +### 1. 如何忽略特定的檢查? + +在代碼中使用註釋: +```go +//nolint:gosec // 忽略安全檢查 +password := "hardcoded" + +//nolint:lll // 忽略行長度檢查 +url := "https://very-long-url-that-exceeds-line-length-limit.com/api/v1/endpoint" +``` + +### 2. 如何為測試文件設置不同的規則? + +配置文件中已經為測試文件設置了特殊規則: +```yaml +exclude-rules: + - path: _test\.go + linters: [gomnd, funlen, dupl, lll, goconst] +``` + +### 3. 如何調整複雜度閾值? + +在 `.golangci.yml` 中調整: +```yaml +linters-settings: + cyclop: + max-complexity: 15 # 調整循環複雜度 + funlen: + lines: 100 # 調整函數行數限制 + statements: 50 # 調整語句數限制 +``` + +### 4. 性能優化 + +- 使用緩存:`golangci-lint cache clean` 清理緩存 +- 只檢查修改的文件:`--new-from-rev=main` +- 並行執行:默認已啟用 + +## 升級和維護 + +定期更新 golangci-lint: +```bash +# 檢查版本 +golangci-lint version + +# 升級到最新版本 +brew upgrade golangci-lint # macOS +# 或重新下載安裝腳本 +``` + +定期檢查配置文件的新選項和 linter: +```bash +# 查看所有可用的 linter +golangci-lint linters + +# 查看配置幫助 +golangci-lint config -h +``` + +## 參考資源 + +- [golangci-lint 官方文檔](https://golangci-lint.run/) +- [Go 代碼風格指南](https://github.com/golang/go/wiki/CodeReviewComments) +- [Effective Go](https://golang.org/doc/effective_go.html) diff --git a/Makefile b/Makefile index 9e10a11..50739a8 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,6 @@ mock-gen: # 建立 mock 資料 fmt: # 格式優化 $(GOFMT) -w $(GOFILES) goimports -w ./ - golangci-lint run .PHONY: build build: # 編譯專案 @@ -63,17 +62,13 @@ install: # 安裝依賴 go mod tidy go mod download -.PHONY: lint -lint: # 代碼檢查 - golangci-lint run - .PHONY: help help: # 顯示幫助信息 @echo "Available commands:" @echo " test - 運行測試" @echo " gen-api - 產生 api" - @echo " gen-swagger - 生成 JSON 格式 Swagger 文檔" - @echo " gen-swagger-yaml - 生成 YAML 格式 Swagger 文檔" + @echo " gen-doc - 生成 Swagger 文檔" + @echo " mock-gen - 建立 mock 資料" @echo " fmt - 格式化代碼" @echo " build - 編譯專案" @echo " run - 運行專案" @@ -81,6 +76,5 @@ help: # 顯示幫助信息 @echo " docker-build - 構建 Docker 映像" @echo " docker-run - 運行 Docker 容器" @echo " install - 安裝依賴" - @echo " lint - 代碼檢查" @echo " help - 顯示幫助信息" diff --git a/bin/gateway b/bin/gateway new file mode 100755 index 0000000..4e49254 Binary files /dev/null and b/bin/gateway differ diff --git a/deployment/docker-compose.yaml b/deployment/docker-compose.yaml new file mode 100644 index 0000000..ba1bc9d --- /dev/null +++ b/deployment/docker-compose.yaml @@ -0,0 +1,31 @@ +services: + mongo: + image: mongo:8.0 + container_name: mongo + restart: always + ports: + - "27017:27017" + environment: + MONGO_INITDB_ROOT_USERNAME: root + MONGO_INITDB_ROOT_PASSWORD: example + + etcd: + image: quay.io/coreos/etcd:v3.5.5 + container_name: etcd + restart: always + command: > + /usr/local/bin/etcd + --data-dir=/etcd-data + --name=etcd + --listen-client-urls=http://0.0.0.0:2379 + --advertise-client-urls=http://etcd:2379 + ports: + - "2379:2379" + - "2380:2380" + + redis: + image: redis:7.0 + container_name: redis + restart: always + ports: + - "6379:6379" \ No newline at end of file diff --git a/etc/gateway.yaml b/etc/gateway.yaml index 70d230c..65c48b3 100644 --- a/etc/gateway.yaml +++ b/etc/gateway.yaml @@ -1,3 +1,44 @@ Name: gateway Host: 0.0.0.0 Port: 8888 + +Cache: + - Host: 127.0.0.1:6379 + type: node +CacheExpireTime: 1s +CacheWithNotFoundExpiry: 1s + +RedisConf: + Host: 127.0.0.1:6379 + Type: node + Pass: "" + Tls: false + +Mongo: + Schema: mongodb + Host: "127.0.0.1:27017" + User: "root" + Password: "example" + Port: "" + Database: digimon_member + ReplicaName: "rs0" + MaxStaleness: 30m + MaxPoolSize: 30 + MinPoolSize: 10 + MaxConnIdleTime: 30m + Compressors: + - f + EnableStandardReadWriteSplitMode: true + ConnectTimeoutMs : 300 + +Bcrypt: + Cost: 10 + +GoogleAuth: + ClientID: xxx.apps.googleusercontent.com + AuthURL: x + +LineAuth: + ClientID : "200000000" + ClientSecret : xxxxx + RedirectURI : http://localhost:8080/line.html \ No newline at end of file diff --git a/gateway.json b/gateway.json deleted file mode 100644 index cd17ef3..0000000 --- a/gateway.json +++ /dev/null @@ -1,2931 +0,0 @@ -{ - "info": { - "contact": { - "email": "igs170911@gmail.com", - "name": "Daniel Wang" - }, - "description": "This is Digimon Platform ", - "title": "Digimon Platform API Gateway", - "version": "v1" - }, - "openapi": "3.0.3", - "paths": { - "/api/v1/account/bind": { - "post": { - "description": "將帳號綁定到指定的用戶 UID", - "operationId": "accountBindAccount", - "requestBody": { - "content": { - "application/json": { - "schema": { - "properties": { - "login_id": { - "type": "string" - }, - "type": { - "type": "integer" - }, - "uid": { - "type": "string" - } - }, - "required": [ - "uid", - "login_id", - "type" - ], - "type": "object" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": { - "login_id": { - "type": "string" - }, - "type": { - "type": "integer" - }, - "uid": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 綁定成功" - }, - "400": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - } - ] - } - } - }, - "description": "客戶端錯誤\n\nPossible errors:\n40004: ErrorResp - 參數驗證失敗\n40005: ErrorResp - UID 格式錯誤\n40001: ErrorResp - 帳號格式錯誤" - }, - "404": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 帳號不存在" - }, - "409": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 帳號已綁定" - }, - "500": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 服務器錯誤" - } - }, - "summary": "綁定帳號" - } - }, - "/api/v1/account/bind-email": { - "post": { - "description": "綁定並驗證用戶的 Email 地址", - "operationId": "accountBindVerifyEmail", - "requestBody": { - "content": { - "application/json": { - "schema": { - "properties": { - "email": { - "type": "string" - }, - "uid": { - "type": "string" - } - }, - "required": [ - "uid", - "email" - ], - "type": "object" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "data": {}, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 綁定成功" - }, - "400": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - } - ] - } - } - }, - "description": "客戶端錯誤\n\nPossible errors:\n40005: ErrorResp - UID 格式錯誤\n40006: ErrorResp - 郵箱格式錯誤" - }, - "404": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 用戶不存在" - }, - "409": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 郵箱已綁定" - }, - "422": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 郵箱未驗證" - }, - "500": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 服務器錯誤" - } - }, - "summary": "綁定驗證 Email" - } - }, - "/api/v1/account/bind-info": { - "post": { - "description": "初次綁定用戶的詳細資料", - "operationId": "accountBindUserInfo", - "requestBody": { - "content": { - "application/json": { - "schema": { - "properties": { - "address": { - "type": "string" - }, - "alarm_type": { - "type": "integer" - }, - "avatar": { - "type": "string" - }, - "birthdate": { - "type": "integer" - }, - "currency": { - "type": "string" - }, - "email": { - "type": "string" - }, - "full_name": { - "type": "string" - }, - "gender": { - "type": "integer" - }, - "language": { - "type": "string" - }, - "nick_name": { - "type": "string" - }, - "phone_number": { - "type": "string" - }, - "status": { - "type": "integer" - }, - "uid": { - "type": "string" - } - }, - "required": [ - "uid", - "alarm_type", - "status", - "language", - "currency", - "avatar", - "nick_name", - "full_name", - "gender", - "birthdate", - "phone_number", - "email", - "address" - ], - "type": "object" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "data": {}, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 綁定成功" - }, - "400": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - } - ] - } - } - }, - "description": "客戶端錯誤\n\nPossible errors:\n40004: ErrorResp - 參數驗證失敗\n40005: ErrorResp - UID 格式錯誤\n40006: ErrorResp - 郵箱格式錯誤\n40007: ErrorResp - 電話格式錯誤" - }, - "404": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 用戶不存在" - }, - "409": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 用戶資料已存在" - }, - "422": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 用戶資料不完整" - }, - "500": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 服務器錯誤" - } - }, - "summary": "綁定用戶資料" - } - }, - "/api/v1/account/bind-phone": { - "post": { - "description": "綁定並驗證用戶的電話號碼", - "operationId": "accountBindVerifyPhone", - "requestBody": { - "content": { - "application/json": { - "schema": { - "properties": { - "phone": { - "type": "string" - }, - "uid": { - "type": "string" - } - }, - "required": [ - "uid", - "phone" - ], - "type": "object" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "data": {}, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 綁定成功" - }, - "400": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - } - ] - } - } - }, - "description": "客戶端錯誤\n\nPossible errors:\n40005: ErrorResp - UID 格式錯誤\n40007: ErrorResp - 電話格式錯誤" - }, - "404": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 用戶不存在" - }, - "409": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 電話已綁定" - }, - "422": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 電話未驗證" - }, - "500": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 服務器錯誤" - } - }, - "summary": "綁定驗證 Phone" - } - }, - "/api/v1/account/check-code": { - "post": { - "description": "檢查驗證碼是否正確,但不刪除驗證碼", - "operationId": "accountCheckRefreshCode", - "requestBody": { - "content": { - "application/json": { - "schema": { - "properties": { - "account": { - "type": "string" - }, - "code_type": { - "type": "integer" - }, - "verify_code": { - "type": "string" - } - }, - "required": [ - "account", - "code_type", - "verify_code" - ], - "type": "object" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "data": {}, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 檢查成功" - }, - "400": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - } - ] - } - } - }, - "description": "客戶端錯誤\n\nPossible errors:\n40010: ErrorResp - 驗證碼類型無效\n40011: ErrorResp - 驗證碼格式錯誤\n40001: ErrorResp - 帳號格式錯誤\n40004: ErrorResp - 參數驗證失敗" - }, - "404": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 驗證碼不存在" - }, - "422": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 驗證碼無效或已過期" - }, - "500": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 服務器錯誤" - } - }, - "summary": "檢查驗證碼" - } - }, - "/api/v1/account/create": { - "post": { - "description": "創建新的用戶帳號,支援多平台登入", - "operationId": "accountCreateUserAccount", - "requestBody": { - "content": { - "application/json": { - "schema": { - "properties": { - "login_id": { - "type": "string" - }, - "platform": { - "type": "integer" - }, - "token": { - "type": "string" - } - }, - "required": [ - "login_id", - "platform", - "token" - ], - "type": "object" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "data": {}, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 創建成功" - }, - "400": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - } - ] - } - } - }, - "description": "客戶端錯誤\n\nPossible errors:\n40001: ErrorResp - 帳號格式錯誤\n40002: ErrorResp - 密碼強度不足\n40003: ErrorResp - 平台類型無效" - }, - "409": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 帳號已存在" - }, - "422": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 請求格式正確但語義錯誤" - }, - "500": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 服務器錯誤" - } - }, - "summary": "創建用戶帳號" - } - }, - "/api/v1/account/generate-code": { - "post": { - "description": "為指定帳號生成驗證碼,用於忘記密碼等功能", - "operationId": "accountGenerateRefreshCode", - "requestBody": { - "content": { - "application/json": { - "schema": { - "properties": { - "account": { - "type": "string" - }, - "code_type": { - "type": "integer" - } - }, - "required": [ - "account", - "code_type" - ], - "type": "object" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": { - "data": { - "properties": { - "verify_code": { - "type": "string" - } - }, - "required": [ - "verify_code" - ], - "type": "object" - } - }, - "type": "object" - } - } - }, - "description": "// 生成成功" - }, - "400": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - } - ] - } - } - }, - "description": "客戶端錯誤\n\nPossible errors:\n40001: ErrorResp - 帳號格式錯誤\n40004: ErrorResp - 參數驗證失敗\n40010: ErrorResp - 驗證碼類型無效" - }, - "404": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 帳號不存在" - }, - "429": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 請求過於頻繁" - }, - "500": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 服務器錯誤" - } - }, - "summary": "生成驗證碼" - } - }, - "/api/v1/account/info": { - "post": { - "description": "根據帳號獲取用戶的帳號資訊", - "operationId": "accountGetUserAccountInfo", - "requestBody": { - "content": { - "application/json": { - "schema": { - "properties": { - "account": { - "type": "string" - } - }, - "required": [ - "account" - ], - "type": "object" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": { - "data": { - "properties": { - "login_id": { - "type": "string" - }, - "platform": { - "type": "integer" - }, - "token": { - "type": "string" - } - }, - "required": [ - "login_id", - "platform", - "token" - ], - "type": "object" - } - }, - "type": "object" - } - } - }, - "description": "// 獲取成功" - }, - "400": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - } - ] - } - } - }, - "description": "客戶端錯誤\n\nPossible errors:\n40001: ErrorResp - 帳號格式錯誤\n40004: ErrorResp - 參數驗證失敗" - }, - "404": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 帳號不存在" - }, - "500": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 服務器錯誤" - } - }, - "summary": "獲取帳號資訊" - } - }, - "/api/v1/account/line/profile": { - "post": { - "description": "使用 LINE Access Token 獲取用戶資料", - "operationId": "accountLineGetProfileByAccessToken", - "requestBody": { - "content": { - "application/json": { - "schema": { - "properties": { - "token": { - "type": "string" - } - }, - "required": [ - "token" - ], - "type": "object" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": { - "display_name": { - "type": "string" - }, - "picture_url": { - "type": "string" - }, - "status_message": { - "type": "string" - }, - "user_id": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 獲取成功" - }, - "400": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - } - ] - } - } - }, - "description": "客戶端錯誤\n\nPossible errors:\n40004: ErrorResp - 參數驗證失敗\n40012: ErrorResp - Token 格式錯誤" - }, - "401": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// Token 無效或過期" - }, - "422": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// LINE 用戶資料獲取失敗" - }, - "500": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 服務器錯誤" - } - }, - "summary": "LINE 獲取用戶資料" - } - }, - "/api/v1/account/line/token": { - "post": { - "description": "使用 LINE 授權碼獲取 Access Token", - "operationId": "accountLineCodeToAccessToken", - "requestBody": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "string" - } - }, - "required": [ - "code" - ], - "type": "object" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": { - "token": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 獲取成功" - }, - "400": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - } - ] - } - } - }, - "description": "客戶端錯誤\n\nPossible errors:\n40004: ErrorResp - 參數驗證失敗\n40013: ErrorResp - 授權碼格式錯誤" - }, - "401": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 授權碼無效或過期" - }, - "422": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// LINE 認證失敗" - }, - "500": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 服務器錯誤" - } - }, - "summary": "LINE 獲取 Access Token" - } - }, - "/api/v1/account/list": { - "post": { - "description": "分頁獲取用戶列表,支援篩選條件", - "operationId": "accountListMember", - "requestBody": { - "content": { - "application/json": { - "schema": { - "properties": { - "alarm_type": { - "type": "integer" - }, - "create_end_time": { - "type": "integer" - }, - "create_start_time": { - "type": "integer" - }, - "page_index": { - "type": "integer" - }, - "page_size": { - "type": "integer" - }, - "status": { - "type": "integer" - } - }, - "required": [ - "alarm_type", - "status", - "create_start_time", - "create_end_time", - "page_size", - "page_index" - ], - "type": "object" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": { - "data": { - "items": { - "properties": { - "address": { - "type": "string" - }, - "alarm_type": { - "type": "integer" - }, - "avatar_url": { - "type": "string" - }, - "birthday": { - "type": "integer" - }, - "create_time": { - "type": "integer" - }, - "currency": { - "type": "string" - }, - "email": { - "type": "string" - }, - "full_name": { - "type": "string" - }, - "gender_code": { - "type": "integer" - }, - "language": { - "type": "string" - }, - "nick_name": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "status": { - "type": "integer" - }, - "uid": { - "type": "string" - }, - "update_time": { - "type": "integer" - } - }, - "required": [ - "uid", - "avatar_url", - "full_name", - "nick_name", - "gender_code", - "birthday", - "phone", - "email", - "address", - "alarm_type", - "status", - "language", - "currency", - "create_time", - "update_time" - ], - "type": "object" - }, - "type": "array" - }, - "pager": { - "properties": { - "index": { - "type": "integer" - }, - "size": { - "type": "integer" - }, - "total": { - "type": "integer" - } - }, - "required": [ - "total", - "size", - "index" - ], - "type": "object" - } - }, - "type": "object" - } - } - }, - "description": "// 獲取成功" - }, - "400": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - } - ] - } - } - }, - "description": "客戶端錯誤\n\nPossible errors:\n40004: ErrorResp - 參數驗證失敗\n40009: ErrorResp - 分頁參數無效" - }, - "500": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 服務器錯誤" - } - }, - "summary": "獲取用戶列表" - } - }, - "/api/v1/account/uid": { - "post": { - "description": "根據帳號獲取對應的用戶 UID", - "operationId": "accountGetUIDByAccount", - "requestBody": { - "content": { - "application/json": { - "schema": { - "properties": { - "account": { - "type": "string" - } - }, - "required": [ - "account" - ], - "type": "object" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": { - "account": { - "type": "string" - }, - "uid": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 獲取成功" - }, - "400": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - } - ] - } - } - }, - "description": "客戶端錯誤\n\nPossible errors:\n40004: ErrorResp - 參數驗證失敗\n40001: ErrorResp - 帳號格式錯誤" - }, - "404": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 帳號不存在" - }, - "500": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 服務器錯誤" - } - }, - "summary": "獲取用戶 UID" - } - }, - "/api/v1/account/update-info": { - "post": { - "description": "更新用戶的個人資料資訊", - "operationId": "accountUpdateUserInfo", - "requestBody": { - "content": { - "application/json": { - "schema": { - "properties": { - "address": { - "type": "string" - }, - "alarm_type": { - "type": "integer" - }, - "avatar": { - "type": "string" - }, - "birthdate": { - "type": "integer" - }, - "currency": { - "type": "string" - }, - "full_name": { - "type": "string" - }, - "gender": { - "type": "integer" - }, - "language": { - "type": "string" - }, - "nick_name": { - "type": "string" - }, - "status": { - "type": "integer" - }, - "uid": { - "type": "string" - } - }, - "required": [ - "uid", - "language", - "currency", - "nick_name", - "avatar", - "alarm_type", - "status", - "full_name", - "gender", - "birthdate", - "address" - ], - "type": "object" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "data": {}, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 更新成功" - }, - "400": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - } - ] - } - } - }, - "description": "客戶端錯誤\n\nPossible errors:\n40006: ErrorResp - 郵箱格式錯誤\n40007: ErrorResp - 電話格式錯誤\n40004: ErrorResp - 參數驗證失敗\n40005: ErrorResp - UID 格式錯誤" - }, - "404": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 用戶不存在" - }, - "422": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 用戶資料無效" - }, - "500": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 服務器錯誤" - } - }, - "summary": "更新用戶資料" - } - }, - "/api/v1/account/update-status": { - "post": { - "description": "更新用戶的帳號狀態", - "operationId": "accountUpdateStatus", - "requestBody": { - "content": { - "application/json": { - "schema": { - "properties": { - "status": { - "type": "integer" - }, - "uid": { - "type": "string" - } - }, - "required": [ - "uid", - "status" - ], - "type": "object" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "data": {}, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 更新成功" - }, - "400": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - } - ] - } - } - }, - "description": "客戶端錯誤\n\nPossible errors:\n40004: ErrorResp - 參數驗證失敗\n40005: ErrorResp - UID 格式錯誤\n40008: ErrorResp - 狀態值無效" - }, - "404": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 用戶不存在" - }, - "422": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 狀態更新無效" - }, - "500": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 服務器錯誤" - } - }, - "summary": "更新用戶狀態" - } - }, - "/api/v1/account/update-token": { - "post": { - "description": "更新指定帳號的密碼", - "operationId": "accountUpdateUserToken", - "requestBody": { - "content": { - "application/json": { - "schema": { - "properties": { - "account": { - "type": "string" - }, - "platform": { - "type": "integer" - }, - "token": { - "type": "string" - } - }, - "required": [ - "account", - "token", - "platform" - ], - "type": "object" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "data": {}, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 更新成功" - }, - "400": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - } - ] - } - } - }, - "description": "客戶端錯誤\n\nPossible errors:\n40003: ErrorResp - 平台類型無效\n40001: ErrorResp - 帳號格式錯誤\n40002: ErrorResp - 密碼強度不足" - }, - "404": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 帳號不存在" - }, - "500": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 服務器錯誤" - } - }, - "summary": "更新用戶密碼" - } - }, - "/api/v1/account/user-info": { - "post": { - "description": "根據 UID 或暱稱獲取用戶詳細資訊", - "operationId": "accountGetUserInfo", - "requestBody": { - "content": { - "application/json": { - "schema": { - "properties": { - "nick_name": { - "type": "string" - }, - "uid": { - "type": "string" - } - }, - "required": [ - "uid", - "nick_name" - ], - "type": "object" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": { - "data": { - "properties": { - "address": { - "type": "string" - }, - "alarm_type": { - "type": "integer" - }, - "avatar_url": { - "type": "string" - }, - "birthday": { - "type": "integer" - }, - "create_time": { - "type": "integer" - }, - "currency": { - "type": "string" - }, - "email": { - "type": "string" - }, - "full_name": { - "type": "string" - }, - "gender_code": { - "type": "integer" - }, - "language": { - "type": "string" - }, - "nick_name": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "status": { - "type": "integer" - }, - "uid": { - "type": "string" - }, - "update_time": { - "type": "integer" - } - }, - "required": [ - "uid", - "avatar_url", - "full_name", - "nick_name", - "gender_code", - "birthday", - "phone", - "email", - "address", - "alarm_type", - "status", - "language", - "currency", - "create_time", - "update_time" - ], - "type": "object" - } - }, - "type": "object" - } - } - }, - "description": "// 獲取成功" - }, - "400": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - } - ] - } - } - }, - "description": "客戶端錯誤\n\nPossible errors:\n40004: ErrorResp - 參數驗證失敗\n40005: ErrorResp - UID 格式錯誤" - }, - "404": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 用戶不存在" - }, - "500": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 服務器錯誤" - } - }, - "summary": "獲取用戶資訊" - } - }, - "/api/v1/account/verify-code": { - "post": { - "description": "驗證並使用驗證碼,驗證後會刪除驗證碼", - "operationId": "accountVerifyRefreshCode", - "requestBody": { - "content": { - "application/json": { - "schema": { - "properties": { - "account": { - "type": "string" - }, - "code_type": { - "type": "integer" - }, - "verify_code": { - "type": "string" - } - }, - "required": [ - "account", - "code_type", - "verify_code" - ], - "type": "object" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "data": {}, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 驗證成功" - }, - "400": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - } - ] - } - } - }, - "description": "客戶端錯誤\n\nPossible errors:\n40001: ErrorResp - 帳號格式錯誤\n40004: ErrorResp - 參數驗證失敗\n40010: ErrorResp - 驗證碼類型無效\n40011: ErrorResp - 驗證碼格式錯誤" - }, - "404": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 驗證碼不存在" - }, - "422": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 驗證碼無效或已過期" - }, - "500": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 服務器錯誤" - } - }, - "summary": "驗證驗證碼" - } - }, - "/api/v1/account/verify-google": { - "post": { - "description": "驗證 Google OAuth 登入是否有效", - "operationId": "accountVerifyGoogleAuthResult", - "requestBody": { - "content": { - "application/json": { - "schema": { - "properties": { - "account": { - "type": "string" - }, - "token": { - "type": "string" - } - }, - "required": [ - "token", - "account" - ], - "type": "object" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": { - "aud": { - "type": "string" - }, - "email": { - "type": "string" - }, - "email_verified": { - "type": "string" - }, - "exp": { - "type": "string" - }, - "iat": { - "type": "string" - }, - "iss": { - "type": "string" - }, - "name": { - "type": "string" - }, - "picture": { - "type": "string" - }, - "status": { - "type": "boolean" - }, - "sub": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 驗證成功" - }, - "400": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - } - ] - } - } - }, - "description": "客戶端錯誤\n\nPossible errors:\n40012: ErrorResp - Token 格式錯誤\n40004: ErrorResp - 參數驗證失敗" - }, - "401": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// Token 無效或過期" - }, - "422": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// Google 認證失敗" - }, - "500": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 服務器錯誤" - } - }, - "summary": "驗證 Google 認證" - } - }, - "/api/v1/account/verify-platform": { - "post": { - "description": "驗證平台登入認證是否有效", - "operationId": "accountVerifyPlatformAuthResult", - "requestBody": { - "content": { - "application/json": { - "schema": { - "properties": { - "account": { - "type": "string" - }, - "token": { - "type": "string" - } - }, - "required": [ - "token", - "account" - ], - "type": "object" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": { - "status": { - "type": "boolean" - } - }, - "type": "object" - } - } - }, - "description": "// 驗證成功" - }, - "400": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/ErrorResp" - }, - { - "$ref": "#/components/schemas/ErrorResp" - } - ] - } - } - }, - "description": "客戶端錯誤\n\nPossible errors:\n40004: ErrorResp - 參數驗證失敗\n40012: ErrorResp - Token 格式錯誤" - }, - "401": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// Token 無效或過期" - }, - "422": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 平台認證失敗" - }, - "500": { - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "integer" - }, - "details": { - "type": "string" - }, - "error": { - "description": "可選的錯誤信息" - }, - "msg": { - "type": "string" - } - }, - "type": "object" - } - } - }, - "description": "// 服務器錯誤" - } - }, - "summary": "驗證平台認證" - } - }, - "/api/v1/health": { - "get": { - "description": "檢查系統服務狀態,用於監控和負載均衡器健康檢查。返回系統運行狀態信息。", - "operationId": "pingPing", - "responses": { - "200": { - "content": { - "application/json": { - "schema": {} - } - }, - "description": "" - } - }, - "summary": "系統健康檢查" - } - } - }, - "servers": [ - { - "url": "http://127.0.0.1:8888" - }, - { - "url": "https://127.0.0.1:8888" - } - ], - "x-date": "2025-10-01 00:43:58", - "x-description": "This is a go-doc generated swagger file.", - "x-generator": "go-doc", - "x-github": "https://github.com/danielchan-25/go-doc", - "x-source": "go-zero API specification" -} \ No newline at end of file diff --git a/generate/api/common.api b/generate/api/common.api index 808dba5..fdfa841 100755 --- a/generate/api/common.api +++ b/generate/api/common.api @@ -3,7 +3,7 @@ syntax = "v1" // ================ 通用響應 ================ type ( // 成功響應 - OKResp { + RespOK { Code int `json:"code"` Msg string `json:"msg"` Data interface{} `json:"data,omitempty"` @@ -26,8 +26,7 @@ type ( BaseReq {} - VerifyHeader { - Token string `header:"token" validate:"required"` + Authorization { + Authorization string `header:"Authorization" validate:"required"` } ) - diff --git a/generate/api/member.api b/generate/api/member.api index 3091993..445718c 100644 --- a/generate/api/member.api +++ b/generate/api/member.api @@ -1,554 +1,307 @@ syntax = "v1" -// ================ 請求/響應結構 ================ + + +// ================================================================= +// Type: 授權與驗證 (Auth) +// ================================================================= type ( - // 創建帳號請求 - CreateUserAccountReq { - LoginID string `json:"login_id" validate:"required,min=3,max=50"` - Platform string `json:"platform" validate:"required,oneof=platform google line apple"` - Token string `json:"token" validate:"required,min=8,max=128"` + // CredentialsPayload 傳統帳號密碼註冊的資料 + CredentialsPayload { + Password string `json:"password" validate:"required,min=8,max=128"` // 密碼 (後端應使用 bcrypt 進行雜湊) + PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password"` // 確認密碼 } - // 綁定用戶請求 - BindingUserReq { - UID string `json:"uid" validate:"required,min=1,max=50"` - LoginID string `json:"login_id" validate:"required,min=3,max=50"` - Type int `json:"type" validate:"required,oneof=1 2 3"` + // PlatformPayload 第三方平台註冊的資料 + PlatformPayload { + Provider string `json:"provider" validate:"required,oneof=google line apple"` // 平台名稱 + Token string `json:"token" validate:"required"` // 平台提供的 Access Token 或 ID Token } - // 綁定用戶響應 - BindingUserResp { - UID string `json:"uid"` - LoginID string `json:"login_id"` - Type int `json:"type"` + // RegisterReq 註冊請求 (整合了兩種方式) + LoginReq { + AuthMethod string `json:"auth_method" validate:"required,oneof=credentials platform"` + LoginID string `json:"login_id" validate:"required,min=3,max=50"` // 信箱或手機號碼 + Credentials *CredentialsPayload `json:"credentials,optional"` // AuthMethod 為 'credentials' 時使用 + Platform *PlatformPayload `json:"platform,optional"` // AuthMethod 為 'platform' 時使用 } - // 創建用戶資料請求 - CreateUserInfoReq { - UID string `json:"uid" validate:"required,min=1,max=50"` - AlarmType int `json:"alarm_type" validate:"required,oneof=0 1 2"` - Status int `json:"status" validate:"required,oneof=0 1 2 3"` - Language string `json:"language" validate:"required,min=2,max=10"` - Currency string `json:"currency" validate:"required,min=3,max=3"` - Avatar *string `json:"avatar,omitempty"` - NickName *string `json:"nick_name,omitempty" validate:"omitempty,min=1,max=50"` - FullName *string `json:"full_name,omitempty" validate:"omitempty,min=1,max=100"` - Gender *int64 `json:"gender,omitempty" validate:"omitempty,oneof=0 1 2"` - Birthdate *int64 `json:"birthdate,omitempty" validate:"omitempty,min=19000101,max=21001231"` - PhoneNumber *string `json:"phone_number,omitempty" validate:"omitempty,min=10,max=20"` - Email *string `json:"email,omitempty" validate:"omitempty,email"` - Address *string `json:"address,omitempty" validate:"omitempty,max=200"` + // LoginResp 登入/註冊成功後的響應 + LoginResp { + UID string `json:"uid"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + TokenType string `json:"token_type"` // 通常固定為 "Bearer" + } + // --- 密碼重設流程 --- + + // RequestPasswordResetReq 請求發送「忘記密碼」的驗證碼 + RequestPasswordResetReq { + Identifier string `json:"identifier" validate:"required,email|phone"` // 使用者帳號 (信箱或手機) + AccountType string `json:"account_type" validate:"required,oneof=email phone"` } - // 獲取帳號資訊響應 - GetAccountInfoResp { - Data CreateUserAccountReq `json:"data"` + // VerifyCodeReq 驗證碼校驗 (通用) + VerifyCodeReq { + Identifier string `json:"identifier" validate:"required"` + VerifyCode string `json:"verify_code" validate:"required,len=6"` } - // 更新用戶資料請求 - UpdateUserInfoReq { - UID string `json:"uid" validate:"required,min=1,max=50"` - Language *string `json:"language,omitempty" validate:"omitempty,min=2,max=10"` - Currency *string `json:"currency,omitempty" validate:"omitempty,min=3,max=3"` - NickName *string `json:"nick_name,omitempty" validate:"omitempty,min=1,max=50"` - Avatar *string `json:"avatar,omitempty"` - AlarmType *int `json:"alarm_type,omitempty" validate:"omitempty,oneof=0 1 2"` - Status *int `json:"status,omitempty" validate:"omitempty,oneof=0 1 2 3"` - FullName *string `json:"full_name,omitempty" validate:"omitempty,min=1,max=100"` - Gender *int64 `json:"gender,omitempty" validate:"omitempty,oneof=0 1 2"` - Birthdate *int64 `json:"birthdate,omitempty" validate:"omitempty,min=19000101,max=21001231"` - Address *string `json:"address,omitempty" validate:"omitempty,max=200"` + // ResetPasswordReq 使用已驗證的 Code 來重設密碼 只有用帳號密碼的才能發送重設密碼 + ResetPasswordReq { + Identifier string `json:"identifier" validate:"required"` + VerifyCode string `json:"verify_code" validate:"required"` // 來自上一步驗證通過的 Code,作為一種「票證」 + Password string `json:"password" validate:"required,min=8,max=128"` // 新密碼 + PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password"` // 確認新密碼 } - // 獲取 UID 請求 - GetUIDByAccountReq { - Account string `json:"account" validate:"required,min=3,max=50"` + // --- 4. 權杖刷新 --- + // RefreshTokenReq 更新 AccessToken + RefreshTokenReq { + RefreshToken string `json:"refresh_token" validate:"required"` } - // 獲取 UID 響應 - GetUIDByAccountResp { - UID string `json:"uid"` - Account string `json:"account"` - } - - // 更新密碼請求 - UpdateTokenReq { - Account string `json:"account" validate:"required,min=3,max=50"` - Token string `json:"token" validate:"required,min=8,max=128"` - Platform int `json:"platform" validate:"required,oneof=1 2 3 4"` - } - - // 生成驗證碼請求 - GenerateRefreshCodeReq { - Account string `json:"account" validate:"required,min=3,max=50"` - CodeType int `json:"code_type" validate:"required,oneof=1 2 3"` - } - - // 驗證碼響應 - VerifyCodeResp { - VerifyCode string `json:"verify_code"` - } - - // 生成驗證碼響應 - GenerateRefreshCodeResp { - Data VerifyCodeResp `json:"data"` - } - - // 驗證碼請求 - VerifyRefreshCodeReq { - Account string `json:"account" validate:"required,min=3,max=50"` - CodeType int `json:"code_type" validate:"required,oneof=1 2 3"` - VerifyCode string `json:"verify_code" validate:"required,min=4,max=10"` - } - - // 更新狀態請求 - UpdateStatusReq { - UID string `json:"uid" validate:"required,min=1,max=50"` - Status int `json:"status" validate:"required,oneof=0 1 2 3"` - } - - // 獲取用戶資訊請求 - GetUserInfoReq { - UID string `json:"uid,omitempty" validate:"omitempty,min=1,max=50"` - NickName *string `json:"nick_name,omitempty" validate:"omitempty,min=1,max=50"` - } - - // 用戶資訊 - UserInfo { - UID string `json:"uid"` - AvatarURL *string `json:"avatar_url,omitempty"` - FullName *string `json:"full_name,omitempty"` - NickName *string `json:"nick_name,omitempty"` - GenderCode *int64 `json:"gender_code,omitempty"` - Birthday *int64 `json:"birthday,omitempty"` - Phone *string `json:"phone,omitempty"` - Email *string `json:"email,omitempty"` - Address *string `json:"address,omitempty"` - AlarmType int `json:"alarm_type"` - Status int `json:"status"` - Language string `json:"language"` - Currency string `json:"currency"` - CreateTime int64 `json:"create_time"` - UpdateTime int64 `json:"update_time"` - } - - // 獲取用戶資訊響應 - GetUserInfoResp { - Data UserInfo `json:"data"` - } - - // 用戶列表請求 - ListUserInfoReq { - AlarmType *int `json:"alarm_type,omitempty" validate:"omitempty,oneof=0 1 2"` - Status *int `json:"status,omitempty" validate:"omitempty,oneof=0 1 2 3"` - CreateStartTime *int64 `json:"create_start_time,omitempty"` - CreateEndTime *int64 `json:"create_end_time,omitempty"` - PageSize int `json:"page_size" validate:"required,min=1,max=100"` - PageIndex int `json:"page_index" validate:"required,min=1"` - } - - // 用戶列表響應 - ListUserInfoResp { - Data []UserInfo `json:"data"` - Pager PagerResp `json:"pager"` - } - - // 驗證認證結果請求 - VerifyAuthResultReq { - Token string `json:"token" validate:"required,min=1"` - Account *string `json:"account,omitempty" validate:"omitempty,min=3,max=50"` - } - - // 驗證認證結果響應 - VerifyAuthResultResp { - Status bool `json:"status"` - } - - // Google 認證結果響應 - VerifyGoogleAuthResultResp { - Status bool `json:"status"` - Iss *string `json:"iss,omitempty"` - Sub *string `json:"sub,omitempty"` - Aud *string `json:"aud,omitempty"` - Exp *string `json:"exp,omitempty"` - Iat *string `json:"iat,omitempty"` - Email *string `json:"email,omitempty"` - EmailVerified *string `json:"email_verified,omitempty"` - Name *string `json:"name,omitempty"` - Picture *string `json:"picture,omitempty"` - } - - // 綁定驗證 Email 請求 - BindVerifyEmailReq { - UID string `json:"uid" validate:"required,min=1,max=50"` - Email string `json:"email" validate:"required,email"` - } - - // 綁定驗證 Phone 請求 - BindVerifyPhoneReq { - UID string `json:"uid" validate:"required,min=1,max=50"` - Phone string `json:"phone" validate:"required,min=10,max=20"` - } - - // LINE 獲取 Token 請求 - LineGetTokenReq { - Code string `json:"code" validate:"required,min=1"` - } - - // LINE Access Token 響應 - LineAccessTokenResp { - Token string `json:"token"` - } - - // LINE 用戶資料 - LineUserProfile { - DisplayName string `json:"display_name"` - UserID string `json:"user_id"` - PictureURL string `json:"picture_url"` - StatusMessage string `json:"status_message"` - } - - // LINE 獲取用戶資訊請求 - LineGetUserInfoReq { - Token string `json:"token" validate:"required,min=1"` + // RefreshTokenResp 刷新權杖後的響應 + RefreshTokenResp { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` // 可選:某些策略下刷新後也會換發新的 Refresh Token + TokenType string `json:"token_type"` } ) -// ================ API 路由 ================ +// ================================================================= +// Type: 使用者資訊與管理 (User) +// ================================================================= +type ( + // --- 1. 會員資訊 --- + + // UserInfoResp 用於獲取會員資訊的標準響應結構 + UserInfoResp { + Platform string `json:"platform"` // 註冊平台 + UID string `json:"uid"` // 用戶 UID + AvatarURL string `json:"avatar_url"` // 頭像 URL + FullName string `json:"full_name"` // 用戶全名 + Nickname string `json:"nickname"` // 暱稱 + GenderCode string `json:"gender_code"` // 性別代碼 + Birthdate string `json:"birthdate"` // 生日 (格式: 1993-04-17) + PhoneNumber string `json:"phone_number"` // 電話 + IsPhoneVerified bool `json:"is_phone_verified"` // 手機是否已驗證 + Email string `json:"email"` // 信箱 + IsEmailVerified bool `json:"is_email_verified"` // 信箱是否已驗證 + Address string `json:"address"` // 地址 + UserStatus string `json:"user_status"` // 用戶狀態 + PreferredLanguage string `json:"preferred_language"` // 偏好語言 + Currency string `json:"currency"` // 偏好幣種 + National string `json:"national"` // 國家 + PostCode string `json:"post_code"` // 郵遞區號 + Carrier string `json:"carrier"` // 載具 + Role string `json:"role"` // 角色 + UpdateAt string `json:"update_at"` + CreateAt string `json:"create_at"` + Authorization + } + + // UpdateUserInfoReq 更新會員資訊的請求結構 + UpdateUserInfoReq { + AvatarURL *string `json:"avatar_url,optional"` // 頭像 URL + FullName *string `json:"full_name,optional"` // 用戶全名 + Nickname *string `json:"nickname,optional"` // 暱稱 + GenderCode *string `json:"gender_code,optional" validate:"omitempty,oneof=secret male female"` // 性別 + Birthdate *string `json:"birthdate,optional"` // 生日 (格式: 1993-04-17) + Address *string `json:"address,optional"` // 地址 + PreferredLanguage *string `json:"preferred_language,optional" validate:"omitempty,oneof=zh-tw en-us"` // 語言 + Currency *string `json:"currency,optional" validate:"omitempty,oneof=TWD USD"` // 貨幣代號 + National *string `json:"national,optional"` // 國家 + PostCode *string `json:"post_code,optional"` // 郵遞區號 + Carrier *string `json:"carrier,optional"` // 載具 + } + + // --- 2. 修改密碼 (已登入狀態) --- + // UpdatePasswordReq 修改密碼的請求 + UpdatePasswordReq { + CurrentPassword string `json:"current_password" validate:"required"` + NewPassword string `json:"new_password" validate:"required,min=8,max=128"` + NewPasswordConfirm string `json:"new_password_confirm" validate:"eqfield=NewPassword"` + Authorization + } + + // --- 3. 通用驗證碼 (已登入狀態) --- + // RequestVerificationCodeReq 請求發送驗證碼 + RequestVerificationCodeReq { + // 驗證目的:'email_verification' 或 'phone_verification' + Purpose string `json:"purpose" validate:"required,oneof=email_verification phone_verification"` + Authorization + } + + // SubmitVerificationCodeReq 提交驗證碼以完成驗證 + SubmitVerificationCodeReq { + Purpose string `json:"purpose" validate:"required,oneof=email_verification phone_verification"` + VerifyCode string `json:"verify_code" validate:"required,len=6"` + Authorization + } +) + +// ================================================================= +// Service: 公開 API - 無需登入 (Auth Service) +// ================================================================= @server( - group: account - prefix: /api/v1/account + group: auth + prefix: /api/v1/auth + schemes: https + timeout: 10s ) service gateway { @doc( - summary: "創建帳號" - description: "創建新的帳號,支援多平台登入" + summary: "註冊新帳號" + description: "使用傳統帳號密碼或第三方平台進行註冊。成功後直接返回登入後的 Token 資訊。" ) /* - @respdoc-201 () // 創建成功 - @respdoc-400 ( - 40001: (ErrorResp) 帳號格式錯誤 - 40002: (ErrorResp) 密碼強度不足 - 40003: (ErrorResp) 平台類型無效 - ) // 客戶端錯誤 - @respdoc-409 (ErrorResp) // 帳號已存在 - @respdoc-500 (ErrorResp) // 服務器錯誤 + @respdoc-200 (LoginResp) // 註冊成功,並返回 Token + @respdoc-400 (ErrorResp) "請求參數格式錯誤" + @respdoc-409 (ErrorResp) "帳號已被註冊" // 409 Conflict: 資源衝突 + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 */ - @handler CreateUserAccount - post /create (CreateUserAccountReq) returns () + @handler register + post /register (LoginReq) returns (LoginResp) @doc( - summary: "獲取帳號資訊" - description: "根據帳號獲取用戶的帳號資訊" + summary: "使用者登入" + description: "使用傳統帳號密碼或第三方平台 Token 進行登入,以創建一個新的會話(Session)。" ) -/* - @respdoc-200 (GetAccountInfoResp) // 獲取成功 - @respdoc-400 ( - 40001: (ErrorResp) 帳號格式錯誤 - 40004: (ErrorResp) 參數驗證失敗 - ) // 客戶端錯誤 - @respdoc-404 (ErrorResp) // 帳號不存在 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler GetUserAccountInfo - post /info (GetUIDByAccountReq) returns (GetAccountInfoResp) + /* + @respdoc-200 (LoginResp) // 登入成功 + @respdoc-400 (ErrorResp) "請求參數格式錯誤" + @respdoc-401 (ErrorResp) "帳號或密碼錯誤 / 無效的平台 Token" // 401 Unauthorized + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler login + post /sessions (LoginReq) returns (LoginResp) @doc( - summary: "更新用戶密碼" - description: "更新指定帳號的密碼" + summary: "刷新 Access Token" + description: "使用有效的 Refresh Token 來獲取一組新的 Access Token 和 Refresh Token。" ) -/* - @respdoc-200 (OKResp) // 更新成功 - @respdoc-400 ( - 40001: (ErrorResp) 帳號格式錯誤 - 40002: (ErrorResp) 密碼強度不足 - 40003: (ErrorResp) 平台類型無效 - ) // 客戶端錯誤 - @respdoc-404 (ErrorResp) // 帳號不存在 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler UpdateUserToken - post /update-token (UpdateTokenReq) returns (OKResp) + /* + @respdoc-200 (RefreshTokenResp) // 刷新成功 + @respdoc-400 (ErrorResp) "請求參數格式錯誤" + @respdoc-401 (ErrorResp) "無效或已過期的 Refresh Token" // 401 Unauthorized + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler refreshToken + post /sessions/refresh (RefreshTokenReq) returns (RefreshTokenResp) @doc( - summary: "獲取用戶 UID" - description: "根據帳號獲取對應的用戶 UID" + summary: "請求發送密碼重設驗證碼" + description: "為指定的 email 或 phone 發送一個一次性的密碼重設驗證碼。" ) -/* - @respdoc-200 (GetUIDByAccountResp) // 獲取成功 - @respdoc-400 ( - 40001: (ErrorResp) 帳號格式錯誤 - 40004: (ErrorResp) 參數驗證失敗 - ) // 客戶端錯誤 - @respdoc-404 (ErrorResp) // 帳號不存在 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler GetUIDByAccount - post /uid (GetUIDByAccountReq) returns (GetUIDByAccountResp) + /* + @respdoc-200 (RespOK) // 請求成功 (為安全起見,即使帳號不存在也應返回成功) + @respdoc-400 (ErrorResp) "請求參數格式錯誤" + @respdoc-429 (ErrorResp) "請求過於頻繁" // 429 Too Many Requests + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler requestPasswordReset + post /password-resets/request (RequestPasswordResetReq) returns (RespOK) @doc( - summary: "綁定帳號" - description: "將帳號綁定到指定的用戶 UID" + summary: "校驗密碼重設驗證碼" + description: "在實際重設密碼前,先驗證使用者輸入的驗證碼是否正確。" ) -/* - @respdoc-200 (BindingUserResp) // 綁定成功 - @respdoc-400 ( - 40001: (ErrorResp) 帳號格式錯誤 - 40004: (ErrorResp) 參數驗證失敗 - 40005: (ErrorResp) UID 格式錯誤 - ) // 客戶端錯誤 - @respdoc-404 (ErrorResp) // 帳號不存在 - @respdoc-409 (ErrorResp) // 帳號已綁定 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler BindAccount - post /bind (BindingUserReq) returns (BindingUserResp) + /* + @respdoc-200 (RespOK) // 驗證碼正確 + @respdoc-400 (ErrorResp) "驗證碼無效或已過期" + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler verifyPasswordResetCode + post /password-resets/verify (VerifyCodeReq) returns (RespOK) @doc( - summary: "綁定用戶資料" - description: "初次綁定用戶的詳細資料" + summary: "執行密碼重設" + description: "使用有效的驗證碼來設定新的密碼。" ) -/* - @respdoc-200 (OKResp) // 綁定成功 - @respdoc-400 ( - 40004: (ErrorResp) 參數驗證失敗 - 40005: (ErrorResp) UID 格式錯誤 - 40006: (ErrorResp) 郵箱格式錯誤 - 40007: (ErrorResp) 電話格式錯誤 - ) // 客戶端錯誤 - @respdoc-404 (ErrorResp) // 用戶不存在 - @respdoc-409 (ErrorResp) // 用戶資料已存在 - @respdoc-422 (ErrorResp) // 用戶資料不完整 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler BindUserInfo - post /bind-info (CreateUserInfoReq) returns (OKResp) - - @doc( - summary: "綁定驗證 Email" - description: "綁定並驗證用戶的 Email 地址" - ) -/* - @respdoc-200 (OKResp) // 綁定成功 - @respdoc-400 ( - 40005: (ErrorResp) UID 格式錯誤 - 40006: (ErrorResp) 郵箱格式錯誤 - ) // 客戶端錯誤 - @respdoc-404 (ErrorResp) // 用戶不存在 - @respdoc-409 (ErrorResp) // 郵箱已綁定 - @respdoc-422 (ErrorResp) // 郵箱未驗證 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler BindVerifyEmail - post /bind-email (BindVerifyEmailReq) returns (OKResp) - - @doc( - summary: "綁定驗證 Phone" - description: "綁定並驗證用戶的電話號碼" - ) -/* - @respdoc-200 (OKResp) // 綁定成功 - @respdoc-400 ( - 40005: (ErrorResp) UID 格式錯誤 - 40007: (ErrorResp) 電話格式錯誤 - ) // 客戶端錯誤 - @respdoc-404 (ErrorResp) // 用戶不存在 - @respdoc-409 (ErrorResp) // 電話已綁定 - @respdoc-422 (ErrorResp) // 電話未驗證 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler BindVerifyPhone - post /bind-phone (BindVerifyPhoneReq) returns (OKResp) - - @doc( - summary: "更新用戶資料" - description: "更新用戶的個人資料資訊" - ) -/* - @respdoc-200 (OKResp) // 更新成功 - @respdoc-400 ( - 40004: (ErrorResp) 參數驗證失敗 - 40005: (ErrorResp) UID 格式錯誤 - 40006: (ErrorResp) 郵箱格式錯誤 - 40007: (ErrorResp) 電話格式錯誤 - ) // 客戶端錯誤 - @respdoc-404 (ErrorResp) // 用戶不存在 - @respdoc-422 (ErrorResp) // 用戶資料無效 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler UpdateUserInfo - post /update-info (UpdateUserInfoReq) returns (OKResp) - - @doc( - summary: "更新用戶狀態" - description: "更新用戶的帳號狀態" - ) -/* - @respdoc-200 (OKResp) // 更新成功 - @respdoc-400 ( - 40004: (ErrorResp) 參數驗證失敗 - 40005: (ErrorResp) UID 格式錯誤 - 40008: (ErrorResp) 狀態值無效 - ) // 客戶端錯誤 - @respdoc-404 (ErrorResp) // 用戶不存在 - @respdoc-422 (ErrorResp) // 狀態更新無效 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler UpdateStatus - post /update-status (UpdateStatusReq) returns (OKResp) - - @doc( - summary: "獲取用戶資訊" - description: "根據 UID 或暱稱獲取用戶詳細資訊" - ) -/* - @respdoc-200 (GetUserInfoResp) // 獲取成功 - @respdoc-400 ( - 40004: (ErrorResp) 參數驗證失敗 - 40005: (ErrorResp) UID 格式錯誤 - ) // 客戶端錯誤 - @respdoc-404 (ErrorResp) // 用戶不存在 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler GetUserInfo - post /user-info (GetUserInfoReq) returns (GetUserInfoResp) - - @doc( - summary: "獲取用戶列表" - description: "分頁獲取用戶列表,支援篩選條件" - ) -/* - @respdoc-200 (ListUserInfoResp) // 獲取成功 - @respdoc-400 ( - 40004: (ErrorResp) 參數驗證失敗 - 40009: (ErrorResp) 分頁參數無效 - ) // 客戶端錯誤 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler ListMember - post /list (ListUserInfoReq) returns (ListUserInfoResp) - - @doc( - summary: "生成驗證碼" - description: "為指定帳號生成驗證碼,用於忘記密碼等功能" - ) -/* - @respdoc-200 (GenerateRefreshCodeResp) // 生成成功 - @respdoc-400 ( - 40001: (ErrorResp) 帳號格式錯誤 - 40004: (ErrorResp) 參數驗證失敗 - 40010: (ErrorResp) 驗證碼類型無效 - ) // 客戶端錯誤 - @respdoc-404 (ErrorResp) // 帳號不存在 - @respdoc-429 (ErrorResp) // 請求過於頻繁 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler GenerateRefreshCode - post /generate-code (GenerateRefreshCodeReq) returns (GenerateRefreshCodeResp) - - @doc( - summary: "驗證驗證碼" - description: "驗證並使用驗證碼,驗證後會刪除驗證碼" - ) -/* - @respdoc-200 (OKResp) // 驗證成功 - @respdoc-400 ( - 40001: (ErrorResp) 帳號格式錯誤 - 40004: (ErrorResp) 參數驗證失敗 - 40010: (ErrorResp) 驗證碼類型無效 - 40011: (ErrorResp) 驗證碼格式錯誤 - ) // 客戶端錯誤 - @respdoc-404 (ErrorResp) // 驗證碼不存在 - @respdoc-422 (ErrorResp) // 驗證碼無效或已過期 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler VerifyRefreshCode - post /verify-code (VerifyRefreshCodeReq) returns (OKResp) - - @doc( - summary: "檢查驗證碼" - description: "檢查驗證碼是否正確,但不刪除驗證碼" - ) -/* - @respdoc-200 (OKResp) // 檢查成功 - @respdoc-400 ( - 40001: (ErrorResp) 帳號格式錯誤 - 40004: (ErrorResp) 參數驗證失敗 - 40010: (ErrorResp) 驗證碼類型無效 - 40011: (ErrorResp) 驗證碼格式錯誤 - ) // 客戶端錯誤 - @respdoc-404 (ErrorResp) // 驗證碼不存在 - @respdoc-422 (ErrorResp) // 驗證碼無效或已過期 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler CheckRefreshCode - post /check-code (VerifyRefreshCodeReq) returns (OKResp) - - @doc( - summary: "驗證 Google 認證" - description: "驗證 Google OAuth 登入是否有效" - ) -/* - @respdoc-200 (VerifyGoogleAuthResultResp) // 驗證成功 - @respdoc-400 ( - 40004: (ErrorResp) 參數驗證失敗 - 40012: (ErrorResp) Token 格式錯誤 - ) // 客戶端錯誤 - @respdoc-401 (ErrorResp) // Token 無效或過期 - @respdoc-422 (ErrorResp) // Google 認證失敗 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler VerifyGoogleAuthResult - post /verify-google (VerifyAuthResultReq) returns (VerifyGoogleAuthResultResp) - - @doc( - summary: "驗證平台認證" - description: "驗證平台登入認證是否有效" - ) -/* - @respdoc-200 (VerifyAuthResultResp) // 驗證成功 - @respdoc-400 ( - 40004: (ErrorResp) 參數驗證失敗 - 40012: (ErrorResp) Token 格式錯誤 - ) // 客戶端錯誤 - @respdoc-401 (ErrorResp) // Token 無效或過期 - @respdoc-422 (ErrorResp) // 平台認證失敗 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler VerifyPlatformAuthResult - post /verify-platform (VerifyAuthResultReq) returns (VerifyAuthResultResp) - - @doc( - summary: "LINE 獲取 Access Token" - description: "使用 LINE 授權碼獲取 Access Token" - ) -/* - @respdoc-200 (LineAccessTokenResp) // 獲取成功 - @respdoc-400 ( - 40004: (ErrorResp) 參數驗證失敗 - 40013: (ErrorResp) 授權碼格式錯誤 - ) // 客戶端錯誤 - @respdoc-401 (ErrorResp) // 授權碼無效或過期 - @respdoc-422 (ErrorResp) // LINE 認證失敗 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler LineCodeToAccessToken - post /line/token (LineGetTokenReq) returns (LineAccessTokenResp) - - @doc( - summary: "LINE 獲取用戶資料" - description: "使用 LINE Access Token 獲取用戶資料" - ) -/* - @respdoc-200 (LineUserProfile) // 獲取成功 - @respdoc-400 ( - 40004: (ErrorResp) 參數驗證失敗 - 40012: (ErrorResp) Token 格式錯誤 - ) // 客戶端錯誤 - @respdoc-401 (ErrorResp) // Token 無效或過期 - @respdoc-422 (ErrorResp) // LINE 用戶資料獲取失敗 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler LineGetProfileByAccessToken - post /line/profile (LineGetUserInfoReq) returns (LineUserProfile) + /* + @respdoc-200 (RespOK) // 密碼重設成功 + @respdoc-400 (ErrorResp) "驗證碼無效或請求參數錯誤" + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler resetPassword + put /password-resets (ResetPasswordReq) returns (RespOK) } + +// ================================================================= +// Service: 授權 API - 需要登入 (User Service) +// ================================================================= +@server( + group: user + prefix: /api/v1/user + schemes: https + timeout: 10s + middleware: AuthMiddleware // 所有此 group 的路由都需要經過 JWT 驗證 +) +service gateway { + @doc( + summary: "取得當前登入的會員資訊(自己)" + ) + /* + @respdoc-200 (UserInfoResp) // 成功獲取 + @respdoc-401 (ErrorResp) "未授權或 Token 無效" + @respdoc-404 (ErrorResp) "找不到使用者" + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler getUserInfo + get /me (Authorization) returns (UserInfoResp) + + @doc( + summary: "更新當前登入的會員資訊" + description: "只更新傳入的欄位,未傳入的欄位將保持不變。" + ) + /* + @respdoc-200 (UserInfoResp) // 更新成功,並返回更新後的使用者資訊 + @respdoc-400 (ErrorResp) "請求參數格式錯誤" + @respdoc-401 (ErrorResp) "未授權或 Token 無效" + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler updateUserInfo + put /me (UpdateUserInfoReq) returns (UserInfoResp) + + @doc( + summary: "修改當前登入使用者的密碼" + description: "必須提供當前密碼以進行驗證。" + ) + /* + @respdoc-200 (RespOK) // 密碼修改成功 + @respdoc-400 (ErrorResp) "請求參數格式錯誤或新舊密碼不符" + @respdoc-401 (ErrorResp) "未授權或 Token 無效" + @respdoc-403 (ErrorResp) "當前密碼不正確" // 403 Forbidden + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler updatePassword + put /me/password (UpdatePasswordReq) returns (RespOK) + + @doc( + summary: "請求發送驗證碼 (用於驗證信箱/手機)" + description: "根據傳入的 `purpose` 發送對應的驗證碼。" + ) + /* + @respdoc-200 (RespOK) // 請求已受理 + @respdoc-400 (ErrorResp) "請求參數格式錯誤" + @respdoc-401 (ErrorResp) "未授權或 Token 無效" + @respdoc-429 (ErrorResp) "請求過於頻繁" // 429 Too Many Requests + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler requestVerificationCode + post /me/verifications (RequestVerificationCodeReq) returns (RespOK) + + @doc( + summary: "提交驗證碼以完成驗證" + description: "提交收到的驗證碼,以完成特定目的的驗證,例如綁定手機或 Email。" + ) + /* + @respdoc-200 (RespOK) // 驗證成功 + @respdoc-400 (ErrorResp) "驗證碼無效或已過期" + @respdoc-401 (ErrorResp) "未授權或 Token 無效" + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler submitVerificationCode + put /me/verifications (SubmitVerificationCodeReq) returns (RespOK) +} \ No newline at end of file diff --git a/go.mod b/go.mod index 36c6245..40dae4b 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.25.1 require ( code.30cm.net/digimon/library-go/errs v1.2.14 - code.30cm.net/digimon/library-go/mongo v0.0.9 code.30cm.net/digimon/library-go/utils/invited_code v1.2.5 github.com/alicebob/miniredis/v2 v2.35.0 github.com/shopspring/decimal v1.4.0 @@ -61,7 +60,6 @@ require ( github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.0 // indirect - github.com/montanaflynn/stats v0.7.1 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect diff --git a/go.sum b/go.sum index 394e493..1b6fed2 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ code.30cm.net/digimon/library-go/errs v1.2.14 h1:Un9wcIIjjJW8D2i0ISf8ibzp9oNT4OqLsaSKW0T4RJU= code.30cm.net/digimon/library-go/errs v1.2.14/go.mod h1:Hs4v7SbXNggDVBGXSYsFMjkii1qLF+rugrIpWePN4/o= -code.30cm.net/digimon/library-go/mongo v0.0.9 h1:fPciIE5B85tXpLg8aeVQqKVbLnfpVAk9xbMu7pE2tVw= -code.30cm.net/digimon/library-go/mongo v0.0.9/go.mod h1:KBVKz/Ci5IheI77BgZxPUeKkaGvDy8fV8EDHSCOLIO4= code.30cm.net/digimon/library-go/utils/invited_code v1.2.5 h1:szWsI0K+1iEHmc/AtKx+5c7tDIc1AZdStvT0tVza1pg= code.30cm.net/digimon/library-go/utils/invited_code v1.2.5/go.mod h1:eHmWpbX6N6KXQ2xaY71uj5bwfzTaNL8pQc2njYo5Gj0= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= @@ -116,8 +114,6 @@ github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= -github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= -github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= diff --git a/internal/config/config.go b/internal/config/config.go index 8da153d..3e18886 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,7 +1,52 @@ package config -import "github.com/zeromicro/go-zero/rest" +import ( + "time" + + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/redis" + "github.com/zeromicro/go-zero/rest" +) type Config struct { rest.RestConf + // Redis 配置 + RedisConf redis.RedisConf + // Redis Cluster (Cache) + Cache cache.CacheConf + CacheExpireTime time.Duration + CacheWithNotFoundExpiry time.Duration + + Mongo struct { + Schema string + User string + Password string + Host string + Port string + Database string + ReplicaName string + MaxStaleness time.Duration + MaxPoolSize uint64 + MinPoolSize uint64 + MaxConnIdleTime time.Duration + Compressors []string + EnableStandardReadWriteSplitMode bool + ConnectTimeoutMs int64 + } + + // 密碼加密層數 + Bcrypt struct { + Cost int + } + + GoogleAuth struct { + ClientID string + AuthURL string + } + + LineAuth struct { + ClientID string + ClientSecret string + RedirectURI string + } } diff --git a/internal/handler/auth/login_handler.go b/internal/handler/auth/login_handler.go new file mode 100644 index 0000000..c3fdedf --- /dev/null +++ b/internal/handler/auth/login_handler.go @@ -0,0 +1,30 @@ +package auth + +import ( + "net/http" + + "backend/internal/logic/auth" + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 使用者登入 +func LoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.LoginReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := auth.NewLoginLogic(r.Context(), svcCtx) + resp, err := l.Login(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/auth/refresh_token_handler.go b/internal/handler/auth/refresh_token_handler.go new file mode 100644 index 0000000..e7f5854 --- /dev/null +++ b/internal/handler/auth/refresh_token_handler.go @@ -0,0 +1,30 @@ +package auth + +import ( + "net/http" + + "backend/internal/logic/auth" + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 刷新 Access Token +func RefreshTokenHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.RefreshTokenReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := auth.NewRefreshTokenLogic(r.Context(), svcCtx) + resp, err := l.RefreshToken(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/auth/register_handler.go b/internal/handler/auth/register_handler.go new file mode 100644 index 0000000..26c8744 --- /dev/null +++ b/internal/handler/auth/register_handler.go @@ -0,0 +1,30 @@ +package auth + +import ( + "net/http" + + "backend/internal/logic/auth" + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 註冊新帳號 +func RegisterHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.LoginReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := auth.NewRegisterLogic(r.Context(), svcCtx) + resp, err := l.Register(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/auth/request_password_reset_handler.go b/internal/handler/auth/request_password_reset_handler.go new file mode 100644 index 0000000..902ac75 --- /dev/null +++ b/internal/handler/auth/request_password_reset_handler.go @@ -0,0 +1,30 @@ +package auth + +import ( + "net/http" + + "backend/internal/logic/auth" + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 請求發送密碼重設驗證碼 +func RequestPasswordResetHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.RequestPasswordResetReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := auth.NewRequestPasswordResetLogic(r.Context(), svcCtx) + resp, err := l.RequestPasswordReset(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/auth/reset_password_handler.go b/internal/handler/auth/reset_password_handler.go new file mode 100644 index 0000000..6100b43 --- /dev/null +++ b/internal/handler/auth/reset_password_handler.go @@ -0,0 +1,30 @@ +package auth + +import ( + "net/http" + + "backend/internal/logic/auth" + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 執行密碼重設 +func ResetPasswordHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.ResetPasswordReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := auth.NewResetPasswordLogic(r.Context(), svcCtx) + resp, err := l.ResetPassword(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/auth/verify_password_reset_code_handler.go b/internal/handler/auth/verify_password_reset_code_handler.go new file mode 100644 index 0000000..534388a --- /dev/null +++ b/internal/handler/auth/verify_password_reset_code_handler.go @@ -0,0 +1,30 @@ +package auth + +import ( + "net/http" + + "backend/internal/logic/auth" + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 校驗密碼重設驗證碼 +func VerifyPasswordResetCodeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.VerifyCodeReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := auth.NewVerifyPasswordResetCodeLogic(r.Context(), svcCtx) + resp, err := l.VerifyPasswordResetCode(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/ping/ping_handler.go b/internal/handler/ping/ping_handler.go index 3ec50e0..10b71a3 100644 --- a/internal/handler/ping/ping_handler.go +++ b/internal/handler/ping/ping_handler.go @@ -5,6 +5,7 @@ import ( "backend/internal/logic/ping" "backend/internal/svc" + "github.com/zeromicro/go-zero/rest/httpx" ) diff --git a/internal/handler/routes.go b/internal/handler/routes.go index f7c98f6..0a58aa2 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -1,5 +1,5 @@ // Code generated by goctl. DO NOT EDIT. -// goctl 1.8.1 +// goctl 1.8.5 package handler @@ -7,13 +7,58 @@ import ( "net/http" "time" + auth "backend/internal/handler/auth" ping "backend/internal/handler/ping" + user "backend/internal/handler/user" "backend/internal/svc" "github.com/zeromicro/go-zero/rest" ) func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { + server.AddRoutes( + []rest.Route{ + { + // 執行密碼重設 + Method: http.MethodPut, + Path: "/password-resets", + Handler: auth.ResetPasswordHandler(serverCtx), + }, + { + // 請求發送密碼重設驗證碼 + Method: http.MethodPost, + Path: "/password-resets/request", + Handler: auth.RequestPasswordResetHandler(serverCtx), + }, + { + // 校驗密碼重設驗證碼 + Method: http.MethodPost, + Path: "/password-resets/verify", + Handler: auth.VerifyPasswordResetCodeHandler(serverCtx), + }, + { + // 註冊新帳號 + Method: http.MethodPost, + Path: "/register", + Handler: auth.RegisterHandler(serverCtx), + }, + { + // 使用者登入 + Method: http.MethodPost, + Path: "/sessions", + Handler: auth.LoginHandler(serverCtx), + }, + { + // 刷新 Access Token + Method: http.MethodPost, + Path: "/sessions/refresh", + Handler: auth.RefreshTokenHandler(serverCtx), + }, + }, + rest.WithPrefix("/api/v1/auth"), + rest.WithTimeout(10000*time.Millisecond), + ) + server.AddRoutes( []rest.Route{ { @@ -26,4 +71,44 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { rest.WithPrefix("/api/v1"), rest.WithTimeout(10000*time.Millisecond), ) + + server.AddRoutes( + rest.WithMiddlewares( + []rest.Middleware{serverCtx.AuthMiddleware}, + []rest.Route{ + { + // 取得當前登入的會員資訊(自己) + Method: http.MethodGet, + Path: "/me", + Handler: user.GetUserInfoHandler(serverCtx), + }, + { + // 更新當前登入的會員資訊 + Method: http.MethodPut, + Path: "/me", + Handler: user.UpdateUserInfoHandler(serverCtx), + }, + { + // 修改當前登入使用者的密碼 + Method: http.MethodPut, + Path: "/me/password", + Handler: user.UpdatePasswordHandler(serverCtx), + }, + { + // 請求發送驗證碼 (用於驗證信箱/手機) + Method: http.MethodPost, + Path: "/me/verifications", + Handler: user.RequestVerificationCodeHandler(serverCtx), + }, + { + // 提交驗證碼以完成驗證 + Method: http.MethodPut, + Path: "/me/verifications", + Handler: user.SubmitVerificationCodeHandler(serverCtx), + }, + }..., + ), + rest.WithPrefix("/api/v1/user"), + rest.WithTimeout(10000*time.Millisecond), + ) } diff --git a/internal/handler/user/get_user_info_handler.go b/internal/handler/user/get_user_info_handler.go new file mode 100644 index 0000000..d0e0b44 --- /dev/null +++ b/internal/handler/user/get_user_info_handler.go @@ -0,0 +1,30 @@ +package user + +import ( + "net/http" + + "backend/internal/logic/user" + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 取得當前登入的會員資訊(自己) +func GetUserInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.Authorization + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := user.NewGetUserInfoLogic(r.Context(), svcCtx) + resp, err := l.GetUserInfo(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/user/request_verification_code_handler.go b/internal/handler/user/request_verification_code_handler.go new file mode 100644 index 0000000..a4a4c02 --- /dev/null +++ b/internal/handler/user/request_verification_code_handler.go @@ -0,0 +1,30 @@ +package user + +import ( + "net/http" + + "backend/internal/logic/user" + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 請求發送驗證碼 (用於驗證信箱/手機) +func RequestVerificationCodeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.RequestVerificationCodeReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := user.NewRequestVerificationCodeLogic(r.Context(), svcCtx) + resp, err := l.RequestVerificationCode(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/user/submit_verification_code_handler.go b/internal/handler/user/submit_verification_code_handler.go new file mode 100644 index 0000000..375467c --- /dev/null +++ b/internal/handler/user/submit_verification_code_handler.go @@ -0,0 +1,30 @@ +package user + +import ( + "net/http" + + "backend/internal/logic/user" + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 提交驗證碼以完成驗證 +func SubmitVerificationCodeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.SubmitVerificationCodeReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := user.NewSubmitVerificationCodeLogic(r.Context(), svcCtx) + resp, err := l.SubmitVerificationCode(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/user/update_password_handler.go b/internal/handler/user/update_password_handler.go new file mode 100644 index 0000000..f827a00 --- /dev/null +++ b/internal/handler/user/update_password_handler.go @@ -0,0 +1,30 @@ +package user + +import ( + "net/http" + + "backend/internal/logic/user" + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 修改當前登入使用者的密碼 +func UpdatePasswordHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.UpdatePasswordReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := user.NewUpdatePasswordLogic(r.Context(), svcCtx) + resp, err := l.UpdatePassword(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/user/update_user_info_handler.go b/internal/handler/user/update_user_info_handler.go new file mode 100644 index 0000000..3478d9b --- /dev/null +++ b/internal/handler/user/update_user_info_handler.go @@ -0,0 +1,30 @@ +package user + +import ( + "net/http" + + "backend/internal/logic/user" + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 更新當前登入的會員資訊 +func UpdateUserInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.UpdateUserInfoReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := user.NewUpdateUserInfoLogic(r.Context(), svcCtx) + resp, err := l.UpdateUserInfo(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/logic/auth/login_logic.go b/internal/logic/auth/login_logic.go new file mode 100644 index 0000000..a36fa26 --- /dev/null +++ b/internal/logic/auth/login_logic.go @@ -0,0 +1,31 @@ +package auth + +import ( + "context" + + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type LoginLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 使用者登入 +func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic { + return &LoginLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginResp, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/logic/auth/refresh_token_logic.go b/internal/logic/auth/refresh_token_logic.go new file mode 100644 index 0000000..4c2d19d --- /dev/null +++ b/internal/logic/auth/refresh_token_logic.go @@ -0,0 +1,31 @@ +package auth + +import ( + "context" + + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type RefreshTokenLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 刷新 Access Token +func NewRefreshTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RefreshTokenLogic { + return &RefreshTokenLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *RefreshTokenLogic) RefreshToken(req *types.RefreshTokenReq) (resp *types.RefreshTokenResp, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/logic/auth/register_logic.go b/internal/logic/auth/register_logic.go new file mode 100644 index 0000000..6274d7d --- /dev/null +++ b/internal/logic/auth/register_logic.go @@ -0,0 +1,31 @@ +package auth + +import ( + "context" + + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type RegisterLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 註冊新帳號 +func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic { + return &RegisterLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *RegisterLogic) Register(req *types.LoginReq) (resp *types.LoginResp, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/logic/auth/request_password_reset_logic.go b/internal/logic/auth/request_password_reset_logic.go new file mode 100644 index 0000000..eaadd90 --- /dev/null +++ b/internal/logic/auth/request_password_reset_logic.go @@ -0,0 +1,31 @@ +package auth + +import ( + "context" + + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type RequestPasswordResetLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 請求發送密碼重設驗證碼 +func NewRequestPasswordResetLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RequestPasswordResetLogic { + return &RequestPasswordResetLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *RequestPasswordResetLogic) RequestPasswordReset(req *types.RequestPasswordResetReq) (resp *types.RespOK, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/logic/auth/reset_password_logic.go b/internal/logic/auth/reset_password_logic.go new file mode 100644 index 0000000..3eca800 --- /dev/null +++ b/internal/logic/auth/reset_password_logic.go @@ -0,0 +1,31 @@ +package auth + +import ( + "context" + + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ResetPasswordLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 執行密碼重設 +func NewResetPasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetPasswordLogic { + return &ResetPasswordLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordReq) (resp *types.RespOK, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/logic/auth/verify_password_reset_code_logic.go b/internal/logic/auth/verify_password_reset_code_logic.go new file mode 100644 index 0000000..1759270 --- /dev/null +++ b/internal/logic/auth/verify_password_reset_code_logic.go @@ -0,0 +1,31 @@ +package auth + +import ( + "context" + + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type VerifyPasswordResetCodeLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 校驗密碼重設驗證碼 +func NewVerifyPasswordResetCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *VerifyPasswordResetCodeLogic { + return &VerifyPasswordResetCodeLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *VerifyPasswordResetCodeLogic) VerifyPasswordResetCode(req *types.VerifyCodeReq) (resp *types.RespOK, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/logic/ping/ping_logic.go b/internal/logic/ping/ping_logic.go index 402d878..2137deb 100644 --- a/internal/logic/ping/ping_logic.go +++ b/internal/logic/ping/ping_logic.go @@ -4,6 +4,7 @@ import ( "context" "backend/internal/svc" + "github.com/zeromicro/go-zero/core/logx" ) diff --git a/internal/logic/user/get_user_info_logic.go b/internal/logic/user/get_user_info_logic.go new file mode 100644 index 0000000..5c945cd --- /dev/null +++ b/internal/logic/user/get_user_info_logic.go @@ -0,0 +1,31 @@ +package user + +import ( + "context" + + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetUserInfoLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 取得當前登入的會員資訊(自己) +func NewGetUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserInfoLogic { + return &GetUserInfoLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetUserInfoLogic) GetUserInfo(req *types.Authorization) (resp *types.UserInfoResp, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/logic/user/request_verification_code_logic.go b/internal/logic/user/request_verification_code_logic.go new file mode 100644 index 0000000..e4dc3ed --- /dev/null +++ b/internal/logic/user/request_verification_code_logic.go @@ -0,0 +1,31 @@ +package user + +import ( + "context" + + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type RequestVerificationCodeLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 請求發送驗證碼 (用於驗證信箱/手機) +func NewRequestVerificationCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RequestVerificationCodeLogic { + return &RequestVerificationCodeLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *RequestVerificationCodeLogic) RequestVerificationCode(req *types.RequestVerificationCodeReq) (resp *types.RespOK, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/logic/user/submit_verification_code_logic.go b/internal/logic/user/submit_verification_code_logic.go new file mode 100644 index 0000000..e9f2e75 --- /dev/null +++ b/internal/logic/user/submit_verification_code_logic.go @@ -0,0 +1,31 @@ +package user + +import ( + "context" + + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type SubmitVerificationCodeLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 提交驗證碼以完成驗證 +func NewSubmitVerificationCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SubmitVerificationCodeLogic { + return &SubmitVerificationCodeLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *SubmitVerificationCodeLogic) SubmitVerificationCode(req *types.SubmitVerificationCodeReq) (resp *types.RespOK, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/logic/user/update_password_logic.go b/internal/logic/user/update_password_logic.go new file mode 100644 index 0000000..5e187b8 --- /dev/null +++ b/internal/logic/user/update_password_logic.go @@ -0,0 +1,31 @@ +package user + +import ( + "context" + + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type UpdatePasswordLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 修改當前登入使用者的密碼 +func NewUpdatePasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdatePasswordLogic { + return &UpdatePasswordLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdatePasswordLogic) UpdatePassword(req *types.UpdatePasswordReq) (resp *types.RespOK, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/logic/user/update_user_info_logic.go b/internal/logic/user/update_user_info_logic.go new file mode 100644 index 0000000..e45ca36 --- /dev/null +++ b/internal/logic/user/update_user_info_logic.go @@ -0,0 +1,31 @@ +package user + +import ( + "context" + + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type UpdateUserInfoLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 更新當前登入的會員資訊 +func NewUpdateUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateUserInfoLogic { + return &UpdateUserInfoLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateUserInfoLogic) UpdateUserInfo(req *types.UpdateUserInfoReq) (resp *types.UserInfoResp, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/middleware/auth_middleware.go b/internal/middleware/auth_middleware.go new file mode 100644 index 0000000..e9ed61e --- /dev/null +++ b/internal/middleware/auth_middleware.go @@ -0,0 +1,19 @@ +package middleware + +import "net/http" + +type AuthMiddleware struct { +} + +func NewAuthMiddleware() *AuthMiddleware { + return &AuthMiddleware{} +} + +func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // TODO generate middleware implement function, delete after code implementation + + // Passthrough to next handler if need + next(w, r) + } +} diff --git a/internal/svc/account_model.go b/internal/svc/account_model.go new file mode 100644 index 0000000..c89e887 --- /dev/null +++ b/internal/svc/account_model.go @@ -0,0 +1,105 @@ +package svc + +import ( + "backend/internal/config" + mgo "backend/pkg/library/mongo" + cfg "backend/pkg/member/domain/config" + "backend/pkg/member/domain/usecase" + "backend/pkg/member/repository" + uc "backend/pkg/member/usecase" + "context" + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/mon" + "github.com/zeromicro/go-zero/core/stores/redis" +) + +func NewAccountUC(c *config.Config, rds *redis.Redis) usecase.AccountUseCase { + // 準備Mongo Config + conf := &mgo.Conf{ + Schema: c.Mongo.Schema, + Host: c.Mongo.Host, + Database: c.Mongo.Database, + MaxStaleness: c.Mongo.MaxStaleness, + MaxPoolSize: c.Mongo.MaxPoolSize, + MinPoolSize: c.Mongo.MinPoolSize, + MaxConnIdleTime: c.Mongo.MaxConnIdleTime, + Compressors: c.Mongo.Compressors, + EnableStandardReadWriteSplitMode: c.Mongo.EnableStandardReadWriteSplitMode, + ConnectTimeoutMs: c.Mongo.ConnectTimeoutMs, + } + if c.Mongo.User != "" { + conf.User = c.Mongo.User + conf.Password = c.Mongo.Password + } + + // 快取選項 + cacheOpts := []cache.Option{ + cache.WithExpiry(c.CacheExpireTime), + cache.WithNotFoundExpiry(c.CacheWithNotFoundExpiry), + } + dbOpts := []mon.Option{ + mgo.SetCustomDecimalType(), + mgo.InitMongoOptions(*conf), + } + + ac := repository.NewAccountRepository(repository.AccountRepositoryParam{ + Conf: conf, + CacheConf: c.Cache, + CacheOpts: cacheOpts, + DBOpts: dbOpts, + }) + + u := repository.NewUserRepository(repository.UserRepositoryParam{ + Conf: conf, + CacheConf: c.Cache, + CacheOpts: cacheOpts, + DBOpts: dbOpts, + }) + guid := repository.NewAutoIDRepository(repository.AutoIDRepositoryParam{ + Conf: conf, + DBOpts: dbOpts, + }) + auid := repository.NewAccountUIDRepository(repository.AccountUIDRepositoryParam{ + Conf: conf, + CacheConf: c.Cache, + CacheOpts: cacheOpts, + DBOpts: dbOpts, + }) + + _, _ = ac.Index20241226001UP(context.Background()) + _, _ = u.Index20241226001UP(context.Background()) + _, _ = guid.Index20241226001UP(context.Background()) + _, _ = auid.Index20241226001UP(context.Background()) + + return uc.MustMemberUseCase(uc.MemberUseCaseParam{ + Account: ac, + User: u, + AccountUID: auid, + VerifyCodeModel: repository.NewVerifyCodeRepository(rds), + GenerateUID: guid, + Config: prepareCfg(c), + }) +} + +func prepareCfg(c *config.Config) cfg.Config { + return cfg.Config{ + Bcrypt: struct{ Cost int }{Cost: c.Bcrypt.Cost}, + GoogleAuth: struct { + ClientID string + AuthURL string + }{ + ClientID: c.GoogleAuth.ClientID, + AuthURL: c.GoogleAuth.AuthURL, + }, + + LineAuth: struct { + ClientID string + ClientSecret string + RedirectURI string + }{ + ClientID: c.LineAuth.ClientID, + ClientSecret: c.LineAuth.ClientSecret, + RedirectURI: c.LineAuth.RedirectURI, + }, + } +} diff --git a/internal/svc/service_context.go b/internal/svc/service_context.go index 7a6bf15..769bc76 100644 --- a/internal/svc/service_context.go +++ b/internal/svc/service_context.go @@ -2,14 +2,28 @@ package svc import ( "backend/internal/config" + "backend/internal/middleware" + "backend/pkg/member/domain/usecase" + + "github.com/zeromicro/go-zero/core/stores/redis" + "github.com/zeromicro/go-zero/rest" ) type ServiceContext struct { - Config config.Config + Config config.Config + AuthMiddleware rest.Middleware + AccountUC usecase.AccountUseCase } func NewServiceContext(c config.Config) *ServiceContext { + rds, err := redis.NewRedis(c.RedisConf) + if err != nil { + panic(err) + } + return &ServiceContext{ - Config: c, + Config: c, + AuthMiddleware: middleware.NewAuthMiddleware().Handle, + AccountUC: NewAccountUC(&c, rds), } } diff --git a/internal/types/types.go b/internal/types/types.go index 58a3461..bb017a8 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -1,32 +1,138 @@ // Code generated by goctl. DO NOT EDIT. -// goctl 1.8.1 +// goctl 1.8.5 package types +type Authorization struct { + Authorization string `header:"Authorization" validate:"required"` +} + type BaseReq struct { } -type BaseResponse struct { - Status Status `json:"status"` // 狀態 - Data interface{} `json:"data"` // 資料 +type CredentialsPayload struct { + Password string `json:"password" validate:"required,min=8,max=128"` // 密碼 (後端應使用 bcrypt 進行雜湊) + PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password"` // 確認密碼 } -type Pager struct { - Total int64 `json:"total"` - PageSize int64 `json:"page_size"` - PageIndex int64 `json:"page_index"` -} - -type RespOK struct { -} - -type Status struct { - Code int64 `json:"code"` // 狀態碼 - Message string `json:"message"` // 訊息 - Data interface{} `json:"data,omitempty"` // 可選的資料,當有返回時才出現 +type ErrorResp struct { + Code int `json:"code"` + Msg string `json:"msg"` + Details string `json:"details,omitempty"` Error interface{} `json:"error,omitempty"` // 可選的錯誤信息 } -type VerifyHeader struct { - Token string `header:"token" validate:"required"` +type LoginReq struct { + AuthMethod string `json:"auth_method" validate:"required,oneof=credentials platform"` + LoginID string `json:"login_id" validate:"required,min=3,max=50"` // 信箱或手機號碼 + Credentials *CredentialsPayload `json:"credentials,optional"` // AuthMethod 為 'credentials' 時使用 + Platform *PlatformPayload `json:"platform,optional"` // AuthMethod 為 'platform' 時使用 +} + +type LoginResp struct { + UID string `json:"uid"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + TokenType string `json:"token_type"` // 通常固定為 "Bearer" +} + +type PagerResp struct { + Total int64 `json:"total"` + Size int64 `json:"size"` + Index int64 `json:"index"` +} + +type PlatformPayload struct { + Provider string `json:"provider" validate:"required,oneof=google line apple"` // 平台名稱 + Token string `json:"token" validate:"required"` // 平台提供的 Access Token 或 ID Token +} + +type RefreshTokenReq struct { + RefreshToken string `json:"refresh_token" validate:"required"` +} + +type RefreshTokenResp struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` // 可選:某些策略下刷新後也會換發新的 Refresh Token + TokenType string `json:"token_type"` +} + +type RequestPasswordResetReq struct { + Identifier string `json:"identifier" validate:"required,email|phone"` // 使用者帳號 (信箱或手機) + AccountType string `json:"account_type" validate:"required,oneof=email phone"` +} + +type RequestVerificationCodeReq struct { + Purpose string `json:"purpose" validate:"required,oneof=email_verification phone_verification"` + Authorization +} + +type ResetPasswordReq struct { + Identifier string `json:"identifier" validate:"required"` + VerifyCode string `json:"verify_code" validate:"required"` // 來自上一步驗證通過的 Code,作為一種「票證」 + Password string `json:"password" validate:"required,min=8,max=128"` // 新密碼 + PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password"` // 確認新密碼 +} + +type RespOK struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data interface{} `json:"data,omitempty"` +} + +type SubmitVerificationCodeReq struct { + Purpose string `json:"purpose" validate:"required,oneof=email_verification phone_verification"` + VerifyCode string `json:"verify_code" validate:"required,len=6"` + Authorization +} + +type UpdatePasswordReq struct { + CurrentPassword string `json:"current_password" validate:"required"` + NewPassword string `json:"new_password" validate:"required,min=8,max=128"` + NewPasswordConfirm string `json:"new_password_confirm" validate:"eqfield=NewPassword"` + Authorization +} + +type UpdateUserInfoReq struct { + AvatarURL *string `json:"avatar_url,optional"` // 頭像 URL + FullName *string `json:"full_name,optional"` // 用戶全名 + Nickname *string `json:"nickname,optional"` // 暱稱 + GenderCode *string `json:"gender_code,optional" validate:"omitempty,oneof=secret male female"` // 性別 + Birthdate *string `json:"birthdate,optional"` // 生日 (格式: 1993-04-17) + Address *string `json:"address,optional"` // 地址 + PreferredLanguage *string `json:"preferred_language,optional" validate:"omitempty,oneof=zh-tw en-us"` // 語言 + Currency *string `json:"currency,optional" validate:"omitempty,oneof=TWD USD"` // 貨幣代號 + National *string `json:"national,optional"` // 國家 + PostCode *string `json:"post_code,optional"` // 郵遞區號 + Carrier *string `json:"carrier,optional"` // 載具 +} + +type UserInfoResp struct { + Platform string `json:"platform"` // 註冊平台 + UID string `json:"uid"` // 用戶 UID + AvatarURL string `json:"avatar_url"` // 頭像 URL + FullName string `json:"full_name"` // 用戶全名 + Nickname string `json:"nickname"` // 暱稱 + GenderCode string `json:"gender_code"` // 性別代碼 + Birthdate string `json:"birthdate"` // 生日 (格式: 1993-04-17) + PhoneNumber string `json:"phone_number"` // 電話 + IsPhoneVerified bool `json:"is_phone_verified"` // 手機是否已驗證 + Email string `json:"email"` // 信箱 + IsEmailVerified bool `json:"is_email_verified"` // 信箱是否已驗證 + Address string `json:"address"` // 地址 + UserStatus string `json:"user_status"` // 用戶狀態 + PreferredLanguage string `json:"preferred_language"` // 偏好語言 + Currency string `json:"currency"` // 偏好幣種 + National string `json:"national"` // 國家 + PostCode string `json:"post_code"` // 郵遞區號 + Carrier string `json:"carrier"` // 載具 + Role string `json:"role"` // 角色 + UpdateAt string `json:"update_at"` + CreateAt string `json:"create_at"` + Authorization +} + +type VerifyCodeReq struct { + Identifier string `json:"identifier" validate:"required"` + VerifyCode string `json:"verify_code" validate:"required,len=6"` } diff --git a/pkg/library/mongo/config_test.go b/pkg/library/mongo/config_test.go index 940f01e..b7d82c8 100644 --- a/pkg/library/mongo/config_test.go +++ b/pkg/library/mongo/config_test.go @@ -7,7 +7,7 @@ import ( func TestConf_DefaultValues(t *testing.T) { conf := &Conf{} - + // Test default values if conf.Schema != "" { t.Errorf("Expected empty Schema, got %s", conf.Schema) @@ -66,7 +66,7 @@ func TestConf_WithValues(t *testing.T) { EnableStandardReadWriteSplitMode: true, ConnectTimeoutMs: 5000, } - + // Test set values if conf.Schema != "mongodb" { t.Errorf("Expected 'mongodb' Schema, got %s", conf.Schema) diff --git a/pkg/library/mongo/custom_mongo_decimal_test.go b/pkg/library/mongo/custom_mongo_decimal_test.go index 712f791..eba78a2 100644 --- a/pkg/library/mongo/custom_mongo_decimal_test.go +++ b/pkg/library/mongo/custom_mongo_decimal_test.go @@ -11,18 +11,18 @@ import ( func TestMgoDecimal_InterfaceCompliance(t *testing.T) { encoder := &MgoDecimal{} decoder := &MgoDecimal{} - + // Test that they implement the required interfaces var _ bson.ValueEncoder = encoder var _ bson.ValueDecoder = decoder - + // Test that they can be used in TypeCodec codec := TypeCodec{ ValueType: reflect.TypeOf(decimal.Decimal{}), Encoder: encoder, Decoder: decoder, } - + if codec.Encoder != encoder { t.Error("Expected encoder to be set correctly") } @@ -33,15 +33,15 @@ func TestMgoDecimal_InterfaceCompliance(t *testing.T) { func TestMgoDecimal_EncodeValue_InvalidType(t *testing.T) { encoder := &MgoDecimal{} - + // Test with invalid type value := reflect.ValueOf("not a decimal") - + err := encoder.EncodeValue(bson.EncodeContext{}, nil, value) if err == nil { t.Error("Expected error for invalid type, got nil") } - + expectedErr := "value not a decimal to encode is not of type decimal.Decimal" if err.Error() != expectedErr { t.Errorf("Expected error '%s', got '%s'", expectedErr, err.Error()) @@ -61,7 +61,7 @@ func TestDecimalConversion(t *testing.T) { {"9999999999999999999.999999999999999", "9999999999999999999.999999999999999"}, {"-9999999999999999999.999999999999999", "-9999999999999999999.999999999999999"}, } - + for _, tc := range testCases { t.Run(tc.input, func(t *testing.T) { // Test decimal to string conversion @@ -69,17 +69,17 @@ func TestDecimalConversion(t *testing.T) { if err != nil { t.Fatalf("Failed to create decimal from %s: %v", tc.input, err) } - + if dec.String() != tc.expected { t.Errorf("Expected %s, got %s", tc.expected, dec.String()) } - + // Test BSON decimal128 conversion primDec, err := bson.ParseDecimal128(dec.String()) if err != nil { t.Fatalf("Failed to parse decimal128 from %s: %v", dec.String(), err) } - + if primDec.String() != tc.expected { t.Errorf("Expected %s, got %s", tc.expected, primDec.String()) } @@ -96,14 +96,14 @@ func TestDecimalConversionErrors(t *testing.T) { "123.45.67", "abc123", } - + for _, invalid := range invalidCases { t.Run(invalid, func(t *testing.T) { _, err := decimal.NewFromString(invalid) if err == nil { t.Errorf("Expected error for invalid decimal string: %s", invalid) } - + _, err = bson.ParseDecimal128(invalid) if err == nil { t.Errorf("Expected error for invalid decimal128 string: %s", invalid) @@ -125,7 +125,7 @@ func TestDecimalEdgeCases(t *testing.T) { {"positive large", decimal.NewFromInt(999999999999999), "999999999999999"}, {"negative large", decimal.NewFromInt(-999999999999999), "-999999999999999"}, } - + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Test conversion to BSON Decimal128 @@ -133,13 +133,13 @@ func TestDecimalEdgeCases(t *testing.T) { if err != nil { t.Fatalf("Failed to parse decimal128 from %s: %v", tc.value.String(), err) } - + // Test conversion back to decimal dec, err := decimal.NewFromString(primDec.String()) if err != nil { t.Fatalf("Failed to create decimal from %s: %v", primDec.String(), err) } - + if !dec.Equal(tc.value) { t.Errorf("Round trip failed: original=%s, result=%s", tc.value.String(), dec.String()) } @@ -150,7 +150,7 @@ func TestDecimalEdgeCases(t *testing.T) { // Test error handling in encoder func TestMgoDecimal_EncoderErrors(t *testing.T) { encoder := &MgoDecimal{} - + testCases := []struct { name string value interface{} @@ -159,7 +159,7 @@ func TestMgoDecimal_EncoderErrors(t *testing.T) { {"int", 123}, {"float", 123.45}, } - + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { value := reflect.ValueOf(tc.value) @@ -183,26 +183,26 @@ func TestDecimalPrecision(t *testing.T) { "0.0000001", "0.00000001", } - + for _, tc := range testCases { t.Run(tc, func(t *testing.T) { dec, err := decimal.NewFromString(tc) if err != nil { t.Fatalf("Failed to create decimal from %s: %v", tc, err) } - + // Test conversion to BSON Decimal128 primDec, err := bson.ParseDecimal128(dec.String()) if err != nil { t.Fatalf("Failed to parse decimal128 from %s: %v", dec.String(), err) } - + // Test conversion back to decimal result, err := decimal.NewFromString(primDec.String()) if err != nil { t.Fatalf("Failed to create decimal from %s: %v", primDec.String(), err) } - + if !result.Equal(dec) { t.Errorf("Precision lost: original=%s, result=%s", dec.String(), result.String()) } @@ -218,26 +218,26 @@ func TestDecimalLargeNumbers(t *testing.T) { "100000000000000000", "1000000000000000000", } - + for _, tc := range testCases { t.Run(tc, func(t *testing.T) { dec, err := decimal.NewFromString(tc) if err != nil { t.Fatalf("Failed to create decimal from %s: %v", tc, err) } - + // Test conversion to BSON Decimal128 primDec, err := bson.ParseDecimal128(dec.String()) if err != nil { t.Fatalf("Failed to parse decimal128 from %s: %v", dec.String(), err) } - + // Test conversion back to decimal result, err := decimal.NewFromString(primDec.String()) if err != nil { t.Fatalf("Failed to create decimal from %s: %v", primDec.String(), err) } - + if !result.Equal(dec) { t.Errorf("Large number lost: original=%s, result=%s", dec.String(), result.String()) } @@ -248,7 +248,7 @@ func TestDecimalLargeNumbers(t *testing.T) { // Benchmark tests func BenchmarkMgoDecimal_ParseDecimal128(b *testing.B) { dec := decimal.NewFromFloat(123.45) - + b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = bson.ParseDecimal128(dec.String()) @@ -257,7 +257,7 @@ func BenchmarkMgoDecimal_ParseDecimal128(b *testing.B) { func BenchmarkMgoDecimal_DecimalFromString(b *testing.B) { primDec, _ := bson.ParseDecimal128("123.45") - + b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = decimal.NewFromString(primDec.String()) @@ -266,10 +266,10 @@ func BenchmarkMgoDecimal_DecimalFromString(b *testing.B) { func BenchmarkMgoDecimal_RoundTrip(b *testing.B) { dec := decimal.NewFromFloat(123.45) - + b.ResetTimer() for i := 0; i < b.N; i++ { primDec, _ := bson.ParseDecimal128(dec.String()) _, _ = decimal.NewFromString(primDec.String()) } -} \ No newline at end of file +} diff --git a/pkg/library/mongo/doc-db-with-cache.go b/pkg/library/mongo/doc-db-with-cache.go index 9711d8e..20122cb 100755 --- a/pkg/library/mongo/doc-db-with-cache.go +++ b/pkg/library/mongo/doc-db-with-cache.go @@ -60,7 +60,7 @@ func (dc *DocumentDBWithCache) DeleteOne(ctx context.Context, key string, filter listerOpts = append(listerOpts, builder) } } - + val, err := dc.GetClient().DeleteOne(ctx, filter, listerOpts...) if err != nil { return 0, err @@ -102,7 +102,7 @@ func (dc *DocumentDBWithCache) FindOne(ctx context.Context, key string, v, filte listerOpts = append(listerOpts, builder) } } - + return dc.GetClient().FindOne(ctx, v, filter, listerOpts...) }) } @@ -132,7 +132,7 @@ func (dc *DocumentDBWithCache) FindOneAndDelete(ctx context.Context, key string, listerOpts = append(listerOpts, builder) } } - + if err := dc.GetClient().FindOneAndDelete(ctx, v, filter, listerOpts...); err != nil { return err } @@ -171,7 +171,7 @@ func (dc *DocumentDBWithCache) FindOneAndReplace(ctx context.Context, key string listerOpts = append(listerOpts, builder) } } - + if err := dc.GetClient().FindOneAndReplace(ctx, v, filter, replacement, listerOpts...); err != nil { return err } @@ -195,7 +195,7 @@ func (dc *DocumentDBWithCache) InsertOne(ctx context.Context, key string, docume listerOpts = append(listerOpts, builder) } } - + res, err := dc.GetClient().Collection.InsertOne(ctx, document, listerOpts...) if err != nil { return nil, err @@ -236,7 +236,7 @@ func (dc *DocumentDBWithCache) UpdateByID(ctx context.Context, key string, id, u listerOpts = append(listerOpts, builder) } } - + res, err := dc.GetClient().Collection.UpdateByID(ctx, id, update, listerOpts...) if err != nil { return nil, err @@ -277,7 +277,7 @@ func (dc *DocumentDBWithCache) UpdateMany(ctx context.Context, keys []string, fi listerOpts = append(listerOpts, builder) } } - + res, err := dc.GetClient().Collection.UpdateMany(ctx, filter, update, listerOpts...) if err != nil { return nil, err @@ -318,7 +318,7 @@ func (dc *DocumentDBWithCache) UpdateOne(ctx context.Context, key string, filter listerOpts = append(listerOpts, builder) } } - + res, err := dc.GetClient().Collection.UpdateOne(ctx, filter, update, listerOpts...) if err != nil { return nil, err @@ -336,4 +336,4 @@ func (dc *DocumentDBWithCache) UpdateOne(ctx context.Context, key string, filter // MustModelCache returns a cache cluster. func MustModelCache(conf cache.CacheConf, opts ...cache.Option) cache.Cache { return cache.New(conf, singleFlight, stats, mongo.ErrNoDocuments, opts...) -} \ No newline at end of file +} diff --git a/pkg/library/mongo/doc-db-with-cache_test.go b/pkg/library/mongo/doc-db-with-cache_test.go index 448eeff..9d79b80 100644 --- a/pkg/library/mongo/doc-db-with-cache_test.go +++ b/pkg/library/mongo/doc-db-with-cache_test.go @@ -16,23 +16,23 @@ func TestDocumentDBWithCache_MustDocumentDBWithCache(t *testing.T) { Host: "localhost:27017", Database: "testdb", } - + collection := "testcollection" cacheConf := cache.CacheConf{} - + // This will panic if MongoDB is not available, so we need to handle it defer func() { if r := recover(); r != nil { t.Logf("Expected panic in test environment: %v", r) } }() - + db, err := MustDocumentDBWithCache(conf, collection, cacheConf, nil, nil) if err != nil { t.Logf("MongoDB connection failed (expected in test environment): %v", err) return } - + if db == nil { t.Error("Expected DocumentDBWithCache to be non-nil") } @@ -43,39 +43,39 @@ func TestDocumentDBWithCache_CacheOperations(t *testing.T) { Host: "localhost:27017", Database: "testdb", } - + collection := "testcollection" cacheConf := cache.CacheConf{} - + ctx := context.Background() db, err := MustDocumentDBWithCache(conf, collection, cacheConf, nil, nil) - + if err != nil { t.Skip("Skipping test - MongoDB not available") return } - + // Test cache operations key := "test-key" value := "test-value" - + // Test SetCache err = db.SetCache(key, value) if err != nil { t.Errorf("Failed to set cache: %v", err) } - + // Test GetCache var cachedValue string err = db.GetCache(key, &cachedValue) if err != nil { t.Errorf("Failed to get cache: %v", err) } - + if cachedValue != value { t.Errorf("Expected cached value %s, got %s", value, cachedValue) } - + // Test DelCache err = db.DelCache(ctx, key) if err != nil { @@ -88,106 +88,106 @@ func TestDocumentDBWithCache_CRUDOperations(t *testing.T) { Host: "localhost:27017", Database: "testdb", } - + collection := "testcollection" cacheConf := cache.CacheConf{} - + ctx := context.Background() db, err := MustDocumentDBWithCache(conf, collection, cacheConf, nil, nil) - + if err != nil { t.Skip("Skipping test - MongoDB not available") return } - + // Test data testDoc := bson.M{ "name": "test", "value": 123, "price": decimal.NewFromFloat(99.99), } - + // Test InsertOne result, err := db.InsertOne(ctx, collection, testDoc) if err != nil { t.Errorf("Failed to insert document: %v", err) } - + insertedID := result.InsertedID if insertedID == nil { t.Error("Expected inserted ID to be non-nil") } - + // Test FindOne var foundDoc bson.M err = db.FindOne(ctx, collection, bson.M{"_id": insertedID}, &foundDoc) if err != nil { t.Errorf("Failed to find document: %v", err) } - + if foundDoc["name"] != "test" { t.Errorf("Expected name 'test', got %v", foundDoc["name"]) } - + // Test UpdateOne update := bson.M{"$set": bson.M{"value": 456}} updateResult, err := db.UpdateOne(ctx, collection, bson.M{"_id": insertedID}, update) if err != nil { t.Errorf("Failed to update document: %v", err) } - + if updateResult.ModifiedCount != 1 { t.Errorf("Expected 1 modified document, got %d", updateResult.ModifiedCount) } - + // Test UpdateByID updateByID := bson.M{"$set": bson.M{"value": 789}} updateByIDResult, err := db.UpdateByID(ctx, collection, insertedID, updateByID) if err != nil { t.Errorf("Failed to update document by ID: %v", err) } - + if updateByIDResult.ModifiedCount != 1 { t.Errorf("Expected 1 modified document, got %d", updateByIDResult.ModifiedCount) } - + // Test UpdateMany updateMany := bson.M{"$set": bson.M{"updated": true}} updateManyResult, err := db.UpdateMany(ctx, []string{collection}, bson.M{"_id": insertedID}, updateMany) if err != nil { t.Errorf("Failed to update many documents: %v", err) } - + if updateManyResult.ModifiedCount != 1 { t.Errorf("Expected 1 modified document, got %d", updateManyResult.ModifiedCount) } - + // Test FindOneAndReplace replacement := bson.M{ "name": "replaced", "value": 999, "price": decimal.NewFromFloat(199.99), } - + var replacedDoc bson.M err = db.FindOneAndReplace(ctx, collection, bson.M{"_id": insertedID}, replacement, &replacedDoc) if err != nil { t.Errorf("Failed to find and replace document: %v", err) } - + // Test FindOneAndDelete var deletedDoc bson.M err = db.FindOneAndDelete(ctx, collection, bson.M{"_id": insertedID}, &deletedDoc) if err != nil { t.Errorf("Failed to find and delete document: %v", err) } - + // Test DeleteOne deleteResult, err := db.DeleteOne(ctx, collection, bson.M{"_id": insertedID}) if err != nil { t.Errorf("Failed to delete document: %v", err) } - + if deleteResult != 0 { // Should be 0 since we already deleted it t.Errorf("Expected 0 deleted documents, got %d", deleteResult) } @@ -198,17 +198,17 @@ func TestDocumentDBWithCache_MustModelCache(t *testing.T) { Host: "localhost:27017", Database: "testdb", } - + collection := "testcollection" cacheConf := cache.CacheConf{} - + db, err := MustDocumentDBWithCache(conf, collection, cacheConf, nil, nil) - + if err != nil { t.Skip("Skipping test - MongoDB not available") return } - + // Test that we got a valid DocumentDBWithCache if db == nil { t.Error("Expected DocumentDBWithCache to be non-nil") @@ -220,12 +220,12 @@ func TestDocumentDBWithCache_ErrorHandling(t *testing.T) { invalidConf := &Conf{ Host: "invalid-host:99999", } - + collection := "testcollection" cacheConf := cache.CacheConf{} - + _, err := MustDocumentDBWithCache(invalidConf, collection, cacheConf, nil, nil) - + // This should fail if err == nil { t.Error("Expected error with invalid host, got nil") @@ -237,24 +237,24 @@ func TestDocumentDBWithCache_ContextHandling(t *testing.T) { Host: "localhost:27017", Database: "testdb", } - + collection := "testcollection" cacheConf := cache.CacheConf{} - + // Test with timeout context ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - + db, err := MustDocumentDBWithCache(conf, collection, cacheConf, nil, nil) - + // Use ctx to avoid unused variable warning _ = ctx - + if err != nil { t.Logf("MongoDB connection failed (expected in test environment): %v", err) return } - + if db == nil { t.Error("Expected DocumentDBWithCache to be non-nil") } @@ -265,45 +265,45 @@ func TestDocumentDBWithCache_WithDecimalValues(t *testing.T) { Host: "localhost:27017", Database: "testdb", } - + collection := "testcollection" cacheConf := cache.CacheConf{} - + ctx := context.Background() db, err := MustDocumentDBWithCache(conf, collection, cacheConf, nil, nil) - + if err != nil { t.Skip("Skipping test - MongoDB not available") return } - + // Test with decimal values testDoc := bson.M{ "name": "decimal-test", "price": decimal.NewFromFloat(123.45), "amount": decimal.NewFromFloat(999.99), } - + // Insert document with decimal values result, err := db.InsertOne(ctx, collection, testDoc) if err != nil { t.Errorf("Failed to insert document with decimal values: %v", err) } - + insertedID := result.InsertedID - + // Find document with decimal values var foundDoc bson.M err = db.FindOne(ctx, collection, bson.M{"_id": insertedID}, &foundDoc) if err != nil { t.Errorf("Failed to find document with decimal values: %v", err) } - + // Verify decimal values if foundDoc["name"] != "decimal-test" { t.Errorf("Expected name 'decimal-test', got %v", foundDoc["name"]) } - + // Clean up _, err = db.DeleteOne(ctx, collection, bson.M{"_id": insertedID}) if err != nil { @@ -316,18 +316,18 @@ func TestDocumentDBWithCache_WithObjectID(t *testing.T) { Host: "localhost:27017", Database: "testdb", } - + collection := "testcollection" cacheConf := cache.CacheConf{} - + ctx := context.Background() db, err := MustDocumentDBWithCache(conf, collection, cacheConf, nil, nil) - + if err != nil { t.Skip("Skipping test - MongoDB not available") return } - + // Test with ObjectID objectID := bson.NewObjectID() testDoc := bson.M{ @@ -335,30 +335,30 @@ func TestDocumentDBWithCache_WithObjectID(t *testing.T) { "name": "objectid-test", "value": 123, } - + // Insert document with ObjectID result, err := db.InsertOne(ctx, collection, testDoc) if err != nil { t.Errorf("Failed to insert document with ObjectID: %v", err) } - + insertedID := result.InsertedID - + // Verify ObjectID if insertedID != objectID { t.Errorf("Expected ObjectID %v, got %v", objectID, insertedID) } - + // Find document by ObjectID var foundDoc bson.M err = db.FindOne(ctx, collection, bson.M{"_id": objectID}, &foundDoc) if err != nil { t.Errorf("Failed to find document by ObjectID: %v", err) } - + // Clean up _, err = db.DeleteOne(ctx, collection, bson.M{"_id": objectID}) if err != nil { t.Errorf("Failed to clean up document: %v", err) } -} \ No newline at end of file +} diff --git a/pkg/library/mongo/doc-db.go b/pkg/library/mongo/doc-db.go index 2a9e73e..1a5ea78 100755 --- a/pkg/library/mongo/doc-db.go +++ b/pkg/library/mongo/doc-db.go @@ -123,7 +123,7 @@ func (document *DocumentDB) GetClient() *mon.Model { func (document *DocumentDB) yieldIndexModel(keys []string, sorts []int32, unique bool, indexOpt *options.IndexOptionsBuilder) mongo.IndexModel { SetKeysDoc := bson.D{} - for index, _ := range keys { + for index := range keys { key := keys[index] sort := sorts[index] SetKeysDoc = append(SetKeysDoc, bson.E{Key: key, Value: sort}) diff --git a/pkg/library/mongo/option_test.go b/pkg/library/mongo/option_test.go index 8da2b2b..4072fd2 100644 --- a/pkg/library/mongo/option_test.go +++ b/pkg/library/mongo/option_test.go @@ -27,7 +27,7 @@ func TestWithTypeCodec(t *testing.T) { if codec.Decoder == nil { t.Error("Expected Decoder to be set") } - + // Test WithTypeCodec function option := WithTypeCodec(codec) if option == nil { @@ -95,20 +95,20 @@ func TestTypeCodec_InterfaceCompliance(t *testing.T) { func TestMgoDecimal_WithRegistry(t *testing.T) { // Test that MgoDecimal can be used with a registry option := SetCustomDecimalType() - + // Test that the option is created if option == nil { t.Error("Expected option to be non-nil") } - + // Test basic decimal operations dec := decimal.NewFromFloat(123.45) - + // Test that decimal operations work if dec.IsZero() { t.Error("Expected decimal to be non-zero") } - + // Test string conversion decStr := dec.String() if decStr != "123.45" { @@ -199,18 +199,18 @@ func TestWithTypeCodec_EdgeCases(t *testing.T) { func TestSetCustomDecimalType_MultipleCalls(t *testing.T) { // Test calling SetCustomDecimalType multiple times - + // First call option1 := SetCustomDecimalType() - + // Second call should not panic option2 := SetCustomDecimalType() - + // Options should be valid if option1 == nil { t.Error("Expected option1 to be non-nil") } - + if option2 == nil { t.Error("Expected option2 to be non-nil") } @@ -219,13 +219,12 @@ func TestSetCustomDecimalType_MultipleCalls(t *testing.T) { func TestInitMongoOptions_ReturnType(t *testing.T) { conf := Conf{} opts := InitMongoOptions(conf) - + // Test that the returned type is correct if opts == nil { t.Error("Expected options to be non-nil") } - + // Test that we can use the options (basic type check) var _ mon.Option = opts } - diff --git a/pkg/member/domain/const.go b/pkg/member/domain/const.go index 6d54560..ffe9966 100644 --- a/pkg/member/domain/const.go +++ b/pkg/member/domain/const.go @@ -7,25 +7,25 @@ package domain const ( // DefaultBcryptCost is the default cost for bcrypt password hashing DefaultBcryptCost = 10 - + // MinPasswordLength is the minimum required password length MinPasswordLength = 8 - + // MaxPasswordLength is the maximum allowed password length MaxPasswordLength = 128 - + // DefaultVerifyCodeDigits is the default number of digits for verification codes DefaultVerifyCodeDigits = 6 - + // MinVerifyCodeDigits is the minimum number of digits for verification codes MinVerifyCodeDigits = 4 - + // MaxVerifyCodeDigits is the maximum number of digits for verification codes MaxVerifyCodeDigits = 10 - + // DefaultCacheExpiration is the default cache expiration time in seconds DefaultCacheExpiration = 3600 - + // MaxRetryAttempts is the maximum number of retry attempts for operations MaxRetryAttempts = 3 ) diff --git a/pkg/member/domain/entity/account.go b/pkg/member/domain/entity/account.go index fb9ccc6..3ecdb10 100644 --- a/pkg/member/domain/entity/account.go +++ b/pkg/member/domain/entity/account.go @@ -5,18 +5,19 @@ import ( "time" "backend/pkg/member/domain/member" + "go.mongodb.org/mongo-driver/v2/bson" ) // Account represents a user account with authentication credentials. // It stores login information, hashed passwords, and platform-specific data. type Account struct { - ID bson.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` - LoginID string `bson:"login_id"` // Unique login identifier (email, phone, username) - Token string `bson:"token"` // Hashed password or platform-specific token + ID bson.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` + LoginID string `bson:"login_id"` // Unique login identifier (email, phone, username) + Token string `bson:"token"` // Hashed password or platform-specific token Platform member.Platform `bson:"platform"` // Platform type: 1. platform 2. google 3. line 4. apple - UpdateAt *int64 `bson:"update_at,omitempty" json:"update_at,omitempty"` - CreateAt *int64 `bson:"create_at,omitempty" json:"create_at,omitempty"` + UpdateAt *int64 `bson:"update_at,omitempty" json:"update_at,omitempty"` + CreateAt *int64 `bson:"create_at,omitempty" json:"create_at,omitempty"` } // CollectionName returns the MongoDB collection name for Account entities. diff --git a/pkg/member/domain/entity/account_uid_table.go b/pkg/member/domain/entity/account_uid_table.go index bdc4690..6097288 100644 --- a/pkg/member/domain/entity/account_uid_table.go +++ b/pkg/member/domain/entity/account_uid_table.go @@ -5,16 +5,17 @@ import ( "time" "backend/pkg/member/domain/member" + "go.mongodb.org/mongo-driver/v2/bson" ) type AccountUID struct { - ID bson.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` - LoginID string `bson:"login_id"` - UID string `bson:"uid"` + ID bson.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` + LoginID string `bson:"login_id"` + UID string `bson:"uid"` Type member.AccountType `bson:"type"` - UpdateAt *int64 `bson:"update_at,omitempty" json:"update_at,omitempty"` - CreateAt *int64 `bson:"create_at,omitempty" json:"create_at,omitempty"` + UpdateAt *int64 `bson:"update_at,omitempty" json:"update_at,omitempty"` + CreateAt *int64 `bson:"create_at,omitempty" json:"create_at,omitempty"` } func (a *AccountUID) CollectionName() string { diff --git a/pkg/member/domain/entity/user.go b/pkg/member/domain/entity/user.go index 100a8a6..b3b6015 100644 --- a/pkg/member/domain/entity/user.go +++ b/pkg/member/domain/entity/user.go @@ -5,28 +5,29 @@ import ( "time" "backend/pkg/member/domain/member" + "go.mongodb.org/mongo-driver/v2/bson" ) // User represents a user profile with personal information and preferences. // It contains detailed user data that is separate from authentication credentials. type User struct { - ID bson.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` - UID string `bson:"uid"` // Unique user identifier - AvatarURL *string `bson:"avatar_url,omitempty"` // User avatar URL (optional) - FullName *string `bson:"full_name,omitempty"` // User's full name - Nickname *string `bson:"nickname,omitempty"` // User's nickname (optional) - GenderCode *int64 `bson:"gender_code,omitempty"` // Gender code - Birthdate *int64 `bson:"birthdate,omitempty"` // Birth date (format: 19930417) - Address *string `bson:"address,omitempty"` // User's address + ID bson.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` + UID string `bson:"uid"` // Unique user identifier + AvatarURL *string `bson:"avatar_url,omitempty"` // User avatar URL (optional) + FullName *string `bson:"full_name,omitempty"` // User's full name + Nickname *string `bson:"nickname,omitempty"` // User's nickname (optional) + GenderCode *int64 `bson:"gender_code,omitempty"` // Gender code + Birthdate *int64 `bson:"birthdate,omitempty"` // Birth date (format: 19930417) + Address *string `bson:"address,omitempty"` // User's address AlarmCategory member.AlarmType `bson:"alarm_category"` // Alert notification settings UserStatus member.Status `bson:"user_status"` // User account status - PreferredLanguage string `bson:"preferred_language"` // User's preferred language - Currency string `bson:"currency"` // User's preferred currency - PhoneNumber *string `bson:"phone_number,omitempty"` // Phone number (appears after verification) - Email *string `bson:"email,omitempty"` // Email address (appears after verification) - UpdateAt *int64 `bson:"update_at,omitempty" json:"update_at,omitempty"` - CreateAt *int64 `bson:"create_at,omitempty" json:"create_at,omitempty"` + PreferredLanguage string `bson:"preferred_language"` // User's preferred language + Currency string `bson:"currency"` // User's preferred currency + PhoneNumber *string `bson:"phone_number,omitempty"` // Phone number (appears after verification) + Email *string `bson:"email,omitempty"` // Email address (appears after verification) + UpdateAt *int64 `bson:"update_at,omitempty" json:"update_at,omitempty"` + CreateAt *int64 `bson:"create_at,omitempty" json:"create_at,omitempty"` } // CollectionName returns the MongoDB collection name for User entities. diff --git a/pkg/member/domain/validation_test.go b/pkg/member/domain/validation_test.go index 082c39b..382fd6f 100644 --- a/pkg/member/domain/validation_test.go +++ b/pkg/member/domain/validation_test.go @@ -108,34 +108,34 @@ func TestValidatePhone(t *testing.T) { func TestValidatePassword(t *testing.T) { tests := []struct { - name string + name string password string - wantErr bool + wantErr bool }{ { - name: "valid password", + name: "valid password", password: "password123", - wantErr: false, + wantErr: false, }, { - name: "password too short", + name: "password too short", password: "123", - wantErr: true, + wantErr: true, }, { - name: "password too long", + name: "password too long", password: string(make([]byte, MaxPasswordLength+1)), - wantErr: true, + wantErr: true, }, { - name: "empty password", + name: "empty password", password: "", - wantErr: true, + wantErr: true, }, { - name: "minimum length password", + name: "minimum length password", password: "12345678", - wantErr: false, + wantErr: false, }, } diff --git a/pkg/member/repository/account.go b/pkg/member/repository/account.go index 65b6329..8dfa475 100644 --- a/pkg/member/repository/account.go +++ b/pkg/member/repository/account.go @@ -12,6 +12,7 @@ import ( "backend/pkg/member/domain/repository" "backend/pkg/library/mongo" + "github.com/zeromicro/go-zero/core/stores/cache" "github.com/zeromicro/go-zero/core/stores/mon" "go.mongodb.org/mongo-driver/v2/bson" diff --git a/pkg/member/repository/account_test.go b/pkg/member/repository/account_test.go index f3d8ad3..1e112ff 100644 --- a/pkg/member/repository/account_test.go +++ b/pkg/member/repository/account_test.go @@ -12,6 +12,7 @@ import ( "backend/pkg/member/domain/repository" mgo "backend/pkg/library/mongo" + "github.com/alicebob/miniredis/v2" "github.com/stretchr/testify/assert" "github.com/zeromicro/go-zero/core/stores/cache" diff --git a/pkg/member/repository/account_uid.go b/pkg/member/repository/account_uid.go index c7340d3..d70cbe4 100644 --- a/pkg/member/repository/account_uid.go +++ b/pkg/member/repository/account_uid.go @@ -10,6 +10,7 @@ import ( "backend/pkg/member/domain/repository" "backend/pkg/library/mongo" + "github.com/zeromicro/go-zero/core/stores/cache" "github.com/zeromicro/go-zero/core/stores/mon" "go.mongodb.org/mongo-driver/v2/bson" diff --git a/pkg/member/repository/account_uid_test.go b/pkg/member/repository/account_uid_test.go index d3c041a..264f595 100644 --- a/pkg/member/repository/account_uid_test.go +++ b/pkg/member/repository/account_uid_test.go @@ -10,6 +10,7 @@ import ( "backend/pkg/member/domain/repository" mgo "backend/pkg/library/mongo" + "github.com/alicebob/miniredis/v2" "github.com/stretchr/testify/assert" "github.com/zeromicro/go-zero/core/stores/cache" diff --git a/pkg/member/repository/auto_id.go b/pkg/member/repository/auto_id.go index 43f5cfa..df5d41d 100644 --- a/pkg/member/repository/auto_id.go +++ b/pkg/member/repository/auto_id.go @@ -11,6 +11,7 @@ import ( GIDLib "code.30cm.net/digimon/library-go/utils/invited_code" "backend/pkg/library/mongo" + "github.com/zeromicro/go-zero/core/stores/mon" "go.mongodb.org/mongo-driver/v2/bson" mongodriver "go.mongodb.org/mongo-driver/v2/mongo" diff --git a/pkg/member/repository/auto_id_test.go b/pkg/member/repository/auto_id_test.go index f65b0b1..ca3d051 100644 --- a/pkg/member/repository/auto_id_test.go +++ b/pkg/member/repository/auto_id_test.go @@ -10,6 +10,7 @@ import ( "backend/pkg/member/domain/repository" mgo "backend/pkg/library/mongo" + "github.com/stretchr/testify/assert" ) diff --git a/pkg/member/repository/error.go b/pkg/member/repository/error.go index 8fe418f..ccbb95e 100755 --- a/pkg/member/repository/error.go +++ b/pkg/member/repository/error.go @@ -10,13 +10,13 @@ import ( var ( // ErrNotFound is returned when a requested resource is not found ErrNotFound = mon.ErrNotFound - + // ErrInvalidObjectID is returned when an invalid MongoDB ObjectID is provided ErrInvalidObjectID = errors.New("invalid objectId") - + // ErrDuplicateKey is returned when attempting to insert a document with a duplicate key ErrDuplicateKey = errors.New("duplicate key error") - + // ErrInvalidInput is returned when input validation fails ErrInvalidInput = errors.New("invalid input") ) diff --git a/pkg/member/repository/user_test.go b/pkg/member/repository/user_test.go index 1c25c8f..2bbbc7f 100644 --- a/pkg/member/repository/user_test.go +++ b/pkg/member/repository/user_test.go @@ -12,6 +12,7 @@ import ( "backend/pkg/member/domain/repository" mgo "backend/pkg/library/mongo" + "github.com/alicebob/miniredis/v2" "github.com/stretchr/testify/assert" "github.com/zeromicro/go-zero/core/stores/cache" diff --git a/pkg/member/repository/verify_code.go b/pkg/member/repository/verify_code.go index 0be2925..ae7af40 100644 --- a/pkg/member/repository/verify_code.go +++ b/pkg/member/repository/verify_code.go @@ -5,6 +5,7 @@ import ( "backend/pkg/member/domain" "backend/pkg/member/domain/repository" + "github.com/zeromicro/go-zero/core/stores/redis" ) diff --git a/pkg/member/usecase/generate_verify_code_utils.go b/pkg/member/usecase/generate_verify_code_utils.go index 99233d0..267cfc1 100644 --- a/pkg/member/usecase/generate_verify_code_utils.go +++ b/pkg/member/usecase/generate_verify_code_utils.go @@ -18,7 +18,7 @@ func generateVerifyCode(digits int) (string, error) { if digits <= 0 { digits = 6 } - + // Validate digit range if digits < 4 || digits > 10 { return "", fmt.Errorf("%w: digits must be between 4 and 10, got %d", ErrInvalidDigits, digits) @@ -26,7 +26,7 @@ func generateVerifyCode(digits int) (string, error) { // Calculate maximum value (10^digits - 1) exp := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(digits)), nil) - + // Generate cryptographically secure random number randomNumber, err := rand.Int(rand.Reader, exp) if err != nil { @@ -35,6 +35,6 @@ func generateVerifyCode(digits int) (string, error) { // Convert to string with zero padding verifyCode := fmt.Sprintf("%0*d", digits, randomNumber) - + return verifyCode, nil } diff --git a/pkg/member/usecase/password_utils.go b/pkg/member/usecase/password_utils.go index f3d9347..6b2e237 100644 --- a/pkg/member/usecase/password_utils.go +++ b/pkg/member/usecase/password_utils.go @@ -2,6 +2,7 @@ package usecase import ( "errors" + "golang.org/x/crypto/bcrypt" ) @@ -14,11 +15,11 @@ func HashPassword(password string, cost int) (string, error) { if password == "" { return "", ErrInvalidPassword } - + if cost < bcrypt.MinCost || cost > bcrypt.MaxCost { cost = bcrypt.DefaultCost } - + bytes, err := bcrypt.GenerateFromPassword([]byte(password), cost) return string(bytes), err } @@ -29,7 +30,7 @@ func CheckPasswordHash(password, hash string) bool { if password == "" || hash == "" { return false } - + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil } @@ -40,11 +41,11 @@ func GetHashingCost(hashedPassword []byte) int { if len(hashedPassword) == 0 { return 0 } - + cost, err := bcrypt.Cost(hashedPassword) if err != nil { return 0 } - + return cost }