Compare commits
No commits in common. "main" and "feat/permission" have entirely different histories.
main
...
feat/permi
112
Makefile
112
Makefile
|
|
@ -7,33 +7,22 @@ LDFLAGS := -s -w
|
|||
VERSION="v1.0.0"
|
||||
DOCKER_REPO="refactor-service"
|
||||
|
||||
|
||||
# 默認目標
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
# 顏色定義
|
||||
GREEN := \033[0;32m
|
||||
YELLOW := \033[0;33m
|
||||
NC := \033[0m # No Color
|
||||
help: ## 顯示幫助訊息
|
||||
@echo "$(GREEN)可用命令:$(NC)"
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " $(YELLOW)%-20s$(NC) %s\n", $$1, $$2}'
|
||||
|
||||
.PHONY: test
|
||||
test: ## 進行測試
|
||||
test: # 進行測試
|
||||
go test -v --cover ./...
|
||||
|
||||
|
||||
.PHONY: gen-api
|
||||
gen-api: ## 產生 api
|
||||
gen-api: # 產生 api
|
||||
goctl api go -api ./generate/api/gateway.api -dir . -style go_zero
|
||||
|
||||
.PHONY: gen-doc
|
||||
gen-doc: ## 生成 Swagger 文檔
|
||||
gen-doc: # 生成 Swagger 文檔
|
||||
# go-doc openapi --api ./generate/api/gateway.api --filename gateway.json --host dev-api.truheart.com.tw --basepath /api/v1
|
||||
go-doc -a generate/api/gateway.api -d ./ -f gateway -s openapi3.0
|
||||
|
||||
.PHONY: mock-gen
|
||||
mock-gen: ## 建立 mock 資料
|
||||
mock-gen: # 建立 mock 資料
|
||||
mockgen -source=./pkg/member/domain/repository/account.go -destination=./pkg/member/mock/repository/account.go -package=mock
|
||||
mockgen -source=./pkg/member/domain/repository/account_uid.go -destination=./pkg/member/mock/repository/account_uid.go -package=mock
|
||||
mockgen -source=./pkg/member/domain/repository/auto_id.go -destination=./pkg/member/mock/repository/auto_id.go -package=mock
|
||||
|
|
@ -56,75 +45,48 @@ mock-gen: ## 建立 mock 資料
|
|||
@echo "Generate mock files successfully"
|
||||
|
||||
.PHONY: fmt
|
||||
fmt: ## 格式優化
|
||||
fmt: # 格式優化
|
||||
$(GOFMT) -w $(GOFILES)
|
||||
goimports -w ./
|
||||
|
||||
.PHONY: build
|
||||
build: # 編譯專案
|
||||
go build -ldflags "$(LDFLAGS)" -o bin/gateway cmd/gateway/main.go
|
||||
|
||||
.PHONY: run
|
||||
run: ## 運行專案
|
||||
go run geteway.go
|
||||
run: # 運行專案
|
||||
go run cmd/gateway/main.go
|
||||
|
||||
.PHONY: clean
|
||||
clean: ## 清理編譯文件
|
||||
clean: # 清理編譯文件
|
||||
rm -rf bin/
|
||||
|
||||
.PHONY: docker-build
|
||||
docker-build: # 構建 Docker 映像
|
||||
docker build -t $(DOCKER_REPO):$(VERSION) .
|
||||
|
||||
.PHONY: docker-run
|
||||
docker-run: # 運行 Docker 容器
|
||||
docker run -p 8888:8888 $(DOCKER_REPO):$(VERSION)
|
||||
|
||||
.PHONY: install
|
||||
install: ## 安裝依賴
|
||||
install: # 安裝依賴
|
||||
go mod tidy
|
||||
go mod download
|
||||
# go install -tags 'mongodb' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
|
||||
# go get -u github.com/golang-migrate/migrate/v4/database/mongodb
|
||||
|
||||
|
||||
# MongoDB Migration 環境變數(可覆寫)
|
||||
MONGO_HOST ?= 127.0.0.1:27017
|
||||
MONGO_DB ?= digimon
|
||||
MONGO_USER ?= root
|
||||
MONGO_PASSWORD ?= example
|
||||
MONGO_AUTH_DB ?= admin
|
||||
|
||||
|
||||
.PHONY: migrate-up
|
||||
migrate-up: ## 執行 MongoDB migration (up) - 使用 mongosh + Docker
|
||||
@echo "=== 執行 MongoDB Migration (UP) ==="
|
||||
@echo "MongoDB: $(MONGO_HOST)/$(MONGO_DB)"
|
||||
docker-compose -f ./build/docker-compose-migrate.yml run --rm \
|
||||
-e MONGO_HOST=$(MONGO_HOST) \
|
||||
-e MONGO_DB=$(MONGO_DB) \
|
||||
-e MONGO_USER=$(MONGO_USER) \
|
||||
-e MONGO_PASSWORD=$(MONGO_PASSWORD) \
|
||||
-e MONGO_AUTH_DB=$(MONGO_AUTH_DB) \
|
||||
migrate
|
||||
|
||||
.PHONY: migrate-down
|
||||
migrate-down: ## 執行 MongoDB migration (down) - 使用 mongosh + Docker
|
||||
@echo "=== 執行 MongoDB Migration (DOWN) ==="
|
||||
@echo "MongoDB: $(MONGO_HOST)/$(MONGO_DB)"
|
||||
docker-compose -f ./build/docker-compose-migrate.yml run --rm \
|
||||
-e MONGO_HOST=$(MONGO_HOST) \
|
||||
-e MONGO_DB=$(MONGO_DB) \
|
||||
-e MONGO_USER=$(MONGO_USER) \
|
||||
-e MONGO_PASSWORD=$(MONGO_PASSWORD) \
|
||||
-e MONGO_AUTH_DB=$(MONGO_AUTH_DB) \
|
||||
migrate sh -c " \
|
||||
if [ -z \"$$MONGO_USER\" ] || [ \"$$MONGO_USER\" = \"\" ]; then \
|
||||
MONGO_URI=\"mongodb://$$MONGO_HOST/$$MONGO_DB\"; \
|
||||
else \
|
||||
MONGO_URI=\"mongodb://$$MONGO_USER:$$MONGO_PASSWORD@$$MONGO_HOST/$$MONGO_DB?authSource=$$MONGO_AUTH_DB\"; \
|
||||
fi && \
|
||||
echo \"執行 MongoDB migration (DOWN)...\" && \
|
||||
echo \"連接: $$MONGO_URI\" && \
|
||||
for file in \$$(ls -1 /migrations/*.down.txt 2>/dev/null | sort -r); do \
|
||||
echo \"執行: \$$(basename \$$file)\" && \
|
||||
mongosh \"$$MONGO_URI\" --file \"\$$file\" || exit 1; \
|
||||
done && \
|
||||
echo \"✅ Migration DOWN 完成\" \
|
||||
"
|
||||
|
||||
.PHONY: migrate-version
|
||||
migrate-version: ## 查看已執行的 migration 文件列表
|
||||
@echo "=== 已執行的 Migration 文件 ==="
|
||||
@echo "注意:使用 mongosh 執行,無法追蹤版本"
|
||||
@echo "Migration 文件列表:"
|
||||
@ls -1 generate/database/mongo/*.up.txt | xargs -n1 basename
|
||||
.PHONY: help
|
||||
help: # 顯示幫助信息
|
||||
@echo "Available commands:"
|
||||
@echo " test - 運行測試"
|
||||
@echo " gen-api - 產生 api"
|
||||
@echo " gen-doc - 生成 Swagger 文檔"
|
||||
@echo " mock-gen - 建立 mock 資料"
|
||||
@echo " fmt - 格式化代碼"
|
||||
@echo " build - 編譯專案"
|
||||
@echo " run - 運行專案"
|
||||
@echo " clean - 清理編譯文件"
|
||||
@echo " docker-build - 構建 Docker 映像"
|
||||
@echo " docker-run - 運行 Docker 容器"
|
||||
@echo " install - 安裝依賴"
|
||||
@echo " help - 顯示幫助信息"
|
||||
|
||||
|
|
|
|||
172
build/README.md
172
build/README.md
|
|
@ -1,172 +0,0 @@
|
|||
# MongoDB Migration 使用說明 (golang-migrate)
|
||||
|
||||
本目錄包含使用 [golang-migrate](https://github.com/golang-migrate/migrate) 執行 MongoDB migration 的配置。
|
||||
|
||||
## 重要說明
|
||||
|
||||
**golang-migrate 對 MongoDB 的文件格式要求**:
|
||||
|
||||
根據 [golang-migrate 官方文檔](https://github.com/golang-migrate/migrate),MongoDB 驅動支援以下格式:
|
||||
|
||||
1. **JSON 格式**:使用 `db.runCommand` 的 JSON 格式
|
||||
2. **JavaScript 格式**:`.js` 文件,包含 MongoDB shell 命令
|
||||
|
||||
**當前狀況**:
|
||||
- 你的 migration 文件是 `.txt` 格式的 MongoDB shell 腳本
|
||||
- golang-migrate 可能無法直接執行 `.txt` 文件
|
||||
|
||||
## 解決方案
|
||||
|
||||
### 方案 1:將 `.txt` 文件重命名為 `.js`(推薦)
|
||||
|
||||
golang-migrate 支援 `.js` 文件,你可以將現有的 `.txt` 文件重命名為 `.js`:
|
||||
|
||||
```bash
|
||||
# 重命名所有 migration 文件
|
||||
cd generate/database/mongo
|
||||
for file in *.up.txt; do mv "$file" "${file%.txt}.js"; done
|
||||
for file in *.down.txt; do mv "$file" "${file%.txt}.js"; done
|
||||
```
|
||||
|
||||
### 方案 2:使用 JSON 格式(如果需要)
|
||||
|
||||
如果 `.js` 格式不工作,可以轉換為 JSON 格式。參考 [golang-migrate MongoDB 文檔](https://pkg.go.dev/github.com/golang-migrate/migrate/v4/database/mongodb)。
|
||||
|
||||
## 文件說明
|
||||
|
||||
- `Dockerfile-migrate` - 編譯帶 MongoDB 支援的 golang-migrate
|
||||
- `docker-compose-migrate.yml` - Docker Compose 配置
|
||||
- `scripts/` - 輔助腳本(可選)
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 使用 Docker(推薦)
|
||||
|
||||
#### 執行 UP Migration
|
||||
|
||||
```bash
|
||||
# 使用預設配置
|
||||
make migrate-up
|
||||
|
||||
# 自定義 MongoDB 連接
|
||||
make migrate-up MONGO_HOST=localhost:27017 MONGO_DB=digimon_member MONGO_USER=root MONGO_PASSWORD=example
|
||||
```
|
||||
|
||||
#### 執行 DOWN Migration
|
||||
|
||||
```bash
|
||||
# 回滾一個版本
|
||||
make migrate-down
|
||||
|
||||
# 自定義連接
|
||||
make migrate-down MONGO_HOST=localhost:27017 MONGO_DB=digimon_member
|
||||
```
|
||||
|
||||
#### 查看版本
|
||||
|
||||
```bash
|
||||
make migrate-version
|
||||
```
|
||||
|
||||
### 本地執行(需要安裝 migrate)
|
||||
|
||||
```bash
|
||||
# 安裝 migrate(帶 MongoDB 支援)
|
||||
go install -tags 'mongodb' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
|
||||
|
||||
# UP migration
|
||||
make migrate-local-up
|
||||
|
||||
# DOWN migration
|
||||
make migrate-local-down
|
||||
```
|
||||
|
||||
## 環境變數
|
||||
|
||||
| 變數 | 預設值 | 說明 |
|
||||
|------|--------|------|
|
||||
| `MONGO_HOST` | `127.0.0.1:27017` | MongoDB 主機和端口 |
|
||||
| `MONGO_DB` | `digimon_member` | 資料庫名稱 |
|
||||
| `MONGO_USER` | `root` | 用戶名(可選,留空則不使用認證) |
|
||||
| `MONGO_PASSWORD` | `example` | 密碼(可選) |
|
||||
| `MONGO_AUTH_DB` | `admin` | 認證資料庫 |
|
||||
|
||||
## Migration 文件格式
|
||||
|
||||
### 當前格式(.txt)
|
||||
|
||||
```javascript
|
||||
db.collection.createIndex({"field": 1}, {unique: true});
|
||||
```
|
||||
|
||||
### golang-migrate 支援的格式
|
||||
|
||||
#### JavaScript (.js)
|
||||
```javascript
|
||||
db.collection.createIndex({"field": 1}, {unique: true});
|
||||
```
|
||||
|
||||
#### JSON 格式
|
||||
```json
|
||||
{
|
||||
"createIndexes": "collection",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {"field": 1},
|
||||
"name": "field_1",
|
||||
"unique": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 連接字符串參數
|
||||
|
||||
golang-migrate 的 MongoDB 連接字符串需要包含:
|
||||
|
||||
- `x-migrations-collection=migrations` - 指定 migration 版本記錄的集合名稱
|
||||
|
||||
完整格式:
|
||||
```
|
||||
mongodb://user:password@host:port/database?authSource=admin&x-migrations-collection=migrations
|
||||
```
|
||||
|
||||
## 注意事項
|
||||
|
||||
1. **文件格式**:確保 migration 文件是 `.js` 或 `.json` 格式
|
||||
2. **版本追蹤**:golang-migrate 會在 MongoDB 中創建 `migrations` 集合來追蹤版本
|
||||
3. **執行順序**:文件按照文件名中的時間戳順序執行
|
||||
4. **錯誤處理**:如果 migration 失敗,版本不會更新,可以安全重試
|
||||
|
||||
## 故障排除
|
||||
|
||||
### migrate 命令找不到
|
||||
|
||||
```bash
|
||||
# 安裝 migrate
|
||||
go install -tags 'mongodb' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
|
||||
```
|
||||
|
||||
### 連接失敗
|
||||
|
||||
確保 MongoDB 正在運行,並且連接資訊正確:
|
||||
|
||||
```bash
|
||||
# 測試連接
|
||||
mongosh "mongodb://root:example@127.0.0.1:27017/digimon_member?authSource=admin"
|
||||
```
|
||||
|
||||
### 文件格式錯誤
|
||||
|
||||
如果遇到文件格式錯誤,檢查文件是否為有效的 JavaScript 或 JSON:
|
||||
|
||||
```bash
|
||||
# 檢查文件
|
||||
node -c generate/database/mongo/2024110500000001_account.up.js
|
||||
```
|
||||
|
||||
## 參考資料
|
||||
|
||||
- [golang-migrate GitHub](https://github.com/golang-migrate/migrate)
|
||||
- [MongoDB Driver 文檔](https://pkg.go.dev/github.com/golang-migrate/migrate/v4/database/mongodb)
|
||||
- [CLI 文檔](https://github.com/golang-migrate/migrate/tree/master/cmd/migrate)
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
services:
|
||||
migrate:
|
||||
image: mongo:7.0
|
||||
volumes:
|
||||
- ../generate/database/mongo:/migrations:ro
|
||||
working_dir: /app
|
||||
environment:
|
||||
- MONGO_HOST=${MONGO_HOST:-127.0.0.1:27017}
|
||||
- MONGO_DB=${MONGO_DB:-digimon}
|
||||
- MONGO_USER=${MONGO_USER:-root}
|
||||
- MONGO_PASSWORD=${MONGO_PASSWORD:-example}
|
||||
- MONGO_AUTH_DB=${MONGO_AUTH_DB:-admin}
|
||||
network_mode: host
|
||||
# 預設執行 up migration
|
||||
command: >
|
||||
sh -c "
|
||||
if [ -z \"$$MONGO_USER\" ] || [ \"$$MONGO_USER\" = \"\" ]; then
|
||||
MONGO_URI=\"mongodb://$$MONGO_HOST/$$MONGO_DB\"
|
||||
else
|
||||
MONGO_URI=\"mongodb://$$MONGO_USER:$$MONGO_PASSWORD@$$MONGO_HOST/$$MONGO_DB?authSource=$$MONGO_AUTH_DB\"
|
||||
fi &&
|
||||
echo \"執行 MongoDB migration (UP)...\" &&
|
||||
echo \"連接: $$MONGO_URI\" &&
|
||||
for file in $$(ls -1 /migrations/*.up.txt 2>/dev/null | sort); do
|
||||
echo \"執行: $$(basename $$file)\" &&
|
||||
mongosh \"$$MONGO_URI\" --file \"$$file\" || exit 1
|
||||
done &&
|
||||
echo \"✅ Migration UP 完成\"
|
||||
"
|
||||
|
|
@ -8,6 +8,7 @@ services:
|
|||
environment:
|
||||
MONGO_INITDB_ROOT_USERNAME: root
|
||||
MONGO_INITDB_ROOT_PASSWORD: example
|
||||
|
||||
etcd:
|
||||
image: quay.io/coreos/etcd:v3.5.5
|
||||
container_name: etcd
|
||||
|
|
@ -21,21 +22,10 @@ services:
|
|||
ports:
|
||||
- "2379:2379"
|
||||
- "2380:2380"
|
||||
|
||||
redis:
|
||||
image: redis:7.0
|
||||
container_name: redis
|
||||
restart: always
|
||||
ports:
|
||||
- "6379:6379"
|
||||
|
||||
minio:
|
||||
image: minio/minio
|
||||
container_name: minio
|
||||
ports:
|
||||
- "9000:9000" # MinIO S3 API port
|
||||
- "9001:9001" # MinIO Console port
|
||||
environment:
|
||||
MINIO_ROOT_USER: minioadmin # Replace with your desired root username
|
||||
MINIO_ROOT_PASSWORD: minioadmin # Replace with your desired root password
|
||||
# MINIO_DEFAULT_BUCKETS: mybucket # Optional: Create a default bucket on startup
|
||||
command: server /data --console-address ":9001" # Start MinIO server and specify console address
|
||||
- "6379:6379"
|
||||
|
|
@ -7,11 +7,13 @@ Cache:
|
|||
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"
|
||||
|
|
@ -28,15 +30,19 @@ Mongo:
|
|||
- 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
|
||||
|
||||
Token:
|
||||
AccessSecret : "1qaz@WSX3edc$RFV"
|
||||
RefreshSecret : "1qaz@WSX3edc$RFV"
|
||||
|
|
@ -45,34 +51,3 @@ Token:
|
|||
OneTimeTokenExpiry : 600s
|
||||
MaxTokensPerUser : 2
|
||||
MaxTokensPerDevice : 2
|
||||
RoleConfig:
|
||||
UIDPrefix: "AM"
|
||||
UIDLength: 6
|
||||
AdminRoleUID: "AM000000"
|
||||
AdminUserUID: "B000000"
|
||||
DefaultRoleName: "USER"
|
||||
SMTPConfig:
|
||||
Enable: true
|
||||
GoroutinePoolNum: 1000
|
||||
Host: smtp.mailgun.org
|
||||
Port: 465
|
||||
Username: postmaster@code.30cm.net
|
||||
Password: 595da25c2a44ef2629ba92bf88ae94f1-02300200-af1d3b04
|
||||
Sender: daniel.wang@code.30cm.net
|
||||
SenderName: "Digimon 平台"
|
||||
DeliveryConfig:
|
||||
max_retries : 5
|
||||
initial_delay : 500ms
|
||||
backoff_factor: 2.0
|
||||
max_delay : 5000ms
|
||||
Timeout: 1000ms
|
||||
enable_history: false
|
||||
AmazonS3Settings:
|
||||
Region: ap-northeast-3
|
||||
Bucket: gutenbergtw-prod
|
||||
CloudFrontDomain: d2gk3kpttfhhhf.cloudfront.net
|
||||
CloudFrontURI: https://d2gk3kpttfhhhf.cloudfront.net
|
||||
BucketURI: https://gutenbergtw-prod.s3.ap-northeast-3.amazonaws.com
|
||||
AccessKey: AKIAVRUVVY4IJOBFOY42
|
||||
SecretKey: sSpml0h3k0y2hU5A+Fxlhcv+QGt4ddobttvvlxm+
|
||||
CloudFrontID: E3UMOQ0CGBOBAE
|
||||
389
gateway.json
389
gateway.json
|
|
@ -29,6 +29,27 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ErrorResp": {
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer"
|
||||
},
|
||||
"details": {
|
||||
"type": "string"
|
||||
},
|
||||
"error": {
|
||||
"description": "可選的錯誤信息"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"code",
|
||||
"msg"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"LoginReq": {
|
||||
"properties": {
|
||||
"auth_method": {
|
||||
|
|
@ -76,100 +97,6 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"MyInfo": {
|
||||
"properties": {
|
||||
"address": {
|
||||
"description": "地址",
|
||||
"type": "string"
|
||||
},
|
||||
"alarm_category": {
|
||||
"description": "告警狀態",
|
||||
"type": "string"
|
||||
},
|
||||
"avatar_url": {
|
||||
"description": "頭像 URL",
|
||||
"type": "string"
|
||||
},
|
||||
"birthdate": {
|
||||
"description": "生日 (格式: 1993-04-17)",
|
||||
"type": "string"
|
||||
},
|
||||
"carrier": {
|
||||
"description": "載具",
|
||||
"type": "string"
|
||||
},
|
||||
"create_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"currency": {
|
||||
"description": "偏好幣種",
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"description": "信箱",
|
||||
"type": "string"
|
||||
},
|
||||
"full_name": {
|
||||
"description": "用戶全名",
|
||||
"type": "string"
|
||||
},
|
||||
"gender_code": {
|
||||
"description": "性別代碼",
|
||||
"type": "string"
|
||||
},
|
||||
"is_email_verified": {
|
||||
"description": "信箱是否已驗證",
|
||||
"type": "boolean"
|
||||
},
|
||||
"is_phone_verified": {
|
||||
"description": "手機是否已驗證",
|
||||
"type": "boolean"
|
||||
},
|
||||
"nickname": {
|
||||
"description": "暱稱",
|
||||
"type": "string"
|
||||
},
|
||||
"phone_number": {
|
||||
"description": "電話",
|
||||
"type": "string"
|
||||
},
|
||||
"platform": {
|
||||
"description": "註冊平台",
|
||||
"type": "string"
|
||||
},
|
||||
"post_code": {
|
||||
"description": "郵遞區號",
|
||||
"type": "string"
|
||||
},
|
||||
"preferred_language": {
|
||||
"description": "偏好語言",
|
||||
"type": "string"
|
||||
},
|
||||
"role": {
|
||||
"description": "角色",
|
||||
"type": "string"
|
||||
},
|
||||
"uid": {
|
||||
"description": "用戶 UID",
|
||||
"type": "string"
|
||||
},
|
||||
"update_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"user_status": {
|
||||
"description": "用戶狀態",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"platform",
|
||||
"uid",
|
||||
"role",
|
||||
"update_at",
|
||||
"create_at"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PagerResp": {
|
||||
"properties": {
|
||||
"index": {
|
||||
|
|
@ -208,15 +135,11 @@
|
|||
},
|
||||
"RefreshTokenReq": {
|
||||
"properties": {
|
||||
"access_token": {
|
||||
"type": "string"
|
||||
},
|
||||
"refresh_token": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"access_token",
|
||||
"refresh_token"
|
||||
],
|
||||
"type": "object"
|
||||
|
|
@ -259,9 +182,6 @@
|
|||
},
|
||||
"RequestVerificationCodeReq": {
|
||||
"properties": {
|
||||
"Account": {
|
||||
"type": "string"
|
||||
},
|
||||
"purpose": {
|
||||
"type": "string"
|
||||
}
|
||||
|
|
@ -297,33 +217,24 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"Resp": {
|
||||
"RespOK": {
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string"
|
||||
"type": "integer"
|
||||
},
|
||||
"data": {},
|
||||
"error": {
|
||||
"description": "可選的錯誤信息"
|
||||
},
|
||||
"message": {
|
||||
"msg": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"code",
|
||||
"message"
|
||||
"msg"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RespOK": {
|
||||
"type": "object"
|
||||
},
|
||||
"SubmitVerificationCodeReq": {
|
||||
"properties": {
|
||||
"Account": {
|
||||
"type": "string"
|
||||
},
|
||||
"purpose": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
@ -405,41 +316,6 @@
|
|||
},
|
||||
"type": "object"
|
||||
},
|
||||
"UploadImgReq": {
|
||||
"properties": {
|
||||
"content": {
|
||||
"description": "base64 編碼的圖片內容",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"content"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"UploadResp": {
|
||||
"properties": {
|
||||
"file_size": {
|
||||
"description": "文件大小(bytes)",
|
||||
"type": "integer"
|
||||
},
|
||||
"file_url": {
|
||||
"description": "文件訪問 URL",
|
||||
"type": "string"
|
||||
},
|
||||
"mime_type": {
|
||||
"description": "MIME 類型",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"file_url"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"UploadVideoReq": {
|
||||
"type": "object"
|
||||
},
|
||||
"UserInfoResp": {
|
||||
"properties": {
|
||||
"address": {
|
||||
|
|
@ -454,6 +330,10 @@
|
|||
"description": "生日 (格式: 1993-04-17)",
|
||||
"type": "string"
|
||||
},
|
||||
"carrier": {
|
||||
"description": "載具",
|
||||
"type": "string"
|
||||
},
|
||||
"create_at": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
@ -481,6 +361,10 @@
|
|||
"description": "手機是否已驗證",
|
||||
"type": "boolean"
|
||||
},
|
||||
"national": {
|
||||
"description": "國家",
|
||||
"type": "string"
|
||||
},
|
||||
"nickname": {
|
||||
"description": "暱稱",
|
||||
"type": "string"
|
||||
|
|
@ -493,6 +377,10 @@
|
|||
"description": "註冊平台",
|
||||
"type": "string"
|
||||
},
|
||||
"post_code": {
|
||||
"description": "郵遞區號",
|
||||
"type": "string"
|
||||
},
|
||||
"preferred_language": {
|
||||
"description": "偏好語言",
|
||||
"type": "string"
|
||||
|
|
@ -529,6 +417,9 @@
|
|||
"user_status",
|
||||
"preferred_language",
|
||||
"currency",
|
||||
"national",
|
||||
"post_code",
|
||||
"carrier",
|
||||
"role",
|
||||
"update_at",
|
||||
"create_at"
|
||||
|
|
@ -591,7 +482,9 @@
|
|||
"400": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "\"驗證碼無效或請求參數錯誤\""
|
||||
|
|
@ -599,7 +492,9 @@
|
|||
"500": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "// 伺服器內部錯誤"
|
||||
|
|
@ -636,7 +531,9 @@
|
|||
"400": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "\"請求參數格式錯誤\""
|
||||
|
|
@ -644,7 +541,9 @@
|
|||
"429": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "\"請求過於頻繁\" // 429 Too Many Requests"
|
||||
|
|
@ -652,7 +551,9 @@
|
|||
"500": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "// 伺服器內部錯誤"
|
||||
|
|
@ -689,7 +590,9 @@
|
|||
"400": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "\"驗證碼無效或已過期\""
|
||||
|
|
@ -697,7 +600,9 @@
|
|||
"500": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "// 伺服器內部錯誤"
|
||||
|
|
@ -734,7 +639,9 @@
|
|||
"400": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "\"請求參數格式錯誤\""
|
||||
|
|
@ -742,7 +649,9 @@
|
|||
"409": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "\"帳號已被註冊\" // 409 Conflict: 資源衝突"
|
||||
|
|
@ -750,7 +659,9 @@
|
|||
"500": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "// 伺服器內部錯誤"
|
||||
|
|
@ -787,7 +698,9 @@
|
|||
"400": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "\"請求參數格式錯誤\""
|
||||
|
|
@ -795,7 +708,9 @@
|
|||
"401": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "\"帳號或密碼錯誤 / 無效的平台 Token\" // 401 Unauthorized"
|
||||
|
|
@ -803,7 +718,9 @@
|
|||
"500": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "// 伺服器內部錯誤"
|
||||
|
|
@ -840,7 +757,9 @@
|
|||
"400": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "\"請求參數格式錯誤\""
|
||||
|
|
@ -848,7 +767,9 @@
|
|||
"401": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "\"無效或已過期的 Refresh Token\" // 401 Unauthorized"
|
||||
|
|
@ -856,7 +777,9 @@
|
|||
"500": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "// 伺服器內部錯誤"
|
||||
|
|
@ -865,72 +788,6 @@
|
|||
"summary": "刷新 Access Token"
|
||||
}
|
||||
},
|
||||
"/api/v1/fileStorage/fileStorage/img/upload": {
|
||||
"post": {
|
||||
"description": "上傳轉成 base64 過後的圖片,建議圖片大小不超過 10MB",
|
||||
"operationId": "fileStorageUploadImgHandler",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "header",
|
||||
"name": "Authorization",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UploadImgReq"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UploadResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"summary": "create - 上傳圖片檔案"
|
||||
}
|
||||
},
|
||||
"/api/v1/fileStorage/fileStorage/video/upload": {
|
||||
"post": {
|
||||
"description": "使用 multipart/form-data 上傳影片檔案,form field 名稱為 'file',注意:大檔案(\u003e50MB)建議使用分片上傳機制",
|
||||
"operationId": "fileStorageUploadVideoHandler",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "header",
|
||||
"name": "Authorization",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UploadResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"summary": "create - 上傳影片檔案"
|
||||
}
|
||||
},
|
||||
"/api/v1/health": {
|
||||
"get": {
|
||||
"description": "檢查系統服務狀態,用於監控和負載均衡器健康檢查。返回系統運行狀態信息。",
|
||||
|
|
@ -974,7 +831,9 @@
|
|||
"401": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "\"未授權或 Token 無效\""
|
||||
|
|
@ -982,7 +841,9 @@
|
|||
"404": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "\"找不到使用者\""
|
||||
|
|
@ -990,7 +851,9 @@
|
|||
"500": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "// 伺服器內部錯誤"
|
||||
|
|
@ -1025,7 +888,9 @@
|
|||
"400": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "\"請求參數格式錯誤\""
|
||||
|
|
@ -1033,7 +898,9 @@
|
|||
"401": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "\"未授權或 Token 無效\""
|
||||
|
|
@ -1041,7 +908,9 @@
|
|||
"500": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "// 伺服器內部錯誤"
|
||||
|
|
@ -1087,7 +956,9 @@
|
|||
"400": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "\"請求參數格式錯誤或新舊密碼不符\""
|
||||
|
|
@ -1095,7 +966,9 @@
|
|||
"401": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "\"未授權或 Token 無效\""
|
||||
|
|
@ -1103,7 +976,9 @@
|
|||
"403": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "\"當前密碼不正確\" // 403 Forbidden"
|
||||
|
|
@ -1111,7 +986,9 @@
|
|||
"500": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "// 伺服器內部錯誤"
|
||||
|
|
@ -1157,7 +1034,9 @@
|
|||
"400": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "\"請求參數格式錯誤\""
|
||||
|
|
@ -1165,7 +1044,9 @@
|
|||
"401": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "\"未授權或 Token 無效\""
|
||||
|
|
@ -1173,7 +1054,9 @@
|
|||
"429": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "\"請求過於頻繁\" // 429 Too Many Requests"
|
||||
|
|
@ -1181,7 +1064,9 @@
|
|||
"500": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "// 伺服器內部錯誤"
|
||||
|
|
@ -1225,7 +1110,9 @@
|
|||
"400": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "\"驗證碼無效或已過期\""
|
||||
|
|
@ -1233,7 +1120,9 @@
|
|||
"401": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "\"未授權或 Token 無效\""
|
||||
|
|
@ -1241,7 +1130,9 @@
|
|||
"500": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "// 伺服器內部錯誤"
|
||||
|
|
@ -1259,7 +1150,7 @@
|
|||
"url": "https://localhost:8888"
|
||||
}
|
||||
],
|
||||
"x-date": "2025-11-12 14:59:58",
|
||||
"x-date": "2025-10-02 23:03:53",
|
||||
"x-description": "This is a go-doc generated swagger file.",
|
||||
"x-generator": "go-doc",
|
||||
"x-github": "https://github.com/danielchan-25/go-doc",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@ syntax = "v1"
|
|||
// ================ 通用響應 ================
|
||||
type (
|
||||
// 成功響應
|
||||
RespOK {}
|
||||
RespOK {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// 分頁響應
|
||||
PagerResp {
|
||||
|
|
@ -13,10 +17,10 @@ type (
|
|||
}
|
||||
|
||||
// 錯誤響應
|
||||
Resp {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
ErrorResp {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Details string `json:"details,omitempty"`
|
||||
Error interface{} `json:"error,omitempty"` // 可選的錯誤信息
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
syntax = "v1"
|
||||
|
||||
// 圖片上傳請求(使用 base64)
|
||||
type UploadImgReq {
|
||||
Authorization
|
||||
Content string `json:"content" validate:"required"` // base64 編碼的圖片內容
|
||||
}
|
||||
|
||||
// 影片上傳請求(使用 multipart/form-data 文件上傳)
|
||||
// 注意:文件需要在 handler 中通過 r.FormFile("file") 獲取
|
||||
type UploadVideoReq {
|
||||
Authorization
|
||||
}
|
||||
|
||||
// 統一的上傳響應
|
||||
type UploadResp {
|
||||
FileUrl string `json:"file_url"` // 文件訪問 URL
|
||||
FileSize int64 `json:"file_size,optional"` // 文件大小(bytes)
|
||||
MimeType string `json:"mime_type,optional"` // MIME 類型
|
||||
}
|
||||
|
||||
@server(
|
||||
group: fileStorage
|
||||
prefix: /api/v1/fileStorage
|
||||
schemes: https
|
||||
timeout: 300s // 影片上傳可能需要更長時間
|
||||
middleware: AuthMiddleware
|
||||
)
|
||||
|
||||
service gateway {
|
||||
/* @respdoc-400 (BaseResponse) // 輸入的參數錯誤 */
|
||||
/* @respdoc-403 (BaseResponse) // 無效的Token */
|
||||
/* @respdoc-413 (BaseResponse) // 文件大小超過限制 */
|
||||
/* @respdoc-500 (BaseResponse) // 伺服器出錯 */
|
||||
@doc(
|
||||
summary: "create - 上傳圖片檔案"
|
||||
description: "上傳轉成 base64 過後的圖片,建議圖片大小不超過 10MB"
|
||||
)
|
||||
@handler UploadImgHandler
|
||||
post /fileStorage/img/upload (UploadImgReq) returns (UploadResp)
|
||||
|
||||
/* @respdoc-400 (BaseResponse) // 輸入的參數錯誤 */
|
||||
/* @respdoc-403 (BaseResponse) // 無效的Token */
|
||||
/* @respdoc-413 (BaseResponse) // 文件大小超過限制 */
|
||||
/* @respdoc-500 (BaseResponse) // 伺服器出錯 */
|
||||
@doc(
|
||||
summary: "create - 上傳影片檔案"
|
||||
description: "使用 multipart/form-data 上傳影片檔案,form field 名稱為 'file',注意:大檔案(>50MB)建議使用分片上傳機制"
|
||||
)
|
||||
@handler UploadVideoHandler
|
||||
post /fileStorage/video/upload (UploadVideoReq) returns (UploadResp)
|
||||
}
|
||||
|
|
@ -16,6 +16,5 @@ import (
|
|||
"common.api"
|
||||
"ping.api"
|
||||
"member.api"
|
||||
"file_storage.api"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ type (
|
|||
|
||||
// RequestPasswordResetReq 請求發送「忘記密碼」的驗證碼
|
||||
RequestPasswordResetReq {
|
||||
Identifier string `json:"identifier" validate:"required"` // 使用者帳號 (信箱或手機)
|
||||
Identifier string `json:"identifier" validate:"required,email|phone"` // 使用者帳號 (信箱或手機)
|
||||
AccountType string `json:"account_type" validate:"required,oneof=email phone"`
|
||||
}
|
||||
|
||||
|
|
@ -94,9 +94,13 @@ type (
|
|||
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 更新會員資訊的請求結構
|
||||
|
|
@ -127,43 +131,16 @@ type (
|
|||
// RequestVerificationCodeReq 請求發送驗證碼
|
||||
RequestVerificationCodeReq {
|
||||
// 驗證目的:'email_verification' 或 'phone_verification'
|
||||
Account string `json:"account" validate:"required`
|
||||
Purpose string `json:"purpose" validate:"required,oneof=email_verification phone_verification"`
|
||||
Authorization
|
||||
}
|
||||
|
||||
// SubmitVerificationCodeReq 提交驗證碼以完成驗證
|
||||
SubmitVerificationCodeReq {
|
||||
Account string `json:"account" validate:"required`
|
||||
Purpose string `json:"purpose" validate:"required,oneof=email_verification phone_verification"`
|
||||
VerifyCode string `json:"verify_code" validate:"required,len=6"`
|
||||
Authorization
|
||||
}
|
||||
|
||||
// MyInfo 用於獲取會員資訊的標準響應結構
|
||||
MyInfo {
|
||||
Platform string `json:"platform"` // 註冊平台
|
||||
UID string `json:"uid"` // 用戶 UID
|
||||
AvatarURL *string `json:"avatar_url,omitempty"` // 頭像 URL
|
||||
FullName *string `json:"full_name,omitempty"` // 用戶全名
|
||||
Nickname *string `json:"nickname,omitempty"` // 暱稱
|
||||
GenderCode *string `json:"gender_code,omitempty"` // 性別代碼
|
||||
Birthdate *string `json:"birthdate,omitempty"` // 生日 (格式: 1993-04-17)
|
||||
PhoneNumber *string `json:"phone_number,omitempty"` // 電話
|
||||
IsPhoneVerified *bool `json:"is_phone_verified,omitempty"` // 手機是否已驗證
|
||||
Email *string `json:"email,omitempty"` // 信箱
|
||||
IsEmailVerified *bool `json:"is_email_verified,omitempty"` // 信箱是否已驗證
|
||||
Address *string `json:"address,omitempty"` // 地址
|
||||
UserStatus string `json:"user_status,omitempty"` // 用戶狀態
|
||||
PreferredLanguage string `json:"preferred_language,omitempty"` // 偏好語言
|
||||
Currency string `json:"currency,omitempty"` // 偏好幣種
|
||||
AlarmCategory string `json:"alarm_category,omitempty"` // 告警狀態
|
||||
PostCode *string `json:"post_code,omitempty"` // 郵遞區號
|
||||
Carrier *string `json:"carrier,omitempty"` // 載具
|
||||
Role string `json:"role"` // 角色
|
||||
UpdateAt string `json:"update_at"`
|
||||
CreateAt string `json:"create_at"`
|
||||
}
|
||||
)
|
||||
|
||||
// =================================================================
|
||||
|
|
@ -274,7 +251,7 @@ service gateway {
|
|||
@respdoc-500 (ErrorResp) // 伺服器內部錯誤
|
||||
*/
|
||||
@handler getUserInfo
|
||||
get /me (Authorization) returns (MyInfo)
|
||||
get /me (Authorization) returns (UserInfoResp)
|
||||
|
||||
@doc(
|
||||
summary: "更新當前登入的會員資訊"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
use digimon_member
|
||||
db.count.dropIndex("name_1");
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
use digimon_member
|
||||
db.count.createIndex({ "name": 1 }, { unique: true });
|
||||
|
|
@ -1 +0,0 @@
|
|||
db.count.dropIndex("name_1");
|
||||
|
|
@ -1 +0,0 @@
|
|||
db.count.createIndex({ "name": 1 }, { unique: true });
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
db.account.dropIndex("login_id_1_platform_1");
|
||||
db.account.dropIndex("create_at_1");
|
||||
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
use digimon_member;
|
||||
db.account.createIndex({ "login_id": 1, "platform": 1}, {unique: true})
|
||||
db.account.createIndex({"create_at": 1})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
db.account_uid_binding.dropIndex("login_id_1");
|
||||
db.account_uid_binding.dropIndex("uid_1");
|
||||
db.account_uid_binding.dropIndex("create_at_1");
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
use digimon_member;
|
||||
db.account_uid_binding.createIndex({"login_id": 1}, {unique: true})
|
||||
db.account_uid_binding.createIndex({"uid": 1})
|
||||
db.account_uid_binding.createIndex({"create_at": 1})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
db.user_info.dropIndex("uid_1");
|
||||
db.user_info.dropIndex("create_at_1");
|
||||
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
use digimon_member;
|
||||
db.user_info.createIndex({"uid": 1},{unique: true})
|
||||
db.user_info.createIndex({"create_at": 1})
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
use digimon_product;
|
||||
|
||||
# 精確查詢索引(針對 owner_uid, is_published, is_visible, created_at)
|
||||
db.product.createIndex({"owner_uid": 1, "is_published": 1, "is_visible": 1, "created_at": -1})
|
||||
|
||||
# 範圍查詢索引(針對 start_time, end_time, created_at)
|
||||
db.product.createIndex({"start_time": 1, "end_time": 1, "created_at": -1})
|
||||
|
||||
# 金額範圍索引(針對 target_amount, created_at)
|
||||
db.product.createIndex({"target_amount": 1, "created_at": -1})
|
||||
|
||||
# 成功排序專用索引(針對 finished, updated_at)
|
||||
db.product.createIndex({"finished": 1, "updated_at": -1})
|
||||
|
||||
# URL 精確查詢索引(針對 url_slug)
|
||||
db.product.createIndex({"url_slug": 1})
|
||||
|
||||
# 基礎排序索引(針對 created_at 單列)
|
||||
db.product.createIndex({"created_at": -1})
|
||||
|
||||
# URL 精確查詢索引(針對 url_slug)
|
||||
db.product.createIndex({"category": 1})
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
use digimon_product;
|
||||
|
||||
# 精確查詢與範圍條件組合索引
|
||||
db.product_item.createIndex({"reference_id": 1}) # 精確匹配 reference_id
|
||||
db.product_item.createIndex({"sale_start_time": 1, "sale_end_time": 1}) # 範圍查詢索引,提升 SaleStartTime 與 SaleEndTime 查詢效率
|
||||
db.product_item.createIndex({"status": 1}) # 精確查詢 status
|
||||
|
||||
# 排序索引
|
||||
db.product_item.createIndex({"created_at": -1}) # 用於按 created_at 倒序排序
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
use digimon_product;
|
||||
|
||||
# 精確查詢與範圍條件組合索引
|
||||
db.supplementary_info.createIndex({"reference_id": 1}) # 精確匹配 reference_id
|
||||
db.supplementary_info.createIndex({"info_type": 1}) # 精確查詢 status
|
||||
|
||||
# 排序索引
|
||||
db.supplementary_info.createIndex({"created_at": -1}) # 用於按 created_at 倒序排序
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
use digimon_cart;
|
||||
|
||||
# 精確查詢與範圍條件組合索引
|
||||
db.cart.createIndex({ "uid": 1, "product_id": 1 }, { unique: true })
|
||||
# 排序索引
|
||||
db.supplementary_info.createIndex({"created_at": -1}) # 用於按 created_at 倒序排序
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
db.permission.dropIndex("name_1");
|
||||
db.permission.dropIndex("http_path_1_http_method_1");
|
||||
db.permission.dropIndex("status_1");
|
||||
db.permission.dropIndex("parent_id_1");
|
||||
db.permission.dropIndex("type_1_status_1");
|
||||
db.permission.dropIndex("create_time_1");
|
||||
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
// 1. 唯一索引:權限名稱必須唯一
|
||||
db.permission.createIndex({"name": 1}, {unique: true});
|
||||
|
||||
// 2. 複合唯一稀疏索引:HTTP 路徑 + 方法的組合必須唯一(用於 API 權限)
|
||||
db.permission.createIndex({"http_path": 1, "http_method": 1}, {unique: true, sparse: true});
|
||||
|
||||
// 3. 查詢索引:按狀態查詢
|
||||
db.permission.createIndex({"status": 1});
|
||||
|
||||
// 4. 查詢索引:按父 ID 查詢(用於獲取子權限)
|
||||
db.permission.createIndex({"parent_id": 1});
|
||||
|
||||
// 5. 複合索引:按類型和狀態查詢(常用組合)
|
||||
db.permission.createIndex({"type": 1, "status": 1});
|
||||
|
||||
// 6. 時間戳索引:用於排序和時間範圍查詢
|
||||
db.permission.createIndex({"create_time": 1});
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
db.role.dropIndex("uid_1");
|
||||
db.role.dropIndex("client_id_1_name_1");
|
||||
db.role.dropIndex("client_id_1");
|
||||
db.role.dropIndex("status_1");
|
||||
db.role.dropIndex("client_id_1_status_1");
|
||||
db.role.dropIndex("create_time_1");
|
||||
db.role.dropIndex("update_time_-1");
|
||||
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
// 1. 唯一索引:角色 UID 必須唯一
|
||||
db.role.createIndex({"uid": 1}, {unique: true});
|
||||
|
||||
// 2. 複合唯一索引:同一個 Client 下角色名稱必須唯一
|
||||
db.role.createIndex({"client_id": 1, "name": 1}, {unique: true});
|
||||
|
||||
// 3. 查詢索引:按 Client ID 查詢
|
||||
db.role.createIndex({"client_id": 1});
|
||||
|
||||
// 4. 查詢索引:按狀態查詢
|
||||
db.role.createIndex({"status": 1});
|
||||
|
||||
// 5. 複合索引:按 Client ID 和狀態查詢(常用組合)
|
||||
db.role.createIndex({"client_id": 1, "status": 1});
|
||||
|
||||
// 6. 時間戳索引:用於排序和時間範圍查詢
|
||||
db.role.createIndex({"create_time": 1});
|
||||
|
||||
// 7. 時間戳索引:用於更新時間排序
|
||||
db.role.createIndex({"update_time": -1});
|
||||
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
db.role_permission.dropIndex("role_id_1_permission_id_1");
|
||||
db.role_permission.dropIndex("role_id_1");
|
||||
db.role_permission.dropIndex("permission_id_1");
|
||||
db.role_permission.dropIndex("permission_id_1_status_1");
|
||||
db.role_permission.dropIndex("create_time_1");
|
||||
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
// 1. 複合唯一索引:角色 ID + 權限 ID 的組合必須唯一(避免重複關聯)
|
||||
db.role_permission.createIndex({"role_id": 1, "permission_id": 1}, {unique: true});
|
||||
|
||||
// 2. 查詢索引:按角色 ID 查詢(用於獲取某角色的所有權限)
|
||||
db.role_permission.createIndex({"role_id": 1});
|
||||
|
||||
// 3. 查詢索引:按權限 ID 查詢(用於獲取擁有某權限的所有角色)
|
||||
db.role_permission.createIndex({"permission_id": 1});
|
||||
|
||||
// 4. 複合索引:按權限 ID 和狀態查詢
|
||||
db.role_permission.createIndex({"permission_id": 1, "status": 1});
|
||||
|
||||
// 5. 時間戳索引:用於排序和時間範圍查詢
|
||||
db.role_permission.createIndex({"create_time": 1});
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
db.user_role.dropIndex("uid_1");
|
||||
db.user_role.dropIndex("role_id_1");
|
||||
db.user_role.dropIndex("brand_1");
|
||||
db.user_role.dropIndex("status_1");
|
||||
db.user_role.dropIndex("brand_1_role_id_1");
|
||||
db.user_role.dropIndex("brand_1_status_1");
|
||||
db.user_role.dropIndex("create_time_1");
|
||||
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
// 1. 唯一索引:使用者 UID 必須唯一(一個使用者只能有一個角色)
|
||||
db.user_role.createIndex({"uid": 1}, {unique: true});
|
||||
|
||||
// 2. 查詢索引:按角色 ID 查詢(用於獲取某角色的所有使用者)
|
||||
db.user_role.createIndex({"role_id": 1});
|
||||
|
||||
// 3. 查詢索引:按 Brand 查詢
|
||||
db.user_role.createIndex({"brand": 1});
|
||||
|
||||
// 4. 查詢索引:按狀態查詢
|
||||
db.user_role.createIndex({"status": 1});
|
||||
|
||||
// 5. 複合索引:按 Brand 和角色 ID 查詢(常用組合)
|
||||
db.user_role.createIndex({"brand": 1, "role_id": 1});
|
||||
|
||||
// 6. 複合索引:按 Brand 和狀態查詢
|
||||
db.user_role.createIndex({"brand": 1, "status": 1});
|
||||
|
||||
// 7. 時間戳索引:用於排序和時間範圍查詢
|
||||
db.user_role.createIndex({"create_time": 1});
|
||||
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
db.role.deleteMany({
|
||||
"uid": { "$in": ["ADMIN", "OPERATOR", "USER"] }
|
||||
});
|
||||
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
// 插入初始角色數據
|
||||
db.role.insertMany([
|
||||
{
|
||||
"client_id": 1,
|
||||
"uid": "ADMIN",
|
||||
"name": "管理員",
|
||||
"status": 1,
|
||||
"create_time": NumberLong(1728745200),
|
||||
"update_time": NumberLong(1728745200)
|
||||
},
|
||||
{
|
||||
"client_id": 1,
|
||||
"uid": "OPERATOR",
|
||||
"name": "操作員",
|
||||
"status": 1,
|
||||
"create_time": NumberLong(1728745200),
|
||||
"update_time": NumberLong(1728745200)
|
||||
},
|
||||
{
|
||||
"client_id": 1,
|
||||
"uid": "USER",
|
||||
"name": "一般使用者",
|
||||
"status": 1,
|
||||
"create_time": NumberLong(1728745200),
|
||||
"update_time": NumberLong(1728745200)
|
||||
},
|
||||
{
|
||||
"client_id": 1,
|
||||
"uid": "PLAYER",
|
||||
"name": "陪玩專員",
|
||||
"status": 1,
|
||||
"create_time": NumberLong(1728745200),
|
||||
"update_time": NumberLong(1728745200)
|
||||
}
|
||||
]);
|
||||
|
||||
// 注意:索引應該在 2025100900000002_role.up.txt 中建立
|
||||
// 這裡只插入初始數據
|
||||
|
||||
|
|
@ -1,457 +0,0 @@
|
|||
# MongoDB 資料庫結構文檔
|
||||
|
||||
本文檔描述所有 MongoDB 集合(Collection)的結構定義。
|
||||
|
||||
## 目錄
|
||||
|
||||
- [Member 模組](#member-模組)
|
||||
- [account](#account)
|
||||
- [account_uid_binding](#account_uid_binding)
|
||||
- [user_info](#user_info)
|
||||
- [count](#count)
|
||||
- [Permission 模組](#permission-模組)
|
||||
- [permission](#permission)
|
||||
- [role](#role)
|
||||
- [role_permission](#role_permission)
|
||||
- [user_role](#user_role)
|
||||
|
||||
---
|
||||
|
||||
## Member 模組
|
||||
|
||||
### account
|
||||
|
||||
**集合名稱**: `account`
|
||||
|
||||
**說明**: 用戶帳號認證資訊,儲存登入憑證、加密密碼和平台相關資料。
|
||||
|
||||
#### 欄位定義
|
||||
|
||||
| 欄位名稱 | 類型 | 必填 | 說明 |
|
||||
|---------|------|------|------|
|
||||
| `_id` | ObjectID | 是 | MongoDB 自動生成的唯一識別碼 |
|
||||
| `login_id` | string | 是 | 唯一登入識別碼(email、phone、username) |
|
||||
| `token` | string | 是 | 加密後的密碼或平台特定的 token |
|
||||
| `platform` | int8 | 是 | 平台類型:1=credentials, 2=google, 3=line, 4=apple |
|
||||
| `create_at` | int64 | 否 | 建立時間(Unix 時間戳,納秒) |
|
||||
| `update_at` | int64 | 否 | 更新時間(Unix 時間戳,納秒) |
|
||||
|
||||
#### 索引
|
||||
|
||||
1. **複合唯一索引**: `{login_id: 1, platform: 1}`
|
||||
- 確保同一平台下 login_id 唯一
|
||||
2. **時間索引**: `{create_at: 1}`
|
||||
- 用於按建立時間排序和查詢
|
||||
|
||||
#### Platform 枚舉值
|
||||
|
||||
- `1` = credentials (Digimon 平台)
|
||||
- `2` = google
|
||||
- `3` = line
|
||||
- `4` = apple
|
||||
|
||||
#### AccountType 枚舉值
|
||||
|
||||
- `1` = phone (手機)
|
||||
- `2` = email (信箱)
|
||||
- `3` = platform (自定義帳號)
|
||||
|
||||
#### AlarmType 枚舉值
|
||||
|
||||
- `0` = uninitialized (未初始化)
|
||||
- `1` = no_alert (未告警)
|
||||
- `2` = system_alert (系統告警中)
|
||||
|
||||
---
|
||||
|
||||
### account_uid_binding
|
||||
|
||||
**集合名稱**: `account_uid_binding`
|
||||
|
||||
**說明**: 帳號與 UID 的綁定關係,用於將多個帳號(不同平台)綁定到同一個用戶。
|
||||
|
||||
#### 欄位定義
|
||||
|
||||
| 欄位名稱 | 類型 | 必填 | 說明 |
|
||||
|---------|------|------|------|
|
||||
| `_id` | ObjectID | 是 | MongoDB 自動生成的唯一識別碼 |
|
||||
| `login_id` | string | 是 | 登入識別碼 |
|
||||
| `uid` | string | 是 | 用戶唯一識別碼 |
|
||||
| `type` | int32 | 是 | 帳號類型:1=phone, 2=email, 3=platform |
|
||||
| `create_at` | int64 | 否 | 建立時間(Unix 時間戳,納秒) |
|
||||
| `update_at` | int64 | 否 | 更新時間(Unix 時間戳,納秒) |
|
||||
|
||||
#### 索引
|
||||
|
||||
1. **唯一索引**: `{login_id: 1}`
|
||||
- 確保 login_id 唯一
|
||||
2. **索引**: `{uid: 1}`
|
||||
- 用於查詢某用戶的所有帳號綁定
|
||||
3. **時間索引**: `{create_at: 1}`
|
||||
- 用於按建立時間排序
|
||||
|
||||
---
|
||||
|
||||
### user_info
|
||||
|
||||
**集合名稱**: `user_info`
|
||||
|
||||
**說明**: 用戶個人資料,包含詳細的用戶資訊,與認證憑證分離。
|
||||
|
||||
#### 欄位定義
|
||||
|
||||
| 欄位名稱 | 類型 | 必填 | 說明 |
|
||||
|---------|------|------|------|
|
||||
| `_id` | ObjectID | 是 | MongoDB 自動生成的唯一識別碼 |
|
||||
| `uid` | string | 是 | 用戶唯一識別碼 |
|
||||
| `avatar_url` | string | 否 | 用戶頭像 URL |
|
||||
| `full_name` | string | 否 | 用戶全名 |
|
||||
| `nickname` | string | 否 | 用戶暱稱 |
|
||||
| `gender_code` | int64 | 否 | 性別代碼 |
|
||||
| `birthdate` | int64 | 否 | 生日(格式:19930417) |
|
||||
| `address` | string | 否 | 用戶地址 |
|
||||
| `alarm_category` | int32 | 是 | 通知設定類型:0=未初始化, 1=未告警, 2=系統告警中 |
|
||||
| `user_status` | int32 | 是 | 用戶帳號狀態:0=未初始化, 1=未驗證, 2=啟用中, 3=停權中 |
|
||||
| `preferred_language` | string | 是 | 偏好語言 |
|
||||
| `currency` | string | 是 | 偏好貨幣 |
|
||||
| `phone_number` | string | 否 | 電話號碼(驗證後顯示) |
|
||||
| `email` | string | 否 | 電子郵件(驗證後顯示) |
|
||||
| `create_at` | int64 | 否 | 建立時間(Unix 時間戳,納秒) |
|
||||
| `update_at` | int64 | 否 | 更新時間(Unix 時間戳,納秒) |
|
||||
|
||||
#### 索引
|
||||
|
||||
1. **唯一索引**: `{uid: 1}`
|
||||
- 確保 uid 唯一
|
||||
2. **時間索引**: `{create_at: 1}`
|
||||
- 用於按建立時間排序
|
||||
|
||||
#### UserStatus 枚舉值
|
||||
|
||||
- `0` = uninitialized (未初始化)
|
||||
- `1` = unverified (尚未驗證)
|
||||
- `2` = active (啟用中)
|
||||
- `3` = suspended (停權中)
|
||||
|
||||
---
|
||||
|
||||
### count
|
||||
|
||||
**集合名稱**: `count`
|
||||
|
||||
**說明**: 自增 ID 計數器,用於生成唯一序號。
|
||||
|
||||
#### 欄位定義
|
||||
|
||||
| 欄位名稱 | 類型 | 必填 | 說明 |
|
||||
|---------|------|------|------|
|
||||
| `_id` | ObjectID | 是 | MongoDB 自動生成的唯一識別碼 |
|
||||
| `name` | string | 是 | 計數器名稱(唯一) |
|
||||
| `counter` | uint64 | 是 | 當前計數值 |
|
||||
| `create_at` | int64 | 否 | 建立時間(Unix 時間戳,納秒) |
|
||||
| `update_at` | int64 | 否 | 更新時間(Unix 時間戳,納秒) |
|
||||
|
||||
#### 索引
|
||||
|
||||
1. **唯一索引**: `{name: 1}`
|
||||
- 確保計數器名稱唯一
|
||||
|
||||
---
|
||||
|
||||
## Permission 模組
|
||||
|
||||
### permission
|
||||
|
||||
**集合名稱**: `permission`
|
||||
|
||||
**說明**: 權限實體,定義系統中的各種權限,支援階層結構和 API 權限。
|
||||
|
||||
#### 欄位定義
|
||||
|
||||
| 欄位名稱 | 類型 | 必填 | 說明 |
|
||||
|---------|------|------|------|
|
||||
| `_id` | ObjectID | 是 | MongoDB 自動生成的唯一識別碼 |
|
||||
| `parent_id` | ObjectID | 否 | 父權限 ID(用於階層結構) |
|
||||
| `name` | string | 是 | 權限名稱(唯一) |
|
||||
| `http_method` | string | 否 | HTTP 方法(GET、POST、PUT、DELETE 等) |
|
||||
| `http_path` | string | 否 | HTTP 路徑(API 端點) |
|
||||
| `status` | int8 | 是 | 權限狀態:0=停用, 1=啟用, 2=已刪除 |
|
||||
| `type` | int8 | 是 | 權限類型:1=後台權限, 2=前台權限 |
|
||||
| `create_time` | int64 | 是 | 建立時間(Unix 時間戳,秒) |
|
||||
| `update_time` | int64 | 是 | 更新時間(Unix 時間戳,秒) |
|
||||
|
||||
#### 索引
|
||||
|
||||
1. **唯一索引**: `{name: 1}`
|
||||
- 確保權限名稱唯一
|
||||
2. **複合唯一稀疏索引**: `{http_path: 1, http_method: 1}` (sparse: true)
|
||||
- 確保 API 權限的路徑和方法組合唯一(只索引存在這些欄位的文檔)
|
||||
3. **查詢索引**: `{status: 1}`
|
||||
- 用於查詢啟用/停用的權限
|
||||
4. **查詢索引**: `{parent_id: 1}`
|
||||
- 用於查詢子權限
|
||||
5. **複合索引**: `{type: 1, status: 1}`
|
||||
- 用於按類型和狀態組合查詢
|
||||
6. **時間戳索引**: `{create_time: 1}`
|
||||
- 用於排序和時間範圍查詢
|
||||
|
||||
#### RecordState 枚舉值
|
||||
|
||||
- `0` = inactive (停用)
|
||||
- `1` = active (啟用)
|
||||
- `2` = deleted (已刪除)
|
||||
|
||||
#### Type 枚舉值
|
||||
|
||||
- `1` = backend (後台權限)
|
||||
- `2` = frontend (前台權限)
|
||||
|
||||
---
|
||||
|
||||
### role
|
||||
|
||||
**集合名稱**: `role`
|
||||
|
||||
**說明**: 角色實體,定義系統中的角色,每個角色可以擁有多個權限。
|
||||
|
||||
#### 欄位定義
|
||||
|
||||
| 欄位名稱 | 類型 | 必填 | 說明 |
|
||||
|---------|------|------|------|
|
||||
| `_id` | ObjectID | 是 | MongoDB 自動生成的唯一識別碼 |
|
||||
| `client_id` | int | 是 | 客戶端 ID |
|
||||
| `uid` | string | 是 | 角色唯一識別碼(唯一) |
|
||||
| `name` | string | 是 | 角色名稱 |
|
||||
| `status` | int8 | 是 | 角色狀態:0=停用, 1=啟用, 2=已刪除 |
|
||||
| `create_time` | int64 | 是 | 建立時間(Unix 時間戳,秒) |
|
||||
| `update_time` | int64 | 是 | 更新時間(Unix 時間戳,秒) |
|
||||
|
||||
#### 索引
|
||||
|
||||
1. **唯一索引**: `{uid: 1}`
|
||||
- 確保角色 UID 唯一
|
||||
2. **複合唯一索引**: `{client_id: 1, name: 1}`
|
||||
- 確保同一客戶端下角色名稱唯一
|
||||
3. **查詢索引**: `{client_id: 1}`
|
||||
- 用於按客戶端查詢
|
||||
4. **查詢索引**: `{status: 1}`
|
||||
- 用於按狀態查詢
|
||||
5. **複合索引**: `{client_id: 1, status: 1}`
|
||||
- 用於按客戶端和狀態組合查詢
|
||||
6. **時間戳索引**: `{create_time: 1}`
|
||||
- 用於排序和時間範圍查詢
|
||||
7. **時間戳索引**: `{update_time: -1}` (降序)
|
||||
- 用於按更新時間排序
|
||||
|
||||
#### 預設角色數據
|
||||
|
||||
系統會自動插入以下初始角色:
|
||||
|
||||
- `ADMIN` - 管理員
|
||||
- `OPERATOR` - 操作員
|
||||
- `USER` - 一般使用者
|
||||
- `PLAYER` - 陪玩專員
|
||||
|
||||
---
|
||||
|
||||
### role_permission
|
||||
|
||||
**集合名稱**: `role_permission`
|
||||
|
||||
**說明**: 角色權限關聯表,建立角色與權限的多對多關係。
|
||||
|
||||
#### 欄位定義
|
||||
|
||||
| 欄位名稱 | 類型 | 必填 | 說明 |
|
||||
|---------|------|------|------|
|
||||
| `_id` | ObjectID | 是 | MongoDB 自動生成的唯一識別碼 |
|
||||
| `role_id` | ObjectID | 是 | 角色 ID |
|
||||
| `permission_id` | ObjectID | 是 | 權限 ID |
|
||||
| `create_time` | int64 | 是 | 建立時間(Unix 時間戳,秒) |
|
||||
| `update_time` | int64 | 是 | 更新時間(Unix 時間戳,秒) |
|
||||
|
||||
#### 索引
|
||||
|
||||
1. **複合唯一索引**: `{role_id: 1, permission_id: 1}`
|
||||
- 確保角色和權限的組合唯一(避免重複關聯)
|
||||
2. **查詢索引**: `{role_id: 1}`
|
||||
- 用於查詢某角色的所有權限
|
||||
3. **查詢索引**: `{permission_id: 1}`
|
||||
- 用於查詢擁有某權限的所有角色
|
||||
4. **複合索引**: `{permission_id: 1, status: 1}`
|
||||
- 用於按權限和狀態組合查詢
|
||||
5. **時間戳索引**: `{create_time: 1}`
|
||||
- 用於排序和時間範圍查詢
|
||||
|
||||
---
|
||||
|
||||
### user_role
|
||||
|
||||
**集合名稱**: `user_role`
|
||||
|
||||
**說明**: 用戶角色關聯表,建立用戶與角色的關係(一個用戶只能有一個角色)。
|
||||
|
||||
#### 欄位定義
|
||||
|
||||
| 欄位名稱 | 類型 | 必填 | 說明 |
|
||||
|---------|------|------|------|
|
||||
| `_id` | ObjectID | 是 | MongoDB 自動生成的唯一識別碼 |
|
||||
| `brand` | string | 是 | 品牌識別碼 |
|
||||
| `uid` | string | 是 | 用戶唯一識別碼(唯一) |
|
||||
| `role_id` | string | 是 | 角色 ID(角色 UID) |
|
||||
| `status` | int8 | 是 | 狀態:0=停用, 1=啟用, 2=已刪除 |
|
||||
| `create_time` | int64 | 是 | 建立時間(Unix 時間戳,秒) |
|
||||
| `update_time` | int64 | 是 | 更新時間(Unix 時間戳,秒) |
|
||||
|
||||
#### 索引
|
||||
|
||||
1. **唯一索引**: `{uid: 1}`
|
||||
- 確保一個用戶只能有一個角色
|
||||
2. **查詢索引**: `{role_id: 1}`
|
||||
- 用於查詢某角色的所有用戶
|
||||
3. **查詢索引**: `{brand: 1}`
|
||||
- 用於按品牌查詢
|
||||
4. **查詢索引**: `{status: 1}`
|
||||
- 用於按狀態查詢
|
||||
5. **複合索引**: `{brand: 1, role_id: 1}`
|
||||
- 用於按品牌和角色組合查詢
|
||||
6. **複合索引**: `{brand: 1, status: 1}`
|
||||
- 用於按品牌和狀態組合查詢
|
||||
7. **時間戳索引**: `{create_time: 1}`
|
||||
- 用於排序和時間範圍查詢
|
||||
|
||||
---
|
||||
|
||||
## 資料類型說明
|
||||
|
||||
### 時間戳記
|
||||
|
||||
- **Member 模組**: 使用 `int64` 類型,單位為納秒(nanoseconds),欄位名為 `create_at`、`update_at`
|
||||
- **Permission 模組**: 使用 `int64` 類型,單位為秒(seconds),欄位名為 `create_time`、`update_time`
|
||||
|
||||
### 枚舉類型
|
||||
|
||||
#### Platform (平台類型)
|
||||
- `1` = credentials (Digimon 平台)
|
||||
- `2` = google
|
||||
- `3` = line
|
||||
- `4` = apple
|
||||
|
||||
#### Status (帳號狀態)
|
||||
- `0` = uninitialized (未初始化)
|
||||
- `1` = unverified (尚未驗證)
|
||||
- `2` = active (啟用中)
|
||||
- `3` = suspended (停權中)
|
||||
|
||||
#### RecordState (記錄狀態)
|
||||
- `0` = inactive (停用)
|
||||
- `1` = active (啟用)
|
||||
- `2` = deleted (已刪除)
|
||||
|
||||
#### Type (權限類型)
|
||||
- `1` = backend (後台權限)
|
||||
- `2` = frontend (前台權限)
|
||||
|
||||
#### AccountType (帳號類型)
|
||||
- `1` = phone (手機)
|
||||
- `2` = email (信箱)
|
||||
- `3` = platform (自定義帳號)
|
||||
|
||||
#### AlarmType (通知類型)
|
||||
- `0` = uninitialized (未初始化)
|
||||
- `1` = no_alert (未告警)
|
||||
- `2` = system_alert (系統告警中)
|
||||
|
||||
---
|
||||
|
||||
## 索引總覽
|
||||
|
||||
### Member 模組索引
|
||||
|
||||
| 集合 | 索引類型 | 索引欄位 | 唯一 | 說明 |
|
||||
|------|---------|---------|------|------|
|
||||
| account | 複合唯一 | login_id, platform | ✅ | 確保同一平台下 login_id 唯一 |
|
||||
| account | 單一 | create_at | ❌ | 時間排序 |
|
||||
| account_uid_binding | 唯一 | login_id | ✅ | 確保 login_id 唯一 |
|
||||
| account_uid_binding | 單一 | uid | ❌ | 查詢用戶的所有帳號 |
|
||||
| account_uid_binding | 單一 | create_at | ❌ | 時間排序 |
|
||||
| user_info | 唯一 | uid | ✅ | 確保 uid 唯一 |
|
||||
| user_info | 單一 | create_at | ❌ | 時間排序 |
|
||||
| count | 唯一 | name | ✅ | 確保計數器名稱唯一 |
|
||||
|
||||
### Permission 模組索引
|
||||
|
||||
| 集合 | 索引類型 | 索引欄位 | 唯一 | 稀疏 | 說明 |
|
||||
|------|---------|---------|------|------|------|
|
||||
| permission | 唯一 | name | ✅ | ❌ | 權限名稱唯一 |
|
||||
| permission | 複合唯一稀疏 | http_path, http_method | ✅ | ✅ | API 權限唯一 |
|
||||
| permission | 單一 | status | ❌ | ❌ | 狀態查詢 |
|
||||
| permission | 單一 | parent_id | ❌ | ❌ | 子權限查詢 |
|
||||
| permission | 複合 | type, status | ❌ | ❌ | 類型和狀態組合查詢 |
|
||||
| permission | 單一 | create_time | ❌ | ❌ | 時間排序 |
|
||||
| role | 唯一 | uid | ✅ | ❌ | 角色 UID 唯一 |
|
||||
| role | 複合唯一 | client_id, name | ✅ | ❌ | 同客戶端下角色名稱唯一 |
|
||||
| role | 單一 | client_id | ❌ | ❌ | 客戶端查詢 |
|
||||
| role | 單一 | status | ❌ | ❌ | 狀態查詢 |
|
||||
| role | 複合 | client_id, status | ❌ | ❌ | 客戶端和狀態組合查詢 |
|
||||
| role | 單一 | create_time | ❌ | ❌ | 時間排序 |
|
||||
| role | 單一 | update_time | ❌ | ❌ | 更新時間排序(降序) |
|
||||
| role_permission | 複合唯一 | role_id, permission_id | ✅ | ❌ | 避免重複關聯 |
|
||||
| role_permission | 單一 | role_id | ❌ | ❌ | 角色權限查詢 |
|
||||
| role_permission | 單一 | permission_id | ❌ | ❌ | 權限角色查詢 |
|
||||
| role_permission | 複合 | permission_id, status | ❌ | ❌ | 權限和狀態組合查詢 |
|
||||
| role_permission | 單一 | create_time | ❌ | ❌ | 時間排序 |
|
||||
| user_role | 唯一 | uid | ✅ | ❌ | 一個用戶一個角色 |
|
||||
| user_role | 單一 | role_id | ❌ | ❌ | 角色用戶查詢 |
|
||||
| user_role | 單一 | brand | ❌ | ❌ | 品牌查詢 |
|
||||
| user_role | 單一 | status | ❌ | ❌ | 狀態查詢 |
|
||||
| user_role | 複合 | brand, role_id | ❌ | ❌ | 品牌和角色組合查詢 |
|
||||
| user_role | 複合 | brand, status | ❌ | ❌ | 品牌和狀態組合查詢 |
|
||||
| user_role | 單一 | create_time | ❌ | ❌ | 時間排序 |
|
||||
|
||||
---
|
||||
|
||||
## 關聯關係
|
||||
|
||||
### Member 模組
|
||||
|
||||
```
|
||||
account (1) ──< (N) account_uid_binding (N) >── (1) user_info
|
||||
```
|
||||
|
||||
- 一個 `account` 可以對應一個 `account_uid_binding`
|
||||
- 一個 `user_info` 可以對應多個 `account_uid_binding`(多個帳號綁定到同一用戶)
|
||||
|
||||
### Permission 模組
|
||||
|
||||
```
|
||||
role (N) <──> (N) permission (透過 role_permission)
|
||||
user_role (N) >── (1) role
|
||||
```
|
||||
|
||||
- 角色和權限是多對多關係(透過 `role_permission`)
|
||||
- 用戶和角色是一對一關係(透過 `user_role`,一個用戶只能有一個角色)
|
||||
|
||||
---
|
||||
|
||||
## 注意事項
|
||||
|
||||
1. **時間戳記差異**:
|
||||
- Member 模組使用納秒級時間戳(`create_at`、`update_at`)
|
||||
- Permission 模組使用秒級時間戳(`create_time`、`update_time`)
|
||||
|
||||
2. **唯一性約束**:
|
||||
- `account`: `login_id + platform` 組合必須唯一
|
||||
- `account_uid_binding`: `login_id` 必須唯一
|
||||
- `user_info`: `uid` 必須唯一
|
||||
- `permission`: `name` 必須唯一,`http_path + http_method` 組合必須唯一(稀疏索引)
|
||||
- `role`: `uid` 必須唯一,`client_id + name` 組合必須唯一
|
||||
- `role_permission`: `role_id + permission_id` 組合必須唯一
|
||||
- `user_role`: `uid` 必須唯一(一個用戶只能有一個角色)
|
||||
|
||||
3. **稀疏索引**:
|
||||
- `permission` 集合的 `http_path + http_method` 索引是稀疏索引,只索引存在這些欄位的文檔
|
||||
|
||||
4. **預設數據**:
|
||||
- `role` 集合會自動插入 4 個預設角色:ADMIN、OPERATOR、USER、PLAYER
|
||||
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
# MongoDB 資料庫文檔
|
||||
|
||||
本目錄包含 MongoDB 資料庫的結構文檔和相關說明。
|
||||
|
||||
## 文件說明
|
||||
|
||||
### DATABASE_SCHEMA.md
|
||||
|
||||
完整的資料庫結構文檔,包含:
|
||||
|
||||
- **所有集合(Collection)的結構定義**
|
||||
- 欄位名稱、類型、必填性、說明
|
||||
- 索引定義和用途
|
||||
- 枚舉值說明
|
||||
|
||||
- **索引總覽**
|
||||
- 所有集合的索引列表
|
||||
- 索引類型和唯一性說明
|
||||
|
||||
- **關聯關係**
|
||||
- 集合之間的關聯圖
|
||||
- 一對一、一對多、多對多關係說明
|
||||
|
||||
- **資料類型說明**
|
||||
- 時間戳記格式差異
|
||||
- 枚舉類型定義
|
||||
|
||||
## 集合列表
|
||||
|
||||
### Member 模組
|
||||
- `account` - 用戶帳號認證資訊
|
||||
- `account_uid_binding` - 帳號與 UID 綁定關係
|
||||
- `user_info` - 用戶個人資料
|
||||
- `count` - 自增 ID 計數器
|
||||
|
||||
### Permission 模組
|
||||
- `permission` - 權限實體
|
||||
- `role` - 角色實體
|
||||
- `role_permission` - 角色權限關聯表
|
||||
- `user_role` - 用戶角色關聯表
|
||||
|
||||
## 使用方式
|
||||
|
||||
1. **查看集合結構**:打開 `DATABASE_SCHEMA.md`,搜尋集合名稱
|
||||
2. **了解索引**:查看「索引總覽」章節
|
||||
3. **了解關聯**:查看「關聯關係」章節
|
||||
4. **了解枚舉值**:查看各集合的「枚舉值」說明
|
||||
|
||||
## 更新說明
|
||||
|
||||
當資料庫結構發生變更時,請更新 `DATABASE_SCHEMA.md` 文檔,確保文檔與實際結構保持一致。
|
||||
|
||||
59
go.mod
59
go.mod
|
|
@ -2,44 +2,45 @@ module backend
|
|||
|
||||
go 1.25.1
|
||||
|
||||
replace backend/pkg/library/errs => ./pkg/library/errs
|
||||
|
||||
require (
|
||||
code.30cm.net/digimon/library-go/utils/invited_code v1.2.5
|
||||
github.com/alicebob/miniredis/v2 v2.35.0
|
||||
github.com/aws/aws-sdk-go v1.55.8
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.6
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21
|
||||
github.com/aws/aws-sdk-go-v2/service/ses v1.34.9
|
||||
github.com/go-playground/validator/v10 v10.28.0
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.2
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.16
|
||||
github.com/aws/aws-sdk-go-v2/service/ses v1.34.5
|
||||
github.com/go-playground/validator/v10 v10.27.0
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/matcornic/hermes/v2 v2.1.0
|
||||
github.com/minchao/go-mitake v1.0.0
|
||||
github.com/panjf2000/ants/v2 v2.11.3
|
||||
github.com/segmentio/ksuid v1.0.4
|
||||
github.com/shopspring/decimal v1.4.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/testcontainers/testcontainers-go v0.40.0
|
||||
github.com/zeromicro/go-zero v1.9.2
|
||||
go.mongodb.org/mongo-driver/v2 v2.4.0
|
||||
github.com/testcontainers/testcontainers-go v0.39.0
|
||||
github.com/zeromicro/go-zero v1.9.1
|
||||
go.mongodb.org/mongo-driver v1.17.4
|
||||
go.mongodb.org/mongo-driver/v2 v2.3.0
|
||||
go.uber.org/mock v0.6.0
|
||||
golang.org/x/crypto v0.44.0
|
||||
google.golang.org/grpc v1.76.0
|
||||
golang.org/x/crypto v0.42.0
|
||||
google.golang.org/grpc v1.75.1
|
||||
google.golang.org/protobuf v1.36.10
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Masterminds/semver v1.4.2 // indirect
|
||||
github.com/Masterminds/sprig v2.16.0+incompatible // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.5.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.0.0 // indirect
|
||||
github.com/aokoli/goutils v1.0.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect
|
||||
github.com/aws/smithy-go v1.23.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 // indirect
|
||||
github.com/aws/smithy-go v1.23.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
|
|
@ -51,19 +52,21 @@ require (
|
|||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/docker v28.5.1+incompatible // indirect
|
||||
github.com/docker/docker v28.3.3+incompatible // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/grafana/pyroscope-go v1.2.7 // indirect
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
|
||||
|
|
@ -71,8 +74,7 @@ require (
|
|||
github.com/huandu/xstrings v1.2.0 // indirect
|
||||
github.com/imdario/mergo v0.3.6 // indirect
|
||||
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/klauspost/compress v1.18.1 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
|
|
@ -100,7 +102,7 @@ require (
|
|||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/redis/go-redis/v9 v9.14.0 // indirect
|
||||
github.com/redis/go-redis/v9 v9.15.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.0.1 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.6 // indirect
|
||||
|
|
@ -108,6 +110,7 @@ require (
|
|||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/vanng822/css v0.0.0-20190504095207-a21e860bcd04 // indirect
|
||||
|
|
@ -119,10 +122,10 @@ require (
|
|||
github.com/yuin/gopher-lua v1.1.1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 // indirect
|
||||
|
|
@ -132,12 +135,12 @@ require (
|
|||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
|
|
|||
134
go.sum
134
go.sum
|
|
@ -4,8 +4,8 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
|||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
|
||||
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
github.com/Masterminds/sprig v2.16.0+incompatible h1:QZbMUPxRQ50EKAq3LFMnxddMu88/EUUG3qmxwtDmPsY=
|
||||
|
|
@ -20,20 +20,18 @@ github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRy
|
|||
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/aokoli/goutils v1.0.1 h1:7fpzNGoJ3VA8qcrm++XEE1QUe0mIwNeLa02Nwq7RDkg=
|
||||
github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ=
|
||||
github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ=
|
||||
github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 h1:56HGpsgnmD+2/KpG0ikvvR8+3v3COCwaF4r+oWwOeNA=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21/go.mod h1:3YELwedmQbw7cXNaII2Wywd+YY58AmLPwX4LzARgmmA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M=
|
||||
github.com/aws/aws-sdk-go-v2/service/ses v1.34.9 h1:hrUBTmbCLLQ+X21wdcoK78sjRW3HGspp/vkAL3TkMx4=
|
||||
github.com/aws/aws-sdk-go-v2/service/ses v1.34.9/go.mod h1:CeGX4LAFCsrBp24qazKmO/dwxghNCGbAoTbi64dGSEM=
|
||||
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
|
||||
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.2 h1:EJLg8IdbzgeD7xgvZ+I8M1e0fL0ptn/M47lianzth0I=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.2/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.16 h1:4JHirI4zp958zC026Sm+V4pSDwW4pwLefKrc0bF2lwI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.16/go.mod h1:qQMtGx9OSw7ty1yLclzLxXCRbrkjWAM7JnObZjmCB7I=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 h1:se2vOWGD3dWQUtfn4wEjRQJb1HK1XsNIt825gskZ970=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9/go.mod h1:hijCGH2VfbZQxqCDN7bwz/4dzxV+hkyhjawAtdPWKZA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 h1:6RBnKZLkJM4hQ+kN6E7yWFveOTg8NLPHAkqrs4ZPlTU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9/go.mod h1:V9rQKRmK7AWuEsOMnHzKj8WyrIir1yUJbZxDuZLFvXI=
|
||||
github.com/aws/aws-sdk-go-v2/service/ses v1.34.5 h1:NwOeuOFrWoh4xWKINrmaAK4Vh75jmmY0RAuNjQ6W5Es=
|
||||
github.com/aws/aws-sdk-go-v2/service/ses v1.34.5/go.mod h1:m3BsMJZD0eqjGIniBzwrNUqG9ZUPquC4hY9FyE2qNFo=
|
||||
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
|
||||
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
|
|
@ -63,8 +61,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
|
|||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
|
||||
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
|
||||
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
|
|
@ -75,8 +73,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
|||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df/go.mod h1:GJr+FCSXshIwgHBtLglIg9M2l2kQSi6QjVAngtzI08Y=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
|
|
@ -91,8 +89,10 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
|||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
|
|
@ -121,12 +121,10 @@ github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
|
|||
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 h1:xqgexXAGQgY3HAjNPSaCqn5Aahbo5TKsmhp8VRfr1iQ=
|
||||
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
|
|
@ -202,8 +200,8 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
|
|||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
|
||||
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/redis/go-redis/v9 v9.15.0 h1:2jdes0xJxer4h3NUZrZ4OGSntGlXp4WbXju2nOTRXto=
|
||||
github.com/redis/go-redis/v9 v9.15.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
|
|
@ -237,8 +235,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
|
|||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU=
|
||||
github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY=
|
||||
github.com/testcontainers/testcontainers-go v0.39.0 h1:uCUJ5tA+fcxbFAB0uP3pIK3EJ2IjjDUHFSZ1H1UxAts=
|
||||
github.com/testcontainers/testcontainers-go v0.39.0/go.mod h1:qmHpkG7H5uPf/EvOORKvS6EuDkBUPE3zpVGaH9NL7f8=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
|
|
@ -255,25 +253,29 @@ github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6
|
|||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/zeromicro/go-zero v1.9.2 h1:ZXOXBIcazZ1pWAMiHyVnDQ3Sxwy7DYPzjE89Qtj9vqM=
|
||||
github.com/zeromicro/go-zero v1.9.2/go.mod h1:k8YBMEFZKjTd4q/qO5RCW+zDgUlNyAs5vue3P4/Kmn0=
|
||||
go.mongodb.org/mongo-driver/v2 v2.4.0 h1:Oq6BmUAAFTzMeh6AonuDlgZMuAuEiUxoAD1koK5MuFo=
|
||||
go.mongodb.org/mongo-driver/v2 v2.4.0/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI=
|
||||
github.com/zeromicro/go-zero v1.9.1 h1:GZCl4jun/ZgZHnSvX3SSNDHf+tEGmEQ8x2Z23xjHa9g=
|
||||
github.com/zeromicro/go-zero v1.9.1/go.mod h1:bHOl7Xr7EV/iHZWEqsUNJwFc/9WgAMrPpPagYvOaMtY=
|
||||
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
|
||||
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
go.mongodb.org/mongo-driver/v2 v2.3.0 h1:sh55yOXA2vUjW1QYw/2tRlHSQViwDyPnW61AwpZ4rtU=
|
||||
go.mongodb.org/mongo-driver/v2 v2.3.0/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4=
|
||||
go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs=
|
||||
|
|
@ -300,24 +302,35 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
|||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
golang.org/x/crypto v0.0.0-20181029175232-7e6ffbd03851/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190225065934-cc5685c2db12/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
|
@ -329,34 +342,38 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
|
||||
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||
|
|
@ -370,7 +387,6 @@ gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkp
|
|||
gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
|
||||
gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
|
|
@ -52,60 +52,12 @@ type Config struct {
|
|||
|
||||
// JWT Token 配置
|
||||
Token struct {
|
||||
AccessSecret string
|
||||
RefreshSecret string
|
||||
AccessTokenExpiry time.Duration
|
||||
RefreshTokenExpiry time.Duration
|
||||
OneTimeTokenExpiry time.Duration
|
||||
MaxTokensPerUser int
|
||||
MaxTokensPerDevice int
|
||||
}
|
||||
|
||||
// RoleConfig 角色配置
|
||||
RoleConfig struct {
|
||||
// UID 前綴 (例如: AM, RL)
|
||||
UIDPrefix string
|
||||
|
||||
// UID 數字長度
|
||||
UIDLength int
|
||||
|
||||
// 管理員角色 UID
|
||||
AdminRoleUID string
|
||||
|
||||
// 管理員用戶 UID
|
||||
AdminUserUID string
|
||||
|
||||
// 預設角色名稱
|
||||
DefaultRoleName string
|
||||
}
|
||||
SMTPConfig struct {
|
||||
Enable bool
|
||||
GoroutinePoolNum int
|
||||
Host string
|
||||
Port int
|
||||
Username string
|
||||
Password string
|
||||
Sender string
|
||||
SenderName string
|
||||
}
|
||||
|
||||
DeliveryConfig struct {
|
||||
MaxRetries int `json:"max_retries"` // 最大重試次數
|
||||
InitialDelay time.Duration `json:"initial_delay"` // 初始重試延遲 (100ms)
|
||||
BackoffFactor float64 `json:"backoff_factor"` // 指數退避因子 (2.0)
|
||||
MaxDelay time.Duration `json:"max_delay"` // 最大延遲時間
|
||||
Timeout time.Duration `json:"timeout"` // 單次發送超時時間
|
||||
EnableHistory bool `json:"enable_history"` // 是否啟用歷史記錄
|
||||
}
|
||||
|
||||
AmazonS3Settings struct {
|
||||
Region string
|
||||
Bucket string
|
||||
CloudFrontDomain string
|
||||
CloudFrontURI string
|
||||
BucketURI string
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
CloudFrontID string
|
||||
AccessSecret string
|
||||
RefreshSecret string
|
||||
AccessTokenExpiry time.Duration
|
||||
RefreshTokenExpiry time.Duration
|
||||
OneTimeTokenExpiry time.Duration
|
||||
MaxTokensPerUser int
|
||||
MaxTokensPerDevice int
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
package domain
|
||||
|
||||
const DefaultBrand = "digimon"
|
||||
const SuccessCode = 10200
|
||||
const SuccessMessage = "success"
|
||||
const DefaultScope = "gateway"
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
package domain
|
||||
|
||||
import "strings"
|
||||
|
||||
type RedisKey string
|
||||
|
||||
const (
|
||||
GenerateVerifyCodeRedisKey RedisKey = "rf_code"
|
||||
)
|
||||
|
||||
func (key RedisKey) ToString() string {
|
||||
return string(key)
|
||||
}
|
||||
|
||||
func (key RedisKey) With(s ...string) RedisKey {
|
||||
parts := append([]string{string(key)}, s...)
|
||||
|
||||
return RedisKey(strings.Join(parts, ":"))
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"backend/internal/domain"
|
||||
"backend/internal/logic/auth"
|
||||
"backend/internal/svc"
|
||||
"backend/internal/types"
|
||||
errs "backend/pkg/library/errors"
|
||||
"backend/pkg/library/errs"
|
||||
ers "backend/pkg/library/errs"
|
||||
"net/http"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
|
|
@ -15,21 +17,20 @@ 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 {
|
||||
e := errs.InputInvalidFormatError(err.Error())
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: err.Error(),
|
||||
e := errs.InvalidFormat(err.Error())
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.RespOK{
|
||||
Code: int(e.FullCode()),
|
||||
Msg: err.Error(),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
//if err := svcCtx.Validate.ValidateAll(req); err != nil {
|
||||
// e := errs.InputInvalidRangeError(err.Error())
|
||||
// httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
// Code: e.DisplayCode(),
|
||||
// Message: err.Error(),
|
||||
// Error: err,
|
||||
// e := errs.InvalidFormat(err.Error())
|
||||
// httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.RespOK{
|
||||
// Code: int(e.FullCode()),
|
||||
// Msg: err.Error(),
|
||||
// })
|
||||
//
|
||||
// return
|
||||
|
|
@ -38,14 +39,18 @@ func LoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|||
l := auth.NewLoginLogic(r.Context(), svcCtx)
|
||||
resp, err := l.Login(&req)
|
||||
if err != nil {
|
||||
e := errs.FromError(err)
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: e.Error(),
|
||||
Error: e.Unwrap(),
|
||||
e := ers.FromError(err)
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.ErrorResp{
|
||||
Code: int(e.FullCode()),
|
||||
Msg: e.Error(),
|
||||
Error: e,
|
||||
})
|
||||
} else {
|
||||
httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp)
|
||||
httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.RespOK{
|
||||
Code: domain.SuccessCode,
|
||||
Msg: domain.SuccessMessage,
|
||||
Data: resp,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
errs "backend/pkg/library/errors"
|
||||
"net/http"
|
||||
|
||||
"backend/internal/logic/auth"
|
||||
|
|
@ -16,36 +15,16 @@ 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 {
|
||||
e := errs.InputInvalidFormatError(err.Error())
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: err.Error(),
|
||||
})
|
||||
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
return
|
||||
}
|
||||
|
||||
//if err := svcCtx.Validate.ValidateAll(req); err != nil {
|
||||
// e := errs.InvalidFormat(err.Error())
|
||||
// httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{
|
||||
// Code: int64(e.FullCode()),
|
||||
// Message: err.Error(),
|
||||
// })
|
||||
//
|
||||
// return
|
||||
//}
|
||||
|
||||
l := auth.NewRefreshTokenLogic(r.Context(), svcCtx)
|
||||
resp, err := l.RefreshToken(&req)
|
||||
if err != nil {
|
||||
e := errs.FromError(err)
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: e.Error(),
|
||||
Error: e.Unwrap(),
|
||||
})
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
} else {
|
||||
httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp)
|
||||
httpx.OkJsonCtx(r.Context(), w, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
errs "backend/pkg/library/errors"
|
||||
"backend/internal/domain"
|
||||
"backend/pkg/library/errs"
|
||||
"net/http"
|
||||
|
||||
"backend/internal/logic/auth"
|
||||
|
|
@ -11,40 +12,45 @@ import (
|
|||
"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 {
|
||||
e := errs.InputInvalidFormatError(err.Error())
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: err.Error(),
|
||||
e := errs.InvalidFormat(err.Error())
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.RespOK{
|
||||
Code: int(e.FullCode()),
|
||||
Msg: err.Error(),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
//if err := svcCtx.Validate.ValidateAll(req); err != nil {
|
||||
// e := errs.InvalidFormat(err.Error())
|
||||
// httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{
|
||||
// Code: int64(e.FullCode()),
|
||||
// Message: err.Error(),
|
||||
// })
|
||||
//
|
||||
// return
|
||||
//}
|
||||
if err := svcCtx.Validate.ValidateAll(req); err != nil {
|
||||
e := errs.InvalidFormat(err.Error())
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.RespOK{
|
||||
Code: int(e.FullCode()),
|
||||
Msg: err.Error(),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
l := auth.NewRegisterLogic(r.Context(), svcCtx)
|
||||
resp, err := l.Register(&req)
|
||||
if err != nil {
|
||||
e := errs.FromError(err)
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: e.Error(),
|
||||
Error: e.Unwrap(),
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.ErrorResp{
|
||||
Code: int(e.FullCode()),
|
||||
Msg: e.Error(),
|
||||
Error: e,
|
||||
})
|
||||
} else {
|
||||
httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp)
|
||||
httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.RespOK{
|
||||
Code: domain.SuccessCode,
|
||||
Msg: domain.SuccessMessage,
|
||||
Data: resp,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
errs "backend/pkg/library/errors"
|
||||
"net/http"
|
||||
|
||||
"backend/internal/logic/auth"
|
||||
|
|
@ -16,36 +15,16 @@ 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 {
|
||||
e := errs.InputInvalidFormatError(err.Error())
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: err.Error(),
|
||||
})
|
||||
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
return
|
||||
}
|
||||
|
||||
//if err := svcCtx.Validate.ValidateAll(req); err != nil {
|
||||
// e := errs.InvalidFormat(err.Error())
|
||||
// httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{
|
||||
// Code: int64(e.FullCode()),
|
||||
// Message: err.Error(),
|
||||
// })
|
||||
//
|
||||
// return
|
||||
//}
|
||||
|
||||
l := auth.NewRequestPasswordResetLogic(r.Context(), svcCtx)
|
||||
resp, err := l.RequestPasswordReset(&req)
|
||||
if err != nil {
|
||||
e := errs.FromError(err)
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: e.Error(),
|
||||
Error: e.Unwrap(),
|
||||
})
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
} else {
|
||||
httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp)
|
||||
httpx.OkJsonCtx(r.Context(), w, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
errs "backend/pkg/library/errors"
|
||||
"net/http"
|
||||
|
||||
"backend/internal/logic/auth"
|
||||
|
|
@ -16,36 +15,16 @@ 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 {
|
||||
e := errs.InputInvalidFormatError(err.Error())
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: err.Error(),
|
||||
})
|
||||
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
return
|
||||
}
|
||||
//
|
||||
//if err := svcCtx.Validate.ValidateAll(req); err != nil {
|
||||
// e := errs.InvalidFormat(err.Error())
|
||||
// httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{
|
||||
// Code: int64(e.FullCode()),
|
||||
// Message: err.Error(),
|
||||
// })
|
||||
//
|
||||
// return
|
||||
//}
|
||||
|
||||
l := auth.NewResetPasswordLogic(r.Context(), svcCtx)
|
||||
resp, err := l.ResetPassword(&req)
|
||||
if err != nil {
|
||||
e := errs.FromError(err)
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: e.Error(),
|
||||
Error: e.Unwrap(),
|
||||
})
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
} else {
|
||||
httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp)
|
||||
httpx.OkJsonCtx(r.Context(), w, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
errs "backend/pkg/library/errors"
|
||||
"net/http"
|
||||
|
||||
"backend/internal/logic/auth"
|
||||
|
|
@ -16,36 +15,16 @@ 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 {
|
||||
e := errs.InputInvalidFormatError(err.Error())
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: err.Error(),
|
||||
})
|
||||
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
return
|
||||
}
|
||||
|
||||
//if err := svcCtx.Validate.ValidateAll(req); err != nil {
|
||||
// e := errs.InvalidFormat(err.Error())
|
||||
// httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{
|
||||
// Code: int64(e.FullCode()),
|
||||
// Message: err.Error(),
|
||||
// })
|
||||
//
|
||||
// return
|
||||
//}
|
||||
|
||||
l := auth.NewVerifyPasswordResetCodeLogic(r.Context(), svcCtx)
|
||||
resp, err := l.VerifyPasswordResetCode(&req)
|
||||
if err != nil {
|
||||
e := errs.FromError(err)
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: e.Error(),
|
||||
Error: e.Unwrap(),
|
||||
})
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
} else {
|
||||
httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp)
|
||||
httpx.OkJsonCtx(r.Context(), w, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
package fileStorage
|
||||
|
||||
import (
|
||||
errs "backend/pkg/library/errors"
|
||||
"net/http"
|
||||
|
||||
"backend/internal/logic/fileStorage"
|
||||
"backend/internal/svc"
|
||||
"backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
// 上傳圖片檔案
|
||||
func UploadImgHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.UploadImgReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
e := errs.InputInvalidFormatError(err.Error())
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
l := fileStorage.NewUploadImgLogic(r.Context(), svcCtx)
|
||||
resp, err := l.UploadImg(&req)
|
||||
if err != nil {
|
||||
e := errs.FromError(err)
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: e.Error(),
|
||||
Error: e.Unwrap(),
|
||||
})
|
||||
} else {
|
||||
httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
package fileStorage
|
||||
|
||||
import (
|
||||
errs "backend/pkg/library/errors"
|
||||
"net/http"
|
||||
|
||||
"backend/internal/logic/fileStorage"
|
||||
"backend/internal/svc"
|
||||
"backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
// 上傳影片檔案
|
||||
func UploadVideoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.UploadVideoReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
e := errs.InputInvalidFormatError(err.Error())
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 解析 multipart form,限制 100MB
|
||||
if err := r.ParseMultipartForm(100 << 20); err != nil {
|
||||
e := errs.InputInvalidFormatError("failed to parse multipart form: " + err.Error())
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 獲取文件
|
||||
file, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
e := errs.InputInvalidFormatError("failed to get file from form: " + err.Error())
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
l := fileStorage.NewUploadVideoLogic(r.Context(), svcCtx)
|
||||
resp, err := l.UploadVideo(&req, file, header)
|
||||
if err != nil {
|
||||
e := errs.FromError(err)
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: e.Error(),
|
||||
Error: e.Unwrap(),
|
||||
})
|
||||
} else {
|
||||
httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
package ping
|
||||
|
||||
import (
|
||||
"backend/internal/types"
|
||||
errs "backend/pkg/library/errors"
|
||||
"backend/pkg/library/errors/code"
|
||||
"net/http"
|
||||
|
||||
"backend/internal/logic/ping"
|
||||
|
|
@ -18,17 +15,9 @@ func PingHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|||
l := ping.NewPingLogic(r.Context(), svcCtx)
|
||||
err := l.Ping()
|
||||
if err != nil {
|
||||
e := errs.FromError(err)
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: e.Error(),
|
||||
Error: e.Unwrap(),
|
||||
})
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
} else {
|
||||
httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Resp{
|
||||
Code: code.SUCCESSCode,
|
||||
Message: code.SUCCESSMessage,
|
||||
})
|
||||
httpx.Ok(w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl 1.9.0
|
||||
// goctl 1.8.1
|
||||
|
||||
package handler
|
||||
|
||||
|
|
@ -8,7 +8,6 @@ import (
|
|||
"time"
|
||||
|
||||
auth "backend/internal/handler/auth"
|
||||
fileStorage "backend/internal/handler/fileStorage"
|
||||
ping "backend/internal/handler/ping"
|
||||
user "backend/internal/handler/user"
|
||||
"backend/internal/svc"
|
||||
|
|
@ -60,28 +59,6 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||
rest.WithTimeout(10000*time.Millisecond),
|
||||
)
|
||||
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.AuthMiddleware},
|
||||
[]rest.Route{
|
||||
{
|
||||
// create - 上傳圖片檔案
|
||||
Method: http.MethodPost,
|
||||
Path: "/fileStorage/img/upload",
|
||||
Handler: fileStorage.UploadImgHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
// create - 上傳影片檔案
|
||||
Method: http.MethodPost,
|
||||
Path: "/fileStorage/video/upload",
|
||||
Handler: fileStorage.UploadVideoHandler(serverCtx),
|
||||
},
|
||||
}...,
|
||||
),
|
||||
rest.WithPrefix("/api/v1/fileStorage"),
|
||||
rest.WithTimeout(300000*time.Millisecond),
|
||||
)
|
||||
|
||||
server.AddRoutes(
|
||||
[]rest.Route{
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
errs "backend/pkg/library/errors"
|
||||
"net/http"
|
||||
|
||||
"backend/internal/logic/user"
|
||||
|
|
@ -16,36 +15,16 @@ 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 {
|
||||
e := errs.InputInvalidFormatError(err.Error())
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: err.Error(),
|
||||
})
|
||||
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
return
|
||||
}
|
||||
|
||||
//if err := svcCtx.Validate.ValidateAll(req); err != nil {
|
||||
// e := errs.InvalidFormat(err.Error())
|
||||
// httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{
|
||||
// Code: int64(e.FullCode()),
|
||||
// Message: err.Error(),
|
||||
// })
|
||||
//
|
||||
// return
|
||||
//}
|
||||
|
||||
l := user.NewGetUserInfoLogic(r.Context(), svcCtx)
|
||||
resp, err := l.GetUserInfo(&req)
|
||||
if err != nil {
|
||||
e := errs.FromError(err)
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: e.Error(),
|
||||
Error: e.Unwrap(),
|
||||
})
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
} else {
|
||||
httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp)
|
||||
httpx.OkJsonCtx(r.Context(), w, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
errs "backend/pkg/library/errors"
|
||||
"net/http"
|
||||
|
||||
"backend/internal/logic/user"
|
||||
|
|
@ -16,26 +15,16 @@ 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 {
|
||||
e := errs.InputInvalidFormatError(err.Error())
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: err.Error(),
|
||||
})
|
||||
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
return
|
||||
}
|
||||
|
||||
l := user.NewRequestVerificationCodeLogic(r.Context(), svcCtx)
|
||||
resp, err := l.RequestVerificationCode(&req)
|
||||
if err != nil {
|
||||
e := errs.FromError(err)
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: e.Error(),
|
||||
Error: e.Unwrap(),
|
||||
})
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
} else {
|
||||
httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp)
|
||||
httpx.OkJsonCtx(r.Context(), w, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
errs "backend/pkg/library/errors"
|
||||
"net/http"
|
||||
|
||||
"backend/internal/logic/user"
|
||||
|
|
@ -16,26 +15,16 @@ 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 {
|
||||
e := errs.InputInvalidFormatError(err.Error())
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: err.Error(),
|
||||
})
|
||||
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
return
|
||||
}
|
||||
|
||||
l := user.NewSubmitVerificationCodeLogic(r.Context(), svcCtx)
|
||||
resp, err := l.SubmitVerificationCode(&req)
|
||||
if err != nil {
|
||||
e := errs.FromError(err)
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: e.Error(),
|
||||
Error: e.Unwrap(),
|
||||
})
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
} else {
|
||||
httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp)
|
||||
httpx.OkJsonCtx(r.Context(), w, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
errs "backend/pkg/library/errors"
|
||||
"net/http"
|
||||
|
||||
"backend/internal/logic/user"
|
||||
|
|
@ -16,26 +15,16 @@ 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 {
|
||||
e := errs.InputInvalidFormatError(err.Error())
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: err.Error(),
|
||||
})
|
||||
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
return
|
||||
}
|
||||
|
||||
l := user.NewUpdatePasswordLogic(r.Context(), svcCtx)
|
||||
resp, err := l.UpdatePassword(&req)
|
||||
if err != nil {
|
||||
e := errs.FromError(err)
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: e.Error(),
|
||||
Error: e.Unwrap(),
|
||||
})
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
} else {
|
||||
httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp)
|
||||
httpx.OkJsonCtx(r.Context(), w, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
errs "backend/pkg/library/errors"
|
||||
"net/http"
|
||||
|
||||
"backend/internal/logic/user"
|
||||
|
|
@ -16,26 +15,16 @@ 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 {
|
||||
e := errs.InputInvalidFormatError(err.Error())
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: err.Error(),
|
||||
})
|
||||
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
return
|
||||
}
|
||||
|
||||
l := user.NewUpdateUserInfoLogic(r.Context(), svcCtx)
|
||||
resp, err := l.UpdateUserInfo(&req)
|
||||
if err != nil {
|
||||
e := errs.FromError(err)
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: e.Error(),
|
||||
Error: e.Unwrap(),
|
||||
})
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
} else {
|
||||
httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp)
|
||||
httpx.OkJsonCtx(r.Context(), w, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ import (
|
|||
)
|
||||
|
||||
// 生成 Token
|
||||
func generateToken(svc *svc.ServiceContext, ctx context.Context, req *types.LoginReq, uid string, role string) (entity.TokenResp, error) {
|
||||
func generateToken(svc *svc.ServiceContext, ctx context.Context, req *types.LoginReq, uid string) (entity.TokenResp, error) {
|
||||
// scope role 要修改,refresh tl
|
||||
role := "user"
|
||||
|
||||
tk, err := svc.TokenUC.NewToken(ctx, entity.AuthorizationReq{
|
||||
GrantType: token.ClientCredentials.ToString(),
|
||||
DeviceID: uid, // TODO 沒傳暫時先用UID 替代
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
errs "backend/pkg/library/errors"
|
||||
"backend/pkg/library/errs"
|
||||
"backend/pkg/library/errs/code"
|
||||
memberD "backend/pkg/member/domain/member"
|
||||
member "backend/pkg/member/domain/usecase"
|
||||
"context"
|
||||
|
|
@ -40,7 +41,7 @@ func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginResp, err erro
|
|||
}
|
||||
|
||||
if !cr.Status {
|
||||
return nil, errs.AuthUnauthorizedError("failed to verify password")
|
||||
return nil, errs.Unauthorized("failed to verify password")
|
||||
}
|
||||
case "platform":
|
||||
switch req.Platform.Provider {
|
||||
|
|
@ -65,10 +66,10 @@ func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginResp, err erro
|
|||
}
|
||||
req.LoginID = userInfo.UserID
|
||||
default:
|
||||
return nil, errs.InputInvalidFormatError("unsupported 3 party platform")
|
||||
return nil, errs.InvalidFormatWithScope(code.CloudEPMember, "unsupported 3 party platform")
|
||||
}
|
||||
default:
|
||||
return nil, errs.InputInvalidFormatError("failed to get correct auth method")
|
||||
return nil, errs.InvalidFormatWithScope(code.CloudEPMember, "failed to get correct auth method")
|
||||
}
|
||||
|
||||
account, err := l.svcCtx.AccountUC.GetUIDByAccount(l.ctx, member.GetUIDByAccountRequest{
|
||||
|
|
@ -78,12 +79,7 @@ func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginResp, err erro
|
|||
return nil, err
|
||||
}
|
||||
|
||||
userRole, err := l.svcCtx.UserRoleUC.Get(l.ctx, account.UID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tk, err := generateToken(l.svcCtx, l.ctx, req, account.UID, userRole.RoleUID)
|
||||
tk, err := generateToken(l.svcCtx, l.ctx, req, account.UID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"backend/internal/domain"
|
||||
"backend/pkg/permission/domain/entity"
|
||||
"context"
|
||||
"time"
|
||||
|
|
@ -34,7 +35,7 @@ func (l *RefreshTokenLogic) RefreshToken(req *types.RefreshTokenReq) (resp *type
|
|||
|
||||
tk, err := l.svcCtx.TokenUC.RefreshToken(l.ctx, entity.RefreshTokenReq{
|
||||
Token: req.RefreshToken,
|
||||
Scope: "gateway",
|
||||
Scope: domain.DefaultScope,
|
||||
Expires: time.Now().UTC().Add(l.svcCtx.Config.Token.RefreshTokenExpiry).Unix(),
|
||||
DeviceID: data["uid"],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"backend/internal/domain"
|
||||
"backend/internal/svc"
|
||||
"backend/internal/types"
|
||||
errs "backend/pkg/library/errors"
|
||||
"backend/pkg/library/errs"
|
||||
"backend/pkg/library/errs/code"
|
||||
mb "backend/pkg/member/domain/member"
|
||||
member "backend/pkg/member/domain/usecase"
|
||||
"backend/pkg/permission/domain/usecase"
|
||||
"context"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
|
|
@ -43,7 +41,7 @@ func (l *RegisterLogic) Register(req *types.LoginReq) (resp *types.LoginResp, er
|
|||
case "credentials":
|
||||
fn, ok := PrepareFunc[mb.Digimon.ToString()]
|
||||
if !ok {
|
||||
return nil, errs.InputInvalidRangeError("failed to get correct credentials method")
|
||||
return nil, errs.InvalidRangeWithScope(code.CloudEPMember, 0, "failed to get correct credentials method")
|
||||
}
|
||||
bd, err = fn(l.ctx, req, l.svcCtx)
|
||||
if err != nil {
|
||||
|
|
@ -52,14 +50,14 @@ func (l *RegisterLogic) Register(req *types.LoginReq) (resp *types.LoginResp, er
|
|||
case "platform":
|
||||
fn, ok := PrepareFunc[req.Platform.Provider]
|
||||
if !ok {
|
||||
return nil, errs.InputInvalidRangeError("failed to get correct credentials method")
|
||||
return nil, errs.InvalidRangeWithScope(code.CloudEPMember, 0, "failed to get correct credentials method")
|
||||
}
|
||||
bd, err = fn(l.ctx, req, l.svcCtx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, errs.InputInvalidFormatError("failed to get correct auth method")
|
||||
return nil, errs.InvalidFormatWithScope(code.CloudEPMember, "failed to get correct auth method")
|
||||
}
|
||||
|
||||
// Step 2: 建立帳號
|
||||
|
|
@ -78,18 +76,9 @@ func (l *RegisterLogic) Register(req *types.LoginReq) (resp *types.LoginResp, er
|
|||
return nil, err
|
||||
}
|
||||
|
||||
_, err = l.svcCtx.UserRoleUC.Assign(l.ctx, usecase.AssignRoleRequest{
|
||||
RoleUID: l.svcCtx.Config.RoleConfig.DefaultRoleName,
|
||||
UserUID: account.UID,
|
||||
Brand: domain.DefaultBrand,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Step 5: 生成 Token
|
||||
req.LoginID = bd.CreateAccountReq.LoginID
|
||||
tk, err := generateToken(l.svcCtx, l.ctx, req, account.UID, l.svcCtx.Config.RoleConfig.DefaultRoleName)
|
||||
tk, err := generateToken(l.svcCtx, l.ctx, req, account.UID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,7 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"backend/internal/domain"
|
||||
"backend/internal/utils"
|
||||
"backend/internal/utils/email_template"
|
||||
errs "backend/pkg/library/errors"
|
||||
"backend/pkg/member/domain/member"
|
||||
"backend/pkg/member/domain/usecase"
|
||||
notificationUC "backend/pkg/notification/domain/usecase"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
|
||||
"backend/internal/svc"
|
||||
"backend/internal/types"
|
||||
|
|
@ -35,174 +25,7 @@ func NewRequestPasswordResetLogic(ctx context.Context, svcCtx *svc.ServiceContex
|
|||
|
||||
// RequestPasswordReset 請求發送密碼重設驗證碼 aka 忘記密碼
|
||||
func (l *RequestPasswordResetLogic) RequestPasswordReset(req *types.RequestPasswordResetReq) (resp *types.RespOK, err error) {
|
||||
// 驗證並標準化帳號
|
||||
acc, err := l.validateAndNormalizeAccount(req.AccountType, req.Identifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// todo: add your logic here and delete this line
|
||||
|
||||
// 檢查發送冷卻時間
|
||||
rk := domain.GenerateVerifyCodeRedisKey.With(fmt.Sprintf("%s:%d", acc, member.GenerateCodeTypeForgetPassword)).ToString()
|
||||
if err := l.checkVerifyCodeCooldown(rk); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 確認帳號是否註冊並檢查平台限制
|
||||
if err := l.checkAccountAndPlatform(acc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 生成驗證碼
|
||||
vcode, err := l.svcCtx.AccountUC.GenerateRefreshCode(l.ctx, usecase.GenerateRefreshCodeRequest{
|
||||
LoginID: acc,
|
||||
CodeType: member.GenerateCodeTypeForgetPassword,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 獲取用戶資訊並確認綁定帳號
|
||||
account, err := l.svcCtx.AccountUC.GetUIDByAccount(l.ctx, usecase.GetUIDByAccountRequest{Account: acc})
|
||||
if err != nil {
|
||||
return nil, errs.ResNotFoundError(fmt.Sprintf("account not found:%s", acc))
|
||||
}
|
||||
info, err := l.svcCtx.AccountUC.GetUserInfo(l.ctx, usecase.GetUserInfoRequest{UID: account.UID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nickname := generateMsgName(&info)
|
||||
switch member.GetAccountTypeByCode(req.AccountType) {
|
||||
case member.AccountTypeMail:
|
||||
body, title, err := email_template.GetEmailTemplate(email_template.Language(info.PreferredLanguage), email_template.ForgetPasswordVerify)
|
||||
if err != nil {
|
||||
e := errs.ResNotFoundError("failed to get correct email template")
|
||||
return nil, e
|
||||
}
|
||||
|
||||
tmpl, err := template.New("ForgetPasswordEmail").Parse(body)
|
||||
if err != nil {
|
||||
e := errs.ResInvalidFormatError("failed to get correct email template")
|
||||
return nil, e
|
||||
}
|
||||
|
||||
emailParams := email_template.ForgetPasswordEmailReq{
|
||||
Username: nickname,
|
||||
VerifyCode: vcode.Data.VerifyCode,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, emailParams); err != nil {
|
||||
e := errs.ResInvalidFormatError("failed to build data")
|
||||
return nil, e
|
||||
}
|
||||
|
||||
err = l.svcCtx.DeliveryUC.SendEmail(l.ctx, notificationUC.MailReq{
|
||||
To: []string{req.Identifier},
|
||||
From: l.svcCtx.Config.SMTPConfig.Sender,
|
||||
SenderName: l.svcCtx.Config.SMTPConfig.SenderName,
|
||||
Subject: title,
|
||||
Body: buf.String(),
|
||||
})
|
||||
if err != nil {
|
||||
e := errs.SvcThirdPartyError("failed to send email").Wrap(err)
|
||||
|
||||
return nil, e
|
||||
}
|
||||
case member.AccountTypePhone:
|
||||
//// 送出手機號碼
|
||||
//templateResp, err := l.svcCtx.NotificationUseCase.GetSMSTemplateByTypeID(
|
||||
// l.ctx, notificationModule.Language(info.PreferredLanguage), notificationModule.BindingPhone)
|
||||
//if err != nil {
|
||||
// return nil, err
|
||||
//}
|
||||
//
|
||||
//fmt.Println(fmt.Sprintf("%s:%s", templateResp.Body, vcode.Data.VerifyCode))
|
||||
////err = l.svcCtx.NotificationUseCase.SendMessage(l.ctx, notificationModule.SMSMessageRequest{
|
||||
//// PhoneNumber: acc,
|
||||
//// RecipientName: nickname,
|
||||
//// MessageContent: fmt.Sprintf("%s:%s", templateResp.Body, vcode),
|
||||
////})
|
||||
////if err != nil {
|
||||
//// return nil, err
|
||||
////}
|
||||
case member.AccountTypeNone:
|
||||
case member.AccountTypeDefine:
|
||||
default:
|
||||
return &types.RespOK{}, errs.InputInvalidRangeError("")
|
||||
}
|
||||
|
||||
// 設置 Redis 鍵
|
||||
l.setRedisKeyWithExpiry(rk, vcode.Data.VerifyCode, 60)
|
||||
|
||||
return &types.RespOK{}, nil
|
||||
}
|
||||
|
||||
// validateAndNormalizeAccount 驗證並標準化帳號
|
||||
func (l *RequestPasswordResetLogic) validateAndNormalizeAccount(accountType, account string) (string, error) {
|
||||
switch member.GetAccountTypeByCode(accountType) {
|
||||
case member.AccountTypePhone:
|
||||
phone, isPhone := utils.NormalizeTaiwanMobile(account)
|
||||
if !isPhone {
|
||||
return "", errs.InputInvalidFormatError("phone number is invalid")
|
||||
}
|
||||
|
||||
return phone, nil
|
||||
case member.AccountTypeMail:
|
||||
if !utils.IsValidEmail(account) {
|
||||
return "", errs.InputInvalidFormatError("email is invalid")
|
||||
}
|
||||
|
||||
return account, nil
|
||||
case member.AccountTypeNone, member.AccountTypeDefine:
|
||||
default:
|
||||
}
|
||||
|
||||
return "", errs.InputInvalidFormatError("unsupported account type")
|
||||
}
|
||||
|
||||
// checkVerifyCodeCooldown 檢查是否已在限制時間內發送過驗證碼
|
||||
func (l *RequestPasswordResetLogic) checkVerifyCodeCooldown(rk string) error {
|
||||
if cachedCode, err := l.svcCtx.Redis.GetCtx(l.ctx, rk); err != nil || cachedCode != "" {
|
||||
return errs.SysTooManyRequestError("verification code already sent, please wait 3min for system to send again")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkAccountAndPlatform 檢查帳號是否註冊及平台限制
|
||||
func (l *RequestPasswordResetLogic) checkAccountAndPlatform(acc string) error {
|
||||
accountInfo, err := l.svcCtx.AccountUC.GetUserAccountInfo(l.ctx, usecase.GetUIDByAccountRequest{Account: acc})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if accountInfo.Data.Platform != member.Digimon {
|
||||
return errs.InputInvalidFormatError(
|
||||
"failed to send verify code since platform not correct")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setRedisKeyWithExpiry 設置 Redis 鍵
|
||||
func (l *RequestPasswordResetLogic) setRedisKeyWithExpiry(rk, verifyCode string, expiry int) {
|
||||
if status, err := l.svcCtx.Redis.SetnxExCtx(l.ctx, rk, verifyCode, expiry); err != nil || !status {
|
||||
_ = errs.DBErrorErrorL(l.svcCtx.Logger, []errs.LogField{
|
||||
{Key: "redisKey", Val: rk},
|
||||
{Key: "error", Val: err.Error()},
|
||||
}, "failed to set redis expire").Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
// generateMsgName 取得寄信用的名稱
|
||||
func generateMsgName(info *usecase.UserInfo) string {
|
||||
if info.FullName != nil {
|
||||
return *info.FullName
|
||||
}
|
||||
if info.Nickname != nil {
|
||||
return *info.Nickname
|
||||
}
|
||||
|
||||
return info.UID
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,7 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"backend/internal/domain"
|
||||
errs "backend/pkg/library/errors"
|
||||
"backend/pkg/member/domain/member"
|
||||
"backend/pkg/member/domain/usecase"
|
||||
"backend/pkg/permission/domain/entity"
|
||||
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"backend/internal/svc"
|
||||
"backend/internal/types"
|
||||
|
|
@ -22,7 +15,7 @@ type ResetPasswordLogic struct {
|
|||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// NewResetPasswordLogic 執行密碼重設
|
||||
// 執行密碼重設
|
||||
func NewResetPasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetPasswordLogic {
|
||||
return &ResetPasswordLogic{
|
||||
Logger: logx.WithContext(ctx),
|
||||
|
|
@ -31,58 +24,8 @@ func NewResetPasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Res
|
|||
}
|
||||
}
|
||||
|
||||
func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordReq) (*types.RespOK, error) {
|
||||
// 驗證密碼,兩次密碼要一致
|
||||
if req.Password != req.PasswordConfirm {
|
||||
return nil, errs.InputInvalidFormatError("password confirmation does not match")
|
||||
}
|
||||
func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordReq) (resp *types.RespOK, err error) {
|
||||
// todo: add your logic here and delete this line
|
||||
|
||||
// 驗證碼
|
||||
err := l.svcCtx.AccountUC.VerifyRefreshCode(l.ctx, usecase.VerifyRefreshCodeRequest{
|
||||
LoginID: req.Identifier,
|
||||
CodeType: member.GenerateCodeTypeForgetPassword,
|
||||
VerifyCode: req.VerifyCode,
|
||||
})
|
||||
if err != nil {
|
||||
// 表使沒有這驗證碼
|
||||
return nil, errs.AuthForbiddenError("failed to get verify code")
|
||||
}
|
||||
|
||||
info, err := l.svcCtx.AccountUC.GetUserAccountInfo(l.ctx, usecase.GetUIDByAccountRequest{Account: req.Identifier})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if info.Data.Platform != member.Digimon {
|
||||
return nil, errs.AuthForbiddenError("invalid platform")
|
||||
}
|
||||
|
||||
// 更新
|
||||
err = l.svcCtx.AccountUC.UpdateUserToken(l.ctx, usecase.UpdateTokenRequest{
|
||||
Account: req.Identifier,
|
||||
Token: req.Password,
|
||||
Platform: member.Digimon.ToInt64(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rk := domain.GenerateVerifyCodeRedisKey.With(
|
||||
fmt.Sprintf("%s-%d", req.Identifier, member.GenerateCodeTypeForgetPassword),
|
||||
).ToString()
|
||||
|
||||
_, _ = l.svcCtx.Redis.Del(rk)
|
||||
|
||||
ac, err := l.svcCtx.AccountUC.GetUIDByAccount(l.ctx, usecase.GetUIDByAccountRequest{Account: req.Identifier})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = l.svcCtx.TokenUC.CancelTokens(l.ctx, entity.DoTokenByUIDReq{UID: ac.UID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 返回成功響應
|
||||
return &types.RespOK{}, nil
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
errs "backend/pkg/library/errors"
|
||||
"backend/pkg/member/domain/member"
|
||||
"backend/pkg/member/domain/usecase"
|
||||
"context"
|
||||
|
||||
"backend/internal/svc"
|
||||
|
|
@ -28,16 +25,7 @@ func NewVerifyPasswordResetCodeLogic(ctx context.Context, svcCtx *svc.ServiceCon
|
|||
|
||||
// VerifyPasswordResetCode 校驗密碼重設驗證碼(頁面需求,預先檢查看看, 顯示表演用)
|
||||
func (l *VerifyPasswordResetCodeLogic) VerifyPasswordResetCode(req *types.VerifyCodeReq) (resp *types.RespOK, err error) {
|
||||
// 先驗證,不刪除
|
||||
if err := l.svcCtx.AccountUC.CheckRefreshCode(l.ctx, usecase.VerifyRefreshCodeRequest{
|
||||
VerifyCode: req.VerifyCode,
|
||||
LoginID: req.Identifier,
|
||||
CodeType: member.GenerateCodeTypeForgetPassword,
|
||||
}); err != nil {
|
||||
e := errs.AuthForbiddenError("failed to get verify code").Wrap(err)
|
||||
// todo: add your logic here and delete this line
|
||||
|
||||
return nil, e
|
||||
}
|
||||
|
||||
return &types.RespOK{}, nil
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,121 +0,0 @@
|
|||
package fileStorage
|
||||
|
||||
import (
|
||||
"backend/pkg/permission/domain/token"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"backend/internal/svc"
|
||||
"backend/internal/types"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type UploadImgLogic struct {
|
||||
logx.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// NewUploadImgLogic 上傳圖片檔案
|
||||
func NewUploadImgLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UploadImgLogic {
|
||||
return &UploadImgLogic{
|
||||
Logger: logx.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *UploadImgLogic) UploadImg(req *types.UploadImgReq) (resp *types.UploadResp, err error) {
|
||||
// 驗證 base64 格式
|
||||
content := req.Content
|
||||
if strings.HasPrefix(content, "data:image") {
|
||||
// 移除 data URL 前綴 (例如: data:image/png;base64,)
|
||||
parts := strings.SplitN(content, ",", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("invalid base64 image format")
|
||||
}
|
||||
content = parts[1]
|
||||
}
|
||||
|
||||
// 解碼 base64
|
||||
imgData, err := base64.StdEncoding.DecodeString(content)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode base64: %w", err)
|
||||
}
|
||||
|
||||
// 驗證文件大小(10MB 限制)
|
||||
const maxImgSize = 10 << 20 // 10MB
|
||||
if len(imgData) > maxImgSize {
|
||||
return nil, fmt.Errorf("image size exceeds 10MB limit")
|
||||
}
|
||||
|
||||
// 驗證圖片格式(檢查 magic bytes)
|
||||
mimeType, err := l.detectImageType(imgData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid image format: %w", err)
|
||||
}
|
||||
|
||||
// 生成唯一文件名
|
||||
fileExt := l.getExtensionFromMimeType(mimeType)
|
||||
fileName := fmt.Sprintf("%s%s", uuid.New().String(), fileExt)
|
||||
objectPath := fmt.Sprintf("images/%s/%d/%s", token.UID(l.ctx), time.Now().Year(), fileName)
|
||||
|
||||
// 上傳到 S3
|
||||
fileStorageUC := l.svcCtx.FileStorageUC
|
||||
if err := fileStorageUC.UploadFromData(l.ctx, imgData, objectPath, mimeType); err != nil {
|
||||
return nil, fmt.Errorf("failed to upload image: %w", err)
|
||||
}
|
||||
|
||||
// 獲取公開 URL
|
||||
fileUrl := fileStorageUC.GetPublicURL(l.ctx, objectPath)
|
||||
|
||||
return &types.UploadResp{
|
||||
FileUrl: fileUrl,
|
||||
FileSize: int64(len(imgData)),
|
||||
MimeType: mimeType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// detectImageType 檢測圖片類型
|
||||
func (l *UploadImgLogic) detectImageType(data []byte) (string, error) {
|
||||
if len(data) < 4 {
|
||||
return "", fmt.Errorf("file too small")
|
||||
}
|
||||
|
||||
// 檢查常見圖片格式的 magic bytes
|
||||
if len(data) >= 2 && data[0] == 0xFF && data[1] == 0xD8 {
|
||||
return "image/jpeg", nil
|
||||
}
|
||||
if len(data) >= 8 && string(data[0:8]) == "\x89PNG\r\n\x1a\n" {
|
||||
return "image/png", nil
|
||||
}
|
||||
if len(data) >= 6 && (string(data[0:6]) == "GIF87a" || string(data[0:6]) == "GIF89a") {
|
||||
return "image/gif", nil
|
||||
}
|
||||
if len(data) >= 12 && string(data[8:12]) == "WEBP" {
|
||||
return "image/webp", nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("unsupported image format")
|
||||
}
|
||||
|
||||
// getExtensionFromMimeType 根據 MIME 類型獲取文件擴展名
|
||||
func (l *UploadImgLogic) getExtensionFromMimeType(mimeType string) string {
|
||||
switch mimeType {
|
||||
case "image/jpeg":
|
||||
return ".jpg"
|
||||
case "image/png":
|
||||
return ".png"
|
||||
case "image/gif":
|
||||
return ".gif"
|
||||
case "image/webp":
|
||||
return ".webp"
|
||||
default:
|
||||
return ".jpg"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
package fileStorage
|
||||
|
||||
import (
|
||||
"backend/pkg/permission/domain/token"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"backend/internal/svc"
|
||||
"backend/internal/types"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type UploadVideoLogic struct {
|
||||
logx.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewUploadVideoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UploadVideoLogic {
|
||||
return &UploadVideoLogic{
|
||||
Logger: logx.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *UploadVideoLogic) UploadVideo(req *types.UploadVideoReq, file multipart.File, header *multipart.FileHeader) (resp *types.UploadResp, err error) {
|
||||
// 驗證文件大小(100MB 限制)
|
||||
const maxVideoSize = 100 << 20 // 100MB
|
||||
if header.Size > maxVideoSize {
|
||||
return nil, fmt.Errorf("video size exceeds 100MB limit")
|
||||
}
|
||||
|
||||
// 讀取文件內容
|
||||
videoData, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
|
||||
// 驗證實際文件大小
|
||||
if int64(len(videoData)) > maxVideoSize {
|
||||
return nil, fmt.Errorf("video size exceeds 100MB limit")
|
||||
}
|
||||
|
||||
// 檢測視頻 MIME 類型
|
||||
mimeType := l.detectVideoType(videoData, header)
|
||||
if mimeType == "" {
|
||||
// 從文件名推斷
|
||||
ext := strings.ToLower(filepath.Ext(header.Filename))
|
||||
mimeType = l.getMimeTypeFromExtension(ext)
|
||||
if mimeType == "" {
|
||||
return nil, fmt.Errorf("unsupported video format")
|
||||
}
|
||||
}
|
||||
|
||||
// 生成唯一文件名
|
||||
fileExt := filepath.Ext(header.Filename)
|
||||
if fileExt == "" {
|
||||
fileExt = l.getExtensionFromMimeType(mimeType)
|
||||
}
|
||||
fileName := fmt.Sprintf("%s%s", uuid.New().String(), fileExt)
|
||||
objectPath := fmt.Sprintf("videos/%s/%d/%s", token.UID(l.ctx), time.Now().Year(), fileName)
|
||||
|
||||
// 上傳到 S3
|
||||
fileStorageUC := l.svcCtx.FileStorageUC
|
||||
if err := fileStorageUC.UploadFromData(l.ctx, videoData, objectPath, mimeType); err != nil {
|
||||
return nil, fmt.Errorf("failed to upload video: %w", err)
|
||||
}
|
||||
|
||||
// 獲取公開 URL
|
||||
fileUrl := fileStorageUC.GetPublicURL(l.ctx, objectPath)
|
||||
|
||||
return &types.UploadResp{
|
||||
FileUrl: fileUrl,
|
||||
FileSize: int64(len(videoData)),
|
||||
MimeType: mimeType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// detectVideoType 檢測視頻類型(通過 magic bytes)
|
||||
func (l *UploadVideoLogic) detectVideoType(data []byte, header *multipart.FileHeader) string {
|
||||
if len(data) < 12 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 檢查常見視頻格式的 magic bytes
|
||||
// MP4: ftyp box 通常在開頭
|
||||
if len(data) >= 12 {
|
||||
// MP4/MOV: 通常以 ftyp 開頭
|
||||
if string(data[4:8]) == "ftyp" {
|
||||
if strings.Contains(string(data[8:12]), "mp4") || strings.Contains(string(data[8:12]), "isom") {
|
||||
return "video/mp4"
|
||||
}
|
||||
if strings.Contains(string(data[8:12]), "qt") {
|
||||
return "video/quicktime"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AVI: RIFF...AVI
|
||||
if len(data) >= 12 && string(data[0:4]) == "RIFF" && string(data[8:12]) == "AVI " {
|
||||
return "video/x-msvideo"
|
||||
}
|
||||
|
||||
// WebM: webm
|
||||
if len(data) >= 4 && string(data[0:4]) == "\x1a\x45\xdf\xa3" {
|
||||
return "video/webm"
|
||||
}
|
||||
|
||||
// 從 Content-Type header 獲取
|
||||
if header != nil && len(header.Header) > 0 {
|
||||
contentType := header.Header.Get("Content-Type")
|
||||
if contentType != "" && strings.HasPrefix(contentType, "video/") {
|
||||
return contentType
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// getMimeTypeFromExtension 根據文件擴展名獲取 MIME 類型
|
||||
func (l *UploadVideoLogic) getMimeTypeFromExtension(ext string) string {
|
||||
ext = strings.ToLower(ext)
|
||||
switch ext {
|
||||
case ".mp4":
|
||||
return "video/mp4"
|
||||
case ".mov":
|
||||
return "video/quicktime"
|
||||
case ".avi":
|
||||
return "video/x-msvideo"
|
||||
case ".webm":
|
||||
return "video/webm"
|
||||
case ".mkv":
|
||||
return "video/x-matroska"
|
||||
case ".flv":
|
||||
return "video/x-flv"
|
||||
case ".wmv":
|
||||
return "video/x-ms-wmv"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// getExtensionFromMimeType 根據 MIME 類型獲取文件擴展名
|
||||
func (l *UploadVideoLogic) getExtensionFromMimeType(mimeType string) string {
|
||||
switch mimeType {
|
||||
case "video/mp4":
|
||||
return ".mp4"
|
||||
case "video/quicktime":
|
||||
return ".mov"
|
||||
case "video/x-msvideo":
|
||||
return ".avi"
|
||||
case "video/webm":
|
||||
return ".webm"
|
||||
case "video/x-matroska":
|
||||
return ".mkv"
|
||||
case "video/x-flv":
|
||||
return ".flv"
|
||||
case "video/x-ms-wmv":
|
||||
return ".wmv"
|
||||
default:
|
||||
return ".mp4"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
package ping
|
||||
|
||||
import (
|
||||
"backend/internal/svc"
|
||||
"context"
|
||||
|
||||
"backend/internal/svc"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
|
|
@ -23,5 +24,7 @@ func NewPingLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PingLogic {
|
|||
}
|
||||
|
||||
func (l *PingLogic) Ping() error {
|
||||
// todo: add your logic here and delete this line
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,7 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"backend/pkg/member/domain/member"
|
||||
"backend/pkg/member/domain/usecase"
|
||||
"backend/pkg/permission/domain/token"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"backend/internal/svc"
|
||||
"backend/internal/types"
|
||||
|
|
@ -21,7 +15,7 @@ type GetUserInfoLogic struct {
|
|||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// NewGetUserInfoLogic 取得當前登入的會員資訊(自己)
|
||||
// 取得當前登入的會員資訊(自己)
|
||||
func NewGetUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserInfoLogic {
|
||||
return &GetUserInfoLogic{
|
||||
Logger: logx.WithContext(ctx),
|
||||
|
|
@ -30,88 +24,8 @@ func NewGetUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUs
|
|||
}
|
||||
}
|
||||
|
||||
func (l *GetUserInfoLogic) GetUserInfo(req *types.Authorization) (*types.MyInfo, error) {
|
||||
uid := token.UID(l.ctx)
|
||||
info, err := l.svcCtx.AccountUC.GetUserInfo(l.ctx, usecase.GetUserInfoRequest{
|
||||
UID: uid,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func (l *GetUserInfoLogic) GetUserInfo(req *types.Authorization) (resp *types.UserInfoResp, err error) {
|
||||
// todo: add your logic here and delete this line
|
||||
|
||||
byUID, err := l.svcCtx.AccountUC.FindLoginIDByUID(l.ctx, uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accountInfo, err := l.svcCtx.AccountUC.GetUserAccountInfo(l.ctx, usecase.GetUIDByAccountRequest{
|
||||
Account: byUID.LoginID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userRole, err := l.svcCtx.UserRoleUC.Get(l.ctx, uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
role := userRole.RoleUID
|
||||
res := &types.MyInfo{
|
||||
Platform: accountInfo.Data.Platform.ToString(),
|
||||
UID: info.UID,
|
||||
UpdateAt: time.Unix(0, info.CreateTime).UTC().Format(time.RFC3339),
|
||||
CreateAt: time.Unix(0, info.UpdateTime).UTC().Format(time.RFC3339),
|
||||
Role: role,
|
||||
UserStatus: info.UserStatus.CodeToString(),
|
||||
PreferredLanguage: info.PreferredLanguage,
|
||||
Currency: info.Currency,
|
||||
AlarmCategory: info.AlarmCategory.CodeToString(),
|
||||
}
|
||||
if info.Address != nil {
|
||||
res.Address = info.Address
|
||||
}
|
||||
if info.AvatarURL != nil {
|
||||
res.AvatarURL = info.AvatarURL
|
||||
}
|
||||
if info.FullName != nil {
|
||||
res.FullName = info.FullName
|
||||
}
|
||||
|
||||
if info.Birthdate != nil {
|
||||
b := ToDate(info.Birthdate)
|
||||
res.Birthdate = b
|
||||
}
|
||||
|
||||
if info.Address != nil {
|
||||
res.Address = info.Address
|
||||
}
|
||||
|
||||
if info.Nickname != nil {
|
||||
res.Nickname = info.Nickname
|
||||
}
|
||||
|
||||
if info.Email != nil {
|
||||
res.Email = info.Email
|
||||
res.IsEmailVerified = proto.Bool(true)
|
||||
}
|
||||
|
||||
if info.PhoneNumber != nil {
|
||||
res.PhoneNumber = info.PhoneNumber
|
||||
res.IsPhoneVerified = proto.Bool(true)
|
||||
}
|
||||
if info.GenderCode != nil {
|
||||
gc := member.GetGenderByCode(*info.GenderCode)
|
||||
res.GenderCode = &gc
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func ToDate(n *int64) *string {
|
||||
result := ""
|
||||
if n != nil {
|
||||
result = time.Unix(*n, 0).UTC().Format(time.DateOnly)
|
||||
}
|
||||
|
||||
return &result
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,7 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"backend/internal/domain"
|
||||
"backend/internal/utils/email_template"
|
||||
errs "backend/pkg/library/errors"
|
||||
mbr "backend/pkg/member/domain/member"
|
||||
member "backend/pkg/member/domain/usecase"
|
||||
"backend/pkg/notification/domain/usecase"
|
||||
"backend/pkg/permission/domain/token"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"backend/internal/svc"
|
||||
"backend/internal/types"
|
||||
|
|
@ -27,7 +15,7 @@ type RequestVerificationCodeLogic struct {
|
|||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// NewRequestVerificationCodeLogic 請求發送驗證碼 (用於驗證信箱/手機)
|
||||
// 請求發送驗證碼 (用於驗證信箱/手機)
|
||||
func NewRequestVerificationCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RequestVerificationCodeLogic {
|
||||
return &RequestVerificationCodeLogic{
|
||||
Logger: logx.WithContext(ctx),
|
||||
|
|
@ -37,175 +25,7 @@ func NewRequestVerificationCodeLogic(ctx context.Context, svcCtx *svc.ServiceCon
|
|||
}
|
||||
|
||||
func (l *RequestVerificationCodeLogic) RequestVerificationCode(req *types.RequestVerificationCodeReq) (resp *types.RespOK, err error) {
|
||||
acc := ""
|
||||
ct := mbr.GenerateCodeTypeEmail
|
||||
switch req.Purpose {
|
||||
case "email_verification":
|
||||
if !isValidEmail(req.Account) {
|
||||
return nil, errs.InputInvalidFormatError("email is invalid")
|
||||
}
|
||||
acc = req.Account
|
||||
// 1. TODO 討論 email 不可以再被使用
|
||||
// 2. TODO 討論 email 跟我帳號是不是一樣(如果是用自己的信箱註冊的話)
|
||||
case "phone_verification":
|
||||
phone, isPhone := normalizeTaiwanMobile(req.Account)
|
||||
if !isPhone {
|
||||
return nil, errs.InputInvalidFormatError("phone number is invalid")
|
||||
}
|
||||
acc = phone
|
||||
// TODO 討論號碼有被用過就不可以再被使用了
|
||||
ct = mbr.GenerateCodeTypePhone
|
||||
default:
|
||||
return &types.RespOK{}, errs.InputInvalidRangeError("")
|
||||
}
|
||||
// todo: add your logic here and delete this line
|
||||
|
||||
uid := token.UID(l.ctx)
|
||||
// 限制三分鐘內只可以發送一次
|
||||
rk := domain.GenerateVerifyCodeRedisKey.With(
|
||||
fmt.Sprintf("%s-%s", uid, req.Purpose),
|
||||
).ToString()
|
||||
|
||||
// 拿不到不會出錯,DB 壞掉才會
|
||||
get, err := l.svcCtx.Redis.GetCtx(l.ctx, rk)
|
||||
if err != nil {
|
||||
return nil, errs.DBErrorError("failed to connect to redis").Wrap(err)
|
||||
}
|
||||
if get != "" {
|
||||
// 已經發送過驗證碼,返回提示
|
||||
return nil, errs.SysTooManyRequestError("code already sent, please wait 3min for system to send again")
|
||||
}
|
||||
|
||||
// 生成驗證碼
|
||||
vcode, err := l.svcCtx.AccountUC.GenerateRefreshCode(l.ctx, member.GenerateRefreshCodeRequest{
|
||||
LoginID: acc,
|
||||
CodeType: ct,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 取得用戶資訊
|
||||
info, err := l.svcCtx.AccountUC.GetUserInfo(l.ctx, member.GetUserInfoRequest{
|
||||
UID: uid,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nickname := generateMsgName(&info)
|
||||
switch ct {
|
||||
case mbr.GenerateCodeTypeEmail:
|
||||
body, title, err := email_template.GetEmailTemplate(email_template.Language(info.PreferredLanguage), email_template.BindingEmail)
|
||||
if err != nil {
|
||||
e := errs.ResNotFoundError("failed to get correct email template")
|
||||
return nil, e
|
||||
}
|
||||
|
||||
tmpl, err := template.New("BindEmailBody").Parse(body)
|
||||
if err != nil {
|
||||
e := errs.ResInvalidFormatError("failed to get correct email template")
|
||||
return nil, e
|
||||
}
|
||||
|
||||
emailParams := email_template.ForgetPasswordEmailReq{
|
||||
Username: nickname,
|
||||
VerifyCode: vcode.Data.VerifyCode,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, emailParams); err != nil {
|
||||
e := errs.ResInvalidFormatError("failed to build data")
|
||||
return nil, e
|
||||
}
|
||||
|
||||
err = l.svcCtx.DeliveryUC.SendEmail(l.ctx, usecase.MailReq{
|
||||
To: []string{req.Account},
|
||||
From: l.svcCtx.Config.SMTPConfig.Sender,
|
||||
SenderName: l.svcCtx.Config.SMTPConfig.SenderName,
|
||||
Subject: title,
|
||||
Body: buf.String(),
|
||||
})
|
||||
if err != nil {
|
||||
e := errs.SvcThirdPartyError("failed to send email").Wrap(err)
|
||||
|
||||
return nil, e
|
||||
}
|
||||
case mbr.GenerateCodeTypePhone:
|
||||
//// 送出手機號碼
|
||||
//templateResp, err := l.svcCtx.NotificationUseCase.GetSMSTemplateByTypeID(
|
||||
// l.ctx, notificationModule.Language(info.PreferredLanguage), notificationModule.BindingPhone)
|
||||
//if err != nil {
|
||||
// return nil, err
|
||||
//}
|
||||
//
|
||||
//fmt.Println(fmt.Sprintf("%s:%s", templateResp.Body, vcode.Data.VerifyCode))
|
||||
////err = l.svcCtx.NotificationUseCase.SendMessage(l.ctx, notificationModule.SMSMessageRequest{
|
||||
//// PhoneNumber: acc,
|
||||
//// RecipientName: nickname,
|
||||
//// MessageContent: fmt.Sprintf("%s:%s", templateResp.Body, vcode),
|
||||
////})
|
||||
////if err != nil {
|
||||
//// return nil, err
|
||||
////}
|
||||
case mbr.GenerateCodeTypeNone:
|
||||
case mbr.GenerateCodeTypeForgetPassword:
|
||||
default:
|
||||
return &types.RespOK{}, errs.InputInvalidRangeError("")
|
||||
}
|
||||
|
||||
// 設置 Redis 鍵,並設置 3 分鐘的過期時間
|
||||
status, err := l.svcCtx.Redis.SetnxExCtx(l.ctx, rk, vcode.Data.VerifyCode, 60*3)
|
||||
if err != nil || !status {
|
||||
// 純記錄,前面都已經成功,就不報錯了
|
||||
_ = errs.DBErrorErrorL(l.svcCtx.Logger,
|
||||
[]errs.LogField{
|
||||
{Key: "req", Val: req},
|
||||
{Key: "func", Val: "Redis.SetnxExCtx"},
|
||||
{Key: "err", Val: err.Error()},
|
||||
}, "failed to set redis expire").Wrap(err)
|
||||
}
|
||||
|
||||
return &types.RespOK{}, nil
|
||||
}
|
||||
|
||||
// 標準化號碼並驗證是否為合法台灣手機號碼
|
||||
func normalizeTaiwanMobile(phone string) (string, bool) {
|
||||
// 移除空格
|
||||
phone = strings.ReplaceAll(phone, " ", "")
|
||||
|
||||
// 移除 "+886" 並將剩餘部分標準化
|
||||
if strings.HasPrefix(phone, "+886") {
|
||||
phone = strings.TrimPrefix(phone, "+886")
|
||||
if !strings.HasPrefix(phone, "0") {
|
||||
phone = "0" + phone
|
||||
}
|
||||
}
|
||||
|
||||
// 正則表達式驗證標準化後的號碼
|
||||
regex := regexp.MustCompile(`^(09\d{8})$`)
|
||||
if regex.MatchString(phone) {
|
||||
return phone, true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// 驗證 Email 格式的函數
|
||||
func isValidEmail(email string) bool {
|
||||
// 定義正則表達式
|
||||
regex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
||||
|
||||
return regex.MatchString(email)
|
||||
}
|
||||
|
||||
// generateMsgName 取得寄信用的名稱
|
||||
func generateMsgName(info *member.UserInfo) string {
|
||||
if info.FullName != nil {
|
||||
return *info.FullName
|
||||
}
|
||||
if info.Nickname != nil {
|
||||
return *info.Nickname
|
||||
}
|
||||
|
||||
return info.UID
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"backend/internal/svc"
|
||||
"backend/internal/types"
|
||||
errs "backend/pkg/library/errors"
|
||||
mbr "backend/pkg/member/domain/member"
|
||||
member "backend/pkg/member/domain/usecase"
|
||||
"backend/pkg/permission/domain/token"
|
||||
"context"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
|
@ -18,7 +15,7 @@ type SubmitVerificationCodeLogic struct {
|
|||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// NewSubmitVerificationCodeLogic 交驗證碼以完成驗證
|
||||
// 提交驗證碼以完成驗證
|
||||
func NewSubmitVerificationCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SubmitVerificationCodeLogic {
|
||||
return &SubmitVerificationCodeLogic{
|
||||
Logger: logx.WithContext(ctx),
|
||||
|
|
@ -27,66 +24,8 @@ func NewSubmitVerificationCodeLogic(ctx context.Context, svcCtx *svc.ServiceCont
|
|||
}
|
||||
}
|
||||
|
||||
func (l *SubmitVerificationCodeLogic) SubmitVerificationCode(req *types.SubmitVerificationCodeReq) (*types.RespOK, error) {
|
||||
acc := ""
|
||||
ct := mbr.GenerateCodeTypeEmail
|
||||
switch req.Purpose {
|
||||
case "email_verification":
|
||||
if !isValidEmail(req.Account) {
|
||||
return nil, errs.InputInvalidFormatError("email is invalid")
|
||||
}
|
||||
acc = req.Account
|
||||
case "phone_verification":
|
||||
phone, isPhone := normalizeTaiwanMobile(req.Account)
|
||||
if !isPhone {
|
||||
return nil, errs.InputInvalidFormatError("phone number is invalid")
|
||||
}
|
||||
acc = phone
|
||||
ct = mbr.GenerateCodeTypePhone
|
||||
default:
|
||||
return &types.RespOK{}, errs.InputInvalidRangeError("")
|
||||
}
|
||||
func (l *SubmitVerificationCodeLogic) SubmitVerificationCode(req *types.SubmitVerificationCodeReq) (resp *types.RespOK, err error) {
|
||||
// todo: add your logic here and delete this line
|
||||
|
||||
// 先驗證,不刪除
|
||||
if err := l.svcCtx.AccountUC.CheckRefreshCode(l.ctx, member.VerifyRefreshCodeRequest{
|
||||
VerifyCode: req.VerifyCode,
|
||||
LoginID: acc,
|
||||
CodeType: ct,
|
||||
}); err != nil {
|
||||
e := errs.AuthForbiddenError("failed to get verify code").Wrap(err)
|
||||
|
||||
return nil, e
|
||||
}
|
||||
uid := token.UID(l.ctx)
|
||||
switch req.Purpose {
|
||||
case "email_verification":
|
||||
err := l.svcCtx.AccountUC.BindVerifyEmail(l.ctx, uid, acc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "phone_verification":
|
||||
err := l.svcCtx.AccountUC.BindVerifyPhone(l.ctx, uid, acc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return &types.RespOK{}, errs.InputInvalidRangeError("")
|
||||
}
|
||||
|
||||
err := l.svcCtx.AccountUC.UpdateStatus(l.ctx, member.UpdateStatusRequest{
|
||||
UID: uid,
|
||||
Status: mbr.AccountStatusActive,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 真的刪除
|
||||
_ = l.svcCtx.AccountUC.VerifyRefreshCode(l.ctx, member.VerifyRefreshCodeRequest{
|
||||
VerifyCode: req.VerifyCode,
|
||||
LoginID: acc,
|
||||
CodeType: ct,
|
||||
})
|
||||
|
||||
return &types.RespOK{}, nil
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,10 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"backend/internal/svc"
|
||||
"backend/internal/types"
|
||||
errs "backend/pkg/library/errors"
|
||||
mbr "backend/pkg/member/domain/member"
|
||||
member "backend/pkg/member/domain/usecase"
|
||||
tokeneEntity "backend/pkg/permission/domain/entity"
|
||||
"backend/pkg/permission/domain/token"
|
||||
"context"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
|
@ -19,7 +15,7 @@ type UpdatePasswordLogic struct {
|
|||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// NewUpdatePasswordLogic 修改當前登入使用者的密碼
|
||||
// 修改當前登入使用者的密碼
|
||||
func NewUpdatePasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdatePasswordLogic {
|
||||
return &UpdatePasswordLogic{
|
||||
Logger: logx.WithContext(ctx),
|
||||
|
|
@ -28,41 +24,8 @@ func NewUpdatePasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Up
|
|||
}
|
||||
}
|
||||
|
||||
func (l *UpdatePasswordLogic) UpdatePassword(req *types.UpdatePasswordReq) (*types.RespOK, error) {
|
||||
if req.NewPassword != req.NewPasswordConfirm {
|
||||
return nil, errs.InputInvalidFormatError("failed to check token")
|
||||
}
|
||||
loginID := token.LoginID(l.ctx)
|
||||
// 驗證是否本地平台
|
||||
info, err := l.svcCtx.AccountUC.GetUserAccountInfo(l.ctx, member.GetUIDByAccountRequest{
|
||||
Account: loginID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func (l *UpdatePasswordLogic) UpdatePassword(req *types.UpdatePasswordReq) (resp *types.RespOK, err error) {
|
||||
// todo: add your logic here and delete this line
|
||||
|
||||
if info.Data.Platform != mbr.Digimon {
|
||||
return nil, errs.InputInvalidFormatError("failed th change password via third party login")
|
||||
}
|
||||
|
||||
// 驗證舊密碼是否正確
|
||||
if _, err := l.svcCtx.AccountUC.VerifyPlatformAuthResult(l.ctx, member.VerifyAuthResultRequest{
|
||||
Account: loginID,
|
||||
Token: req.CurrentPassword,
|
||||
}); err != nil {
|
||||
return nil, errs.AuthForbiddenError("failed to verify correct password")
|
||||
}
|
||||
|
||||
// 更新
|
||||
err = l.svcCtx.AccountUC.UpdateUserToken(l.ctx, member.UpdateTokenRequest{
|
||||
Account: loginID,
|
||||
Token: req.NewPassword,
|
||||
Platform: mbr.Digimon.ToInt64(),
|
||||
})
|
||||
|
||||
_ = l.svcCtx.TokenUC.CancelToken(l.ctx, tokeneEntity.CancelTokenReq{
|
||||
Token: req.Authorization.Authorization,
|
||||
})
|
||||
|
||||
return &types.RespOK{}, nil
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,7 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
errs "backend/pkg/library/errors"
|
||||
mbr "backend/pkg/member/domain/member"
|
||||
member "backend/pkg/member/domain/usecase"
|
||||
"backend/pkg/permission/domain/token"
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"backend/internal/svc"
|
||||
"backend/internal/types"
|
||||
|
|
@ -22,7 +15,7 @@ type UpdateUserInfoLogic struct {
|
|||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// NewUpdateUserInfoLogic 更新當前登入的會員資訊
|
||||
// 更新當前登入的會員資訊
|
||||
func NewUpdateUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateUserInfoLogic {
|
||||
return &UpdateUserInfoLogic{
|
||||
Logger: logx.WithContext(ctx),
|
||||
|
|
@ -32,108 +25,7 @@ func NewUpdateUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Up
|
|||
}
|
||||
|
||||
func (l *UpdateUserInfoLogic) UpdateUserInfo(req *types.UpdateUserInfoReq) (resp *types.UserInfoResp, err error) {
|
||||
update, err := ConvertBindingUserInfoToUpdateRequest(token.UID(l.ctx), req)
|
||||
if err != nil {
|
||||
return nil, errs.InputInvalidFormatError("failed to get correct user info", err.Error())
|
||||
}
|
||||
// todo: add your logic here and delete this line
|
||||
|
||||
err = l.svcCtx.AccountUC.UpdateUserInfo(l.ctx, update)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, err := l.svcCtx.AccountUC.GetUserInfo(l.ctx, member.GetUserInfoRequest{
|
||||
UID: token.UID(l.ctx),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accountInfo, err := l.svcCtx.AccountUC.GetUserAccountInfo(l.ctx, member.GetUIDByAccountRequest{Account: token.LoginID(l.ctx)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := &types.UserInfoResp{
|
||||
UID: token.UID(l.ctx),
|
||||
Platform: accountInfo.Data.Platform.ToString(),
|
||||
UserStatus: info.UserStatus.CodeToString(),
|
||||
PreferredLanguage: info.PreferredLanguage,
|
||||
Currency: info.Currency,
|
||||
UpdateAt: time.Unix(0, info.CreateTime).UTC().Format(time.RFC3339),
|
||||
CreateAt: time.Unix(0, info.UpdateTime).UTC().Format(time.RFC3339),
|
||||
//Role string `json:"role"`
|
||||
}
|
||||
if info.Address != nil {
|
||||
res.Address = *info.Address
|
||||
}
|
||||
if info.AvatarURL != nil {
|
||||
res.AvatarURL = *info.AvatarURL
|
||||
}
|
||||
if info.FullName != nil {
|
||||
res.FullName = *info.FullName
|
||||
}
|
||||
if info.PhoneNumber != nil {
|
||||
res.PhoneNumber = *info.PhoneNumber
|
||||
res.IsPhoneVerified = true
|
||||
}
|
||||
if info.Nickname != nil {
|
||||
res.Nickname = *info.Nickname
|
||||
}
|
||||
if info.Email != nil {
|
||||
res.Email = *info.Email
|
||||
res.IsEmailVerified = true
|
||||
}
|
||||
if info.GenderCode != nil {
|
||||
res.GenderCode = mbr.GetGenderByCode(*info.GenderCode)
|
||||
}
|
||||
if info.Birthdate != nil {
|
||||
res.Birthdate = toStringStr(info.Birthdate)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func toStringStr(n *int64) string {
|
||||
result := ""
|
||||
if n != nil {
|
||||
result = time.Unix(*n, 0).UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func ConvertBindingUserInfoToUpdateRequest(uid string, bindingInfo *types.UpdateUserInfoReq) (*member.UpdateUserInfoRequest, error) {
|
||||
updateRequest := &member.UpdateUserInfoRequest{
|
||||
UID: uid,
|
||||
AvatarURL: bindingInfo.AvatarURL,
|
||||
FullName: bindingInfo.FullName,
|
||||
Nickname: bindingInfo.Nickname,
|
||||
Address: bindingInfo.Address,
|
||||
PreferredLanguage: bindingInfo.PreferredLanguage,
|
||||
Currency: bindingInfo.Currency,
|
||||
}
|
||||
|
||||
// Convert GenderCode from string to *int8
|
||||
if &bindingInfo.GenderCode != nil {
|
||||
gender := mbr.GetGenderCodeByStr(*bindingInfo.GenderCode)
|
||||
// 檢查 gender 是否在 int8 範圍內
|
||||
if gender < math.MinInt8 || gender > math.MaxInt8 {
|
||||
return nil, fmt.Errorf("gender code %d is out of int8 range", gender)
|
||||
}
|
||||
genderInt8 := int8(gender)
|
||||
updateRequest.GenderCode = &genderInt8
|
||||
}
|
||||
|
||||
// Convert Birthdate from string to *int64
|
||||
if &bindingInfo.Birthdate != nil {
|
||||
parse, err := time.Parse(time.RFC3339, *bindingInfo.Birthdate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
date := parse.Unix()
|
||||
|
||||
updateRequest.Birthdate = &date
|
||||
}
|
||||
|
||||
return updateRequest, nil
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,86 +1,19 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"backend/internal/types"
|
||||
errs "backend/pkg/library/errors"
|
||||
"backend/pkg/permission/domain/entity"
|
||||
"backend/pkg/permission/domain/token"
|
||||
"context"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
|
||||
"backend/pkg/permission/domain/usecase"
|
||||
uc "backend/pkg/permission/usecase"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type AuthMiddlewareParam struct {
|
||||
TokenSec string
|
||||
TokenUseCase usecase.TokenUseCase
|
||||
}
|
||||
import "net/http"
|
||||
|
||||
type AuthMiddleware struct {
|
||||
TokenSec string
|
||||
TokenUseCase usecase.TokenUseCase
|
||||
}
|
||||
|
||||
func NewAuthMiddleware(param AuthMiddlewareParam) *AuthMiddleware {
|
||||
return &AuthMiddleware{
|
||||
TokenSec: param.TokenSec,
|
||||
TokenUseCase: param.TokenUseCase,
|
||||
}
|
||||
func NewAuthMiddleware() *AuthMiddleware {
|
||||
return &AuthMiddleware{}
|
||||
}
|
||||
|
||||
func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// 解析 Header
|
||||
header := types.Authorization{}
|
||||
if err := httpx.ParseHeaders(r, &header); err != nil {
|
||||
e := errs.AuthInvalidPosixTimeError("failed to parse headers")
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: e.Error(),
|
||||
})
|
||||
// TODO generate middleware implement function, delete after code implementation
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 驗證 Token
|
||||
claim, err := uc.ParseClaims(header.Authorization, m.TokenSec, true)
|
||||
if err != nil {
|
||||
|
||||
e := errs.AuthInvalidPosixTimeError(err.Error())
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: e.Error(),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 驗證 Token 是否在黑名單中
|
||||
if _, err := m.TokenUseCase.ValidationToken(r.Context(), entity.ValidationTokenReq{Token: header.Authorization}); err != nil {
|
||||
e := errs.AuthUnauthorizedError("failed to use this token")
|
||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||
Code: e.DisplayCode(),
|
||||
Message: e.Error(),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 設置 context 並傳遞給下一個處理器
|
||||
ctx := SetContext(r, claim)
|
||||
next(w, r.WithContext(ctx))
|
||||
// Passthrough to next handler if need
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func SetContext(r *http.Request, claim uc.TokenClaims) context.Context {
|
||||
ctx := context.WithValue(r.Context(), token.KeyRole, claim.Role())
|
||||
ctx = context.WithValue(ctx, token.KeyUID, claim.UID())
|
||||
ctx = context.WithValue(ctx, token.KeyDeviceID, claim.DeviceID())
|
||||
ctx = context.WithValue(ctx, token.KeyScope, claim.Scope())
|
||||
ctx = context.WithValue(ctx, token.KeyLoginID, claim.LoginID())
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ import (
|
|||
uc "backend/pkg/member/usecase"
|
||||
"context"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/stores/cache"
|
||||
"github.com/zeromicro/go-zero/core/stores/mon"
|
||||
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||
|
|
@ -81,7 +79,6 @@ func NewAccountUC(c *config.Config, rds *redis.Redis) usecase.AccountUseCase {
|
|||
VerifyCodeModel: repository.NewVerifyCodeRepository(rds),
|
||||
GenerateUID: guid,
|
||||
Config: prepareCfg(c),
|
||||
Logger: MustLogger(logx.WithContext(context.Background())),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
package svc
|
||||
|
||||
import (
|
||||
"backend/internal/config"
|
||||
fileStorageConfig "backend/pkg/fileStorage/config"
|
||||
fileStorageUC "backend/pkg/fileStorage/domain/usecase"
|
||||
fileStorageRepo "backend/pkg/fileStorage/repository"
|
||||
fileStorageUseCase "backend/pkg/fileStorage/usecase"
|
||||
errs "backend/pkg/library/errors"
|
||||
)
|
||||
|
||||
func MustS3Storage(c *config.Config, logger errs.Logger) fileStorageUC.FileStorageUseCase {
|
||||
// 初始化 FileStorage 配置
|
||||
fileStorageConf := &fileStorageConfig.Config{
|
||||
AmazonS3Settings: struct {
|
||||
Region string
|
||||
Bucket string
|
||||
CloudFrontDomain string
|
||||
CloudFrontURI string
|
||||
BucketURI string
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
CloudFrontID string
|
||||
}{
|
||||
Region: c.AmazonS3Settings.Region,
|
||||
Bucket: c.AmazonS3Settings.Bucket,
|
||||
CloudFrontDomain: c.AmazonS3Settings.CloudFrontDomain,
|
||||
CloudFrontURI: c.AmazonS3Settings.CloudFrontURI,
|
||||
BucketURI: c.AmazonS3Settings.BucketURI,
|
||||
AccessKey: c.AmazonS3Settings.AccessKey,
|
||||
SecretKey: c.AmazonS3Settings.SecretKey,
|
||||
CloudFrontID: c.AmazonS3Settings.CloudFrontID,
|
||||
},
|
||||
}
|
||||
|
||||
// 初始化 FileStorage Repository 和 UseCase
|
||||
fileStorageRepoInstance := fileStorageRepo.MustAwsS3FileStorageRepo(fileStorageRepo.AwsS3FileStorageRepositoryParam{
|
||||
Conf: fileStorageConf,
|
||||
Logger: logger,
|
||||
})
|
||||
fileStorageUCInstance := fileStorageUseCase.MustAwsS3FileStorageUseCase(fileStorageUseCase.AwsS3FileStorageUseCaseParam{
|
||||
Conf: fileStorageConf,
|
||||
Logger: logger,
|
||||
Repo: fileStorageRepoInstance,
|
||||
})
|
||||
|
||||
return fileStorageUCInstance
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
package svc
|
||||
|
||||
import (
|
||||
errs "backend/pkg/library/errors"
|
||||
"context"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type logger struct {
|
||||
l logx.Logger
|
||||
}
|
||||
|
||||
func (lgr *logger) WithCallerSkip(skip int) errs.Logger {
|
||||
return &logger{
|
||||
l: logx.WithContext(context.Background()).WithCallerSkip(skip),
|
||||
}
|
||||
}
|
||||
|
||||
func (lgr *logger) WithFields(fields ...errs.LogField) errs.Logger {
|
||||
return &logger{
|
||||
l: logx.WithContext(context.Background()).WithFields(fTof(fields)...),
|
||||
}
|
||||
}
|
||||
|
||||
func (lgr *logger) Error(msg string) {
|
||||
lgr.l.Error(msg)
|
||||
}
|
||||
|
||||
func (lgr *logger) Info(msg string) {
|
||||
lgr.l.Info(msg)
|
||||
}
|
||||
|
||||
func (lgr *logger) Warn(msg string) {
|
||||
lgr.l.Error(msg)
|
||||
}
|
||||
|
||||
func MustLogger(log logx.Logger) errs.Logger {
|
||||
return &logger{
|
||||
l: log,
|
||||
}
|
||||
}
|
||||
|
||||
func fTof(field []errs.LogField) []logx.LogField {
|
||||
f := make([]logx.LogField, 0, len(field)+1)
|
||||
for _, v := range field {
|
||||
f = append(f, logx.LogField{
|
||||
Key: v.Key,
|
||||
Value: v.Val,
|
||||
})
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
package svc
|
||||
|
||||
import (
|
||||
"backend/internal/config"
|
||||
errs "backend/pkg/library/errors"
|
||||
"backend/pkg/notification/domain/usecase"
|
||||
uc "backend/pkg/notification/usecase"
|
||||
|
||||
cfg "backend/pkg/notification/config"
|
||||
"backend/pkg/notification/domain/repository"
|
||||
rp "backend/pkg/notification/repository"
|
||||
)
|
||||
|
||||
func MustSMTPEmailSender(c *config.Config) repository.MailRepository {
|
||||
return rp.MustSMTPUseCase(rp.SMTPMailUseCaseParam{
|
||||
Conf: cfg.SMTPConfig{
|
||||
Enable: c.SMTPConfig.Enable,
|
||||
Sort: 1,
|
||||
GoroutinePoolNum: c.SMTPConfig.GoroutinePoolNum,
|
||||
Host: c.SMTPConfig.Host,
|
||||
Port: c.SMTPConfig.Port,
|
||||
Username: c.SMTPConfig.Username,
|
||||
Password: c.SMTPConfig.Password,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func MustAwsEmailSender(c *config.Config) repository.MailRepository {
|
||||
return rp.MustAwsSesMailRepository(rp.AwsEmailDeliveryParam{
|
||||
Conf: &cfg.AmazonSesSettings{},
|
||||
})
|
||||
}
|
||||
|
||||
func MustSMS(c *config.Config) repository.SMSClientRepository {
|
||||
return rp.MustMitakeRepository(rp.MitakeSMSDeliveryParam{
|
||||
Conf: &cfg.MitakeSMSSender{},
|
||||
})
|
||||
}
|
||||
|
||||
func MustDeliveryUseCase(c *config.Config, logger errs.Logger) usecase.DeliveryUseCase {
|
||||
emailProviders := make([]usecase.EmailProvider, 0, 10)
|
||||
smsProviders := make([]usecase.SMSProvider, 0)
|
||||
//smsProviders = append(smsProviders, usecase.SMSProvider{
|
||||
// Sort: 1,
|
||||
// Repo: MustSMS(c),
|
||||
//})
|
||||
|
||||
//emailProviders = append(emailProviders, usecase.EmailProvider{
|
||||
// Sort: 2,
|
||||
// Repo: MustAwsEmailSender(c),
|
||||
//})
|
||||
|
||||
emailProviders = append(emailProviders, usecase.EmailProvider{
|
||||
Sort: 1,
|
||||
Repo: MustSMTPEmailSender(c),
|
||||
})
|
||||
|
||||
return uc.MustDeliveryUseCase(uc.DeliveryUseCaseParam{
|
||||
SMSProviders: smsProviders,
|
||||
EmailProviders: emailProviders,
|
||||
DeliveryConfig: cfg.DeliveryConfig{
|
||||
MaxRetries: c.DeliveryConfig.MaxRetries,
|
||||
InitialDelay: c.DeliveryConfig.InitialDelay,
|
||||
BackoffFactor: c.DeliveryConfig.BackoffFactor,
|
||||
MaxDelay: c.DeliveryConfig.MaxDelay,
|
||||
Timeout: c.DeliveryConfig.Timeout,
|
||||
EnableHistory: false,
|
||||
},
|
||||
Logger: logger,
|
||||
})
|
||||
}
|
||||
|
|
@ -3,16 +3,10 @@ package svc
|
|||
import (
|
||||
"backend/internal/config"
|
||||
"backend/internal/middleware"
|
||||
errs "backend/pkg/library/errors"
|
||||
"backend/pkg/library/errors/code"
|
||||
"context"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
|
||||
fileStorageUC "backend/pkg/fileStorage/domain/usecase"
|
||||
"backend/pkg/library/errs"
|
||||
"backend/pkg/library/errs/code"
|
||||
vi "backend/pkg/library/validator"
|
||||
memberUC "backend/pkg/member/domain/usecase"
|
||||
deliveryUC "backend/pkg/notification/domain/usecase"
|
||||
tokenUC "backend/pkg/permission/domain/usecase"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||
|
|
@ -25,14 +19,6 @@ type ServiceContext struct {
|
|||
AccountUC memberUC.AccountUseCase
|
||||
Validate vi.Validate
|
||||
TokenUC tokenUC.TokenUseCase
|
||||
PermissionUC tokenUC.PermissionUseCase
|
||||
RoleUC tokenUC.RoleUseCase
|
||||
RolePermission tokenUC.RolePermissionUseCase
|
||||
UserRoleUC tokenUC.UserRoleUseCase
|
||||
DeliveryUC deliveryUC.DeliveryUseCase
|
||||
FileStorageUC fileStorageUC.FileStorageUseCase
|
||||
Redis *redis.Redis
|
||||
Logger errs.Logger
|
||||
}
|
||||
|
||||
func NewServiceContext(c config.Config) *ServiceContext {
|
||||
|
|
@ -40,28 +26,13 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
|||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
errs.Scope = code.Gateway
|
||||
|
||||
rp := NewPermissionUC(&c)
|
||||
tkUC := NewTokenUC(&c, rds)
|
||||
lgr := MustLogger(logx.WithContext(context.Background()))
|
||||
errs.Scope = code.CloudEPPortalGW
|
||||
|
||||
return &ServiceContext{
|
||||
Config: c,
|
||||
AuthMiddleware: middleware.NewAuthMiddleware(middleware.AuthMiddlewareParam{
|
||||
TokenSec: c.Token.AccessSecret,
|
||||
TokenUseCase: tkUC,
|
||||
}).Handle,
|
||||
Config: c,
|
||||
AuthMiddleware: middleware.NewAuthMiddleware().Handle,
|
||||
AccountUC: NewAccountUC(&c, rds),
|
||||
Validate: vi.MustValidator(),
|
||||
TokenUC: tkUC,
|
||||
PermissionUC: rp.PermissionUC,
|
||||
RoleUC: rp.RoleUC,
|
||||
RolePermission: rp.RolePermission,
|
||||
UserRoleUC: rp.UserRole,
|
||||
Redis: rds,
|
||||
DeliveryUC: MustDeliveryUseCase(&c, lgr),
|
||||
FileStorageUC: MustS3Storage(&c, lgr),
|
||||
Logger: lgr,
|
||||
TokenUC: NewTokenUC(&c, rds),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,9 @@ package svc
|
|||
|
||||
import (
|
||||
"backend/internal/config"
|
||||
mgo "backend/pkg/library/mongo"
|
||||
"backend/pkg/permission/domain/usecase"
|
||||
"backend/pkg/permission/repository"
|
||||
uc "backend/pkg/permission/usecase"
|
||||
"context"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
"github.com/zeromicro/go-zero/core/stores/cache"
|
||||
"github.com/zeromicro/go-zero/core/stores/mon"
|
||||
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||
)
|
||||
|
||||
|
|
@ -20,105 +14,5 @@ func NewTokenUC(c *config.Config, rds *redis.Redis) usecase.TokenUseCase {
|
|||
Redis: rds,
|
||||
}),
|
||||
Config: c,
|
||||
Logger: MustLogger(logx.WithContext(context.Background())),
|
||||
})
|
||||
}
|
||||
|
||||
type PermissionUC struct {
|
||||
PermissionUC usecase.PermissionUseCase
|
||||
RoleUC usecase.RoleUseCase
|
||||
RolePermission usecase.RolePermissionUseCase
|
||||
UserRole usecase.UserRoleUseCase
|
||||
}
|
||||
|
||||
func NewPermissionUC(c *config.Config) PermissionUC {
|
||||
// 準備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),
|
||||
}
|
||||
permRepo := repository.NewPermissionRepository(repository.PermissionRepositoryParam{
|
||||
Conf: conf,
|
||||
CacheConf: c.Cache,
|
||||
CacheOpts: cacheOpts,
|
||||
DBOpts: dbOpts,
|
||||
})
|
||||
|
||||
rolePermRepo := repository.NewRolePermissionRepository(repository.RolePermissionRepositoryParam{
|
||||
Conf: conf,
|
||||
CacheConf: c.Cache,
|
||||
CacheOpts: cacheOpts,
|
||||
DBOpts: dbOpts,
|
||||
})
|
||||
|
||||
roleRepo := repository.NewRoleRepository(repository.RoleRepositoryParam{
|
||||
Conf: conf,
|
||||
CacheConf: c.Cache,
|
||||
CacheOpts: cacheOpts,
|
||||
DBOpts: dbOpts,
|
||||
})
|
||||
|
||||
userRoleRepo := repository.NewUserRoleRepository(repository.UserRoleRepositoryParam{
|
||||
Conf: conf,
|
||||
CacheConf: c.Cache,
|
||||
CacheOpts: cacheOpts,
|
||||
DBOpts: dbOpts,
|
||||
})
|
||||
|
||||
puc := uc.NewPermissionUseCase(uc.PermissionUseCaseParam{
|
||||
RoleRepo: roleRepo,
|
||||
RolePermRepo: rolePermRepo,
|
||||
UserRoleRepo: userRoleRepo,
|
||||
PermRepo: permRepo,
|
||||
})
|
||||
rpuc := uc.NewRolePermissionUseCase(uc.RolePermissionUseCaseParam{
|
||||
RoleRepo: roleRepo,
|
||||
RolePermRepo: rolePermRepo,
|
||||
UserRoleRepo: userRoleRepo,
|
||||
PermRepo: permRepo,
|
||||
PermUseCase: puc,
|
||||
AdminRoleUID: c.RoleConfig.AdminRoleUID,
|
||||
})
|
||||
ruc := uc.NewRoleUseCase(uc.RoleUseCaseParam{
|
||||
RoleRepo: roleRepo,
|
||||
UserRoleRepo: userRoleRepo,
|
||||
Config: uc.RoleUseCaseConfig{
|
||||
AdminRoleUID: c.RoleConfig.AdminRoleUID,
|
||||
UIDPrefix: c.RoleConfig.UIDPrefix,
|
||||
UIDLength: c.RoleConfig.UIDLength,
|
||||
},
|
||||
RolePermUseCase: rpuc,
|
||||
})
|
||||
|
||||
return PermissionUC{
|
||||
PermissionUC: puc,
|
||||
RolePermission: rpuc,
|
||||
RoleUC: ruc,
|
||||
UserRole: uc.NewUserRoleUseCase(uc.UserRoleUseCaseParam{
|
||||
UserRoleRepo: userRoleRepo,
|
||||
RoleRepo: roleRepo,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl 1.9.0
|
||||
// goctl 1.8.1
|
||||
|
||||
package types
|
||||
|
||||
|
|
@ -16,6 +16,13 @@ type CredentialsPayload struct {
|
|||
AccountType string `json:"account_type" validate:"required,oneof=email phone any"` // 帳號型別 email phone any
|
||||
}
|
||||
|
||||
type ErrorResp struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Details string `json:"details,omitempty"`
|
||||
Error interface{} `json:"error,omitempty"` // 可選的錯誤信息
|
||||
}
|
||||
|
||||
type LoginReq struct {
|
||||
AuthMethod string `json:"auth_method" validate:"required,oneof=credentials platform"` // 驗證類型 credentials platform
|
||||
LoginID string `json:"login_id" validate:"required,min=3,max=50"` // 信箱或手機號碼
|
||||
|
|
@ -30,30 +37,6 @@ type LoginResp struct {
|
|||
TokenType string `json:"token_type"` // 通常固定為 "Bearer"
|
||||
}
|
||||
|
||||
type MyInfo struct {
|
||||
Platform string `json:"platform"` // 註冊平台
|
||||
UID string `json:"uid"` // 用戶 UID
|
||||
AvatarURL *string `json:"avatar_url,omitempty"` // 頭像 URL
|
||||
FullName *string `json:"full_name,omitempty"` // 用戶全名
|
||||
Nickname *string `json:"nickname,omitempty"` // 暱稱
|
||||
GenderCode *string `json:"gender_code,omitempty"` // 性別代碼
|
||||
Birthdate *string `json:"birthdate,omitempty"` // 生日 (格式: 1993-04-17)
|
||||
PhoneNumber *string `json:"phone_number,omitempty"` // 電話
|
||||
IsPhoneVerified *bool `json:"is_phone_verified,omitempty"` // 手機是否已驗證
|
||||
Email *string `json:"email,omitempty"` // 信箱
|
||||
IsEmailVerified *bool `json:"is_email_verified,omitempty"` // 信箱是否已驗證
|
||||
Address *string `json:"address,omitempty"` // 地址
|
||||
UserStatus string `json:"user_status,omitempty"` // 用戶狀態
|
||||
PreferredLanguage string `json:"preferred_language,omitempty"` // 偏好語言
|
||||
Currency string `json:"currency,omitempty"` // 偏好幣種
|
||||
AlarmCategory string `json:"alarm_category,omitempty"` // 告警狀態
|
||||
PostCode *string `json:"post_code,omitempty"` // 郵遞區號
|
||||
Carrier *string `json:"carrier,omitempty"` // 載具
|
||||
Role string `json:"role"` // 角色
|
||||
UpdateAt string `json:"update_at"`
|
||||
CreateAt string `json:"create_at"`
|
||||
}
|
||||
|
||||
type PagerResp struct {
|
||||
Total int64 `json:"total"`
|
||||
Size int64 `json:"size"`
|
||||
|
|
@ -77,12 +60,11 @@ type RefreshTokenResp struct {
|
|||
}
|
||||
|
||||
type RequestPasswordResetReq struct {
|
||||
Identifier string `json:"identifier" validate:"required"` // 使用者帳號 (信箱或手機)
|
||||
Identifier string `json:"identifier" validate:"required,email|phone"` // 使用者帳號 (信箱或手機)
|
||||
AccountType string `json:"account_type" validate:"required,oneof=email phone"`
|
||||
}
|
||||
|
||||
type RequestVerificationCodeReq struct {
|
||||
Account string `json:"account" validate:"required`
|
||||
Purpose string `json:"purpose" validate:"required,oneof=email_verification phone_verification"`
|
||||
Authorization
|
||||
}
|
||||
|
|
@ -94,18 +76,13 @@ type ResetPasswordReq struct {
|
|||
PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password"` // 確認新密碼
|
||||
}
|
||||
|
||||
type Resp struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Error interface{} `json:"error,omitempty"` // 可選的錯誤信息
|
||||
}
|
||||
|
||||
type RespOK struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type SubmitVerificationCodeReq struct {
|
||||
Account string `json:"account" validate:"required`
|
||||
Purpose string `json:"purpose" validate:"required,oneof=email_verification phone_verification"`
|
||||
VerifyCode string `json:"verify_code" validate:"required,len=6"`
|
||||
Authorization
|
||||
|
|
@ -132,21 +109,6 @@ type UpdateUserInfoReq struct {
|
|||
Carrier *string `json:"carrier,optional"` // 載具
|
||||
}
|
||||
|
||||
type UploadImgReq struct {
|
||||
Authorization
|
||||
Content string `json:"content" validate:"required"` // base64 編碼的圖片內容
|
||||
}
|
||||
|
||||
type UploadResp struct {
|
||||
FileUrl string `json:"file_url"` // 文件訪問 URL
|
||||
FileSize int64 `json:"file_size,optional"` // 文件大小(bytes)
|
||||
MimeType string `json:"mime_type,optional"` // MIME 類型
|
||||
}
|
||||
|
||||
type UploadVideoReq struct {
|
||||
Authorization
|
||||
}
|
||||
|
||||
type UserInfoResp struct {
|
||||
Platform string `json:"platform"` // 註冊平台
|
||||
UID string `json:"uid"` // 用戶 UID
|
||||
|
|
@ -163,9 +125,13 @@ type UserInfoResp struct {
|
|||
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 {
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
package email_template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Language string
|
||||
|
||||
const (
|
||||
LanguageZhTW Language = "zh_tw"
|
||||
LanguageEnUS Language = "en_us"
|
||||
)
|
||||
|
||||
type TypeID int64
|
||||
|
||||
func (id TypeID) String() string {
|
||||
return fmt.Sprintf("%4d", id)
|
||||
}
|
||||
|
||||
// 驗證碼通知類 0 ~ 100
|
||||
const (
|
||||
BindingEmail TypeID = 1 // 驗證碼:綁定 Email
|
||||
BindingPhone TypeID = 2 // 驗證碼:綁定 手機
|
||||
ForgetPasswordVerify TypeID = 3 // 驗證碼: 忘記密碼
|
||||
)
|
||||
|
||||
var EmailTemplateMap = map[Language]map[TypeID]func() (string, string, error){
|
||||
LanguageZhTW: {
|
||||
ForgetPasswordVerify: GenerateForgetPasswordEmailZHTW,
|
||||
BindingEmail: GenerateBindingEmailZHTW,
|
||||
},
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package email_template
|
||||
|
||||
import "fmt"
|
||||
|
||||
// GetEmailTemplate 取得指定的 Email 樣板
|
||||
func GetEmailTemplate(lang Language, typeID TypeID) (string, string, error) {
|
||||
// 查找指定語言的模板映射
|
||||
templateByLang, exists := EmailTemplateMap[lang]
|
||||
if !exists {
|
||||
return "", "", fmt.Errorf("email template not found for language: %s", lang)
|
||||
}
|
||||
|
||||
// 查找指定類型的模板生成函數
|
||||
templateFunc, exists := templateByLang[typeID]
|
||||
if !exists {
|
||||
return "", "", fmt.Errorf("email template not found for type ID: %s", typeID)
|
||||
}
|
||||
|
||||
// 執行模板生成函數
|
||||
return templateFunc()
|
||||
}
|
||||
|
|
@ -1,150 +0,0 @@
|
|||
package email_template
|
||||
|
||||
import "github.com/matcornic/hermes/v2"
|
||||
|
||||
// ProductInfo 包含產品相關的資訊,用於郵件模板中的產品展示部分
|
||||
type ProductInfo struct {
|
||||
Name string
|
||||
Link string
|
||||
Logo string
|
||||
Copyright string
|
||||
}
|
||||
|
||||
// EmailBodyContent 包含郵件正文的資訊,用於生成郵件的主要內容
|
||||
type EmailBodyContent struct {
|
||||
RecipientName string
|
||||
Intros []string
|
||||
Actions []hermes.Action
|
||||
Outros []string
|
||||
Signature string
|
||||
}
|
||||
|
||||
// ForgetPasswordEmailContentParams 包含生成忘記密碼郵件所需的參數
|
||||
type ForgetPasswordEmailContentParams struct {
|
||||
Product ProductInfo
|
||||
Content EmailBodyContent
|
||||
}
|
||||
|
||||
type ForgetPasswordEmailReq struct {
|
||||
Username string
|
||||
VerifyCode string
|
||||
}
|
||||
|
||||
type CooperateThanksEmailReq struct {
|
||||
Username string
|
||||
BusinessID string
|
||||
Rewards []RewardItem
|
||||
Country string
|
||||
TotalAmount string
|
||||
}
|
||||
|
||||
type RewardItem struct {
|
||||
Title string
|
||||
Content string
|
||||
Amount string
|
||||
//Add string
|
||||
}
|
||||
type CooperateUserEmailReq struct {
|
||||
BusinessID string `json:"business_id"` // 開案編號
|
||||
Name string `json:"name"` // 聯絡人姓名
|
||||
Phone string `json:"phone"` // 聯絡電話
|
||||
OrgName string `json:"org_name"` // 團體名稱
|
||||
Email string `json:"email"` // 電子信箱
|
||||
TargetAmount string `json:"target_amount"` // 目標募款金額
|
||||
HasPermit string `json:"has_permit"` // 是否有勸募字號
|
||||
FundRequest string `json:"fund_request"` // 募款需求
|
||||
}
|
||||
|
||||
const (
|
||||
nTw = "Digimon 團隊"
|
||||
link = "https://code.30cm.net"
|
||||
logo = "https://true-heart-dev.s3.ap-northeast-3.amazonaws.com/f70904eb-1a29-40f7-8940-9a124f23793a.png"
|
||||
cryptoTw = "© 2025~ Digimon Inc. 版權所有"
|
||||
)
|
||||
|
||||
// GenerateForgetPasswordEmailZHTW 生成繁體中文的忘記密碼驗證信
|
||||
func GenerateForgetPasswordEmailZHTW() (string, string, error) {
|
||||
req := ForgetPasswordEmailContentParams{
|
||||
Product: ProductInfo{
|
||||
Name: nTw,
|
||||
Link: link,
|
||||
Logo: logo,
|
||||
Copyright: cryptoTw,
|
||||
},
|
||||
Content: EmailBodyContent{
|
||||
RecipientName: "{{.Username}}",
|
||||
Intros: []string{
|
||||
"您收到此電子郵件是因為我們收到了針對帳戶的密碼重置請求。",
|
||||
},
|
||||
Actions: []hermes.Action{
|
||||
{
|
||||
Instructions: "請複製您的驗證碼,到網頁重置",
|
||||
InviteCode: "{{.VerifyCode}}",
|
||||
},
|
||||
},
|
||||
Outros: []string{
|
||||
"如果您不要求重設密碼,則無需您採取進一步的措施。",
|
||||
},
|
||||
Signature: "",
|
||||
},
|
||||
}
|
||||
|
||||
emailBody, err := buildForgetPasswordEmailContent(req)
|
||||
|
||||
return emailBody, "Digimon 重設密碼驗證信", err
|
||||
}
|
||||
|
||||
// GenerateBindingEmailZHTW 生成綁定帳號驗證信
|
||||
func GenerateBindingEmailZHTW() (string, string, error) {
|
||||
req := ForgetPasswordEmailContentParams{
|
||||
Product: ProductInfo{
|
||||
Name: nTw,
|
||||
Link: link,
|
||||
Logo: logo,
|
||||
Copyright: cryptoTw,
|
||||
},
|
||||
Content: EmailBodyContent{
|
||||
RecipientName: "{{.Username}}",
|
||||
Intros: []string{
|
||||
"您收到此電子郵件是因為我們收到了針對帳戶的Email認證請求。",
|
||||
},
|
||||
Actions: []hermes.Action{
|
||||
{
|
||||
Instructions: "請複製您的驗證碼,到網頁重置",
|
||||
InviteCode: "{{.VerifyCode}}",
|
||||
},
|
||||
},
|
||||
Outros: []string{
|
||||
"如果您不要求重設密碼,則無需您採取進一步的措施。",
|
||||
},
|
||||
Signature: "",
|
||||
},
|
||||
}
|
||||
|
||||
emailBody, err := buildForgetPasswordEmailContent(req)
|
||||
|
||||
return emailBody, "Digimon 綁定信箱驗證信", err
|
||||
}
|
||||
|
||||
// buildForgetPasswordEmailContent 根據參數生成忘記密碼郵件的產品和內容結構
|
||||
func buildForgetPasswordEmailContent(params ForgetPasswordEmailContentParams) (string, error) {
|
||||
product := hermes.Product{
|
||||
Name: params.Product.Name,
|
||||
Link: params.Product.Link,
|
||||
Logo: params.Product.Logo,
|
||||
Copyright: params.Product.Copyright,
|
||||
}
|
||||
|
||||
body := hermes.Body{
|
||||
Name: params.Content.RecipientName,
|
||||
Intros: params.Content.Intros,
|
||||
Actions: params.Content.Actions,
|
||||
Outros: params.Content.Outros,
|
||||
Signature: params.Content.Signature,
|
||||
}
|
||||
|
||||
h := hermes.Hermes{Product: product}
|
||||
email := hermes.Email{Body: body}
|
||||
|
||||
return h.GenerateHTML(email)
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NormalizeTaiwanMobile 標準化號碼並驗證是否為合法台灣手機號碼
|
||||
func NormalizeTaiwanMobile(phone string) (string, bool) {
|
||||
// 移除空格
|
||||
phone = strings.ReplaceAll(phone, " ", "")
|
||||
|
||||
// 移除 "+886" 並將剩餘部分標準化
|
||||
if strings.HasPrefix(phone, "+886") {
|
||||
phone = strings.TrimPrefix(phone, "+886")
|
||||
if !strings.HasPrefix(phone, "0") {
|
||||
phone = "0" + phone
|
||||
}
|
||||
}
|
||||
|
||||
// 正則表達式驗證標準化後的號碼
|
||||
regex := regexp.MustCompile(`^(09\d{8})$`)
|
||||
if regex.MatchString(phone) {
|
||||
return phone, true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// IsValidEmail 驗證 Email 格式的函數
|
||||
func IsValidEmail(email string) bool {
|
||||
// 定義正則表達式
|
||||
regex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
||||
|
||||
return regex.MatchString(email)
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
package config
|
||||
|
||||
type Config struct {
|
||||
AmazonS3Settings struct {
|
||||
Region string
|
||||
Bucket string
|
||||
CloudFrontDomain string
|
||||
CloudFrontURI string
|
||||
BucketURI string
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
CloudFrontID string
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
package domain
|
||||
|
||||
const (
|
||||
S3AclPublic = "public-read"
|
||||
S3AclPrivate = "private"
|
||||
)
|
||||
|
||||
var S3AclSetting = S3AclPublic
|
||||
|
||||
func SetACLIsPublic() {
|
||||
S3AclSetting = S3AclPublic
|
||||
}
|
||||
|
||||
func SetACLIsPrivate() {
|
||||
S3AclSetting = S3AclPrivate
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FileStorageRepository 是一個通用的文件儲存操作接口,用於管理雲存儲或本地存儲中的對象文件。
|
||||
type FileStorageRepository interface {
|
||||
// Download 下載指定路徑的對象並返回其內容
|
||||
Download(ctx context.Context, objectPath string) ([]byte, error)
|
||||
// Move 將對象從一個路徑移動到另一個指定路徑
|
||||
Move(ctx context.Context, objectPath string, destinationPath string) error
|
||||
// Delete 刪除指定路徑的對象
|
||||
Delete(ctx context.Context, objectPath string) error
|
||||
// Exists 檢查指定路徑的對象是否存在
|
||||
Exists(ctx context.Context, objectPath string) (bool, error)
|
||||
// DeleteDirectory 刪除指定路徑的文件夾及其內容
|
||||
DeleteDirectory(ctx context.Context, directoryPath string) error
|
||||
// UploadWithTTL 從 io.Reader 上傳文件到指定路徑,並設置自訂過期時間
|
||||
UploadWithTTL(ctx context.Context, content io.Reader, objectPath string, expires *time.Time) error
|
||||
// UploadFromData 直接從數據上傳文件到指定路徑,可指定 MIME 類型
|
||||
UploadFromData(ctx context.Context, data []byte, objectPath string, contentType string) error
|
||||
// UploadFromPath 將本地文件從指定路徑上傳到對象存儲路徑
|
||||
UploadFromPath(ctx context.Context, localFilePath string, objectPath string) error
|
||||
// GetPublicURL 獲取對象的完整公共 URL
|
||||
GetPublicURL(ctx context.Context, objectPath string) string
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FileStorageUseCase ...
|
||||
type FileStorageUseCase interface {
|
||||
// Download 下載指定路徑的對象並返回其內容
|
||||
Download(ctx context.Context, objectPath string) ([]byte, error)
|
||||
// Move 將對象從一個路徑移動到另一個指定路徑
|
||||
Move(ctx context.Context, objectPath string, destinationPath string) error
|
||||
// Delete 刪除指定路徑的對象
|
||||
Delete(ctx context.Context, objectPath string) error
|
||||
// Exists 檢查指定路徑的對象是否存在
|
||||
Exists(ctx context.Context, objectPath string) (bool, error)
|
||||
// DeleteDirectory 刪除指定路徑的文件夾及其內容
|
||||
DeleteDirectory(ctx context.Context, directoryPath string) error
|
||||
// UploadWithTTL 從 io.Reader 上傳文件到指定路徑,並設置自訂過期時間
|
||||
UploadWithTTL(ctx context.Context, content io.Reader, objectPath string, expires *time.Time) error
|
||||
// UploadFromData 直接從數據上傳文件到指定路徑,可指定 MIME 類型
|
||||
UploadFromData(ctx context.Context, data []byte, objectPath string, contentType string) error
|
||||
// UploadFromPath 將本地文件從指定路徑上傳到對象存儲路徑
|
||||
UploadFromPath(ctx context.Context, localFilePath string, objectPath string) error
|
||||
// GetPublicURL 獲取對象的完整公共 URL
|
||||
GetPublicURL(ctx context.Context, objectPath string) string
|
||||
}
|
||||
|
|
@ -1,248 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"backend/pkg/fileStorage/config"
|
||||
s3Storage "backend/pkg/fileStorage/domain/aws"
|
||||
"backend/pkg/fileStorage/domain/repository"
|
||||
"backend/pkg/fileStorage/utils"
|
||||
errs "backend/pkg/library/errors"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
)
|
||||
|
||||
type AwsS3FileStorageRepositoryParam struct {
|
||||
Conf *config.Config
|
||||
Logger errs.Logger
|
||||
}
|
||||
|
||||
type AwsS3FileStorageRepository struct {
|
||||
AwsS3FileStorageRepositoryParam
|
||||
}
|
||||
|
||||
func MustAwsS3FileStorageRepo(param AwsS3FileStorageRepositoryParam) repository.FileStorageRepository {
|
||||
return &AwsS3FileStorageRepository{
|
||||
param,
|
||||
}
|
||||
}
|
||||
|
||||
// 每次上傳都用一個新的 Session
|
||||
func (repo *AwsS3FileStorageRepository) getAwsSession() (*session.Session, error) {
|
||||
conf := &aws.Config{
|
||||
Region: aws.String(repo.Conf.AmazonS3Settings.Region),
|
||||
Credentials: credentials.NewStaticCredentials(
|
||||
repo.Conf.AmazonS3Settings.AccessKey,
|
||||
repo.Conf.AmazonS3Settings.SecretKey,
|
||||
"",
|
||||
),
|
||||
}
|
||||
|
||||
awsSession, err := session.NewSession(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return awsSession, nil
|
||||
}
|
||||
|
||||
func (repo *AwsS3FileStorageRepository) Move(_ context.Context, objectPath string, destinationPath string) error {
|
||||
awsSession, _ := repo.getAwsSession()
|
||||
svc := s3.New(awsSession)
|
||||
|
||||
copyObjectInput := &s3.CopyObjectInput{
|
||||
Key: aws.String(destinationPath),
|
||||
Bucket: aws.String(repo.Conf.AmazonS3Settings.Bucket),
|
||||
CopySource: aws.String(path.Join(repo.Conf.AmazonS3Settings.Bucket, objectPath)),
|
||||
}
|
||||
deleteObjectInput := &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(repo.Conf.AmazonS3Settings.Bucket),
|
||||
Key: aws.String(objectPath),
|
||||
}
|
||||
if _, err := svc.CopyObject(copyObjectInput); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := svc.DeleteObject(deleteObjectInput); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *AwsS3FileStorageRepository) Delete(_ context.Context, objectPath string) error {
|
||||
awsSession, _ := repo.getAwsSession()
|
||||
svc := s3.New(awsSession)
|
||||
|
||||
deleteObjectInput := &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(repo.Conf.AmazonS3Settings.Bucket),
|
||||
Key: aws.String(objectPath),
|
||||
}
|
||||
deleteObjOutput, err := svc.DeleteObject(deleteObjectInput)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if deleteObjOutput.DeleteMarker != nil && deleteObjOutput.VersionId != nil {
|
||||
repo.Logger.Info(fmt.Sprintf("s3 - delete object, delete marker: %t, VersionId: %s", *deleteObjOutput.DeleteMarker, *deleteObjOutput.VersionId))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *AwsS3FileStorageRepository) Exists(_ context.Context, objectPath string) (bool, error) {
|
||||
awsSession, _ := repo.getAwsSession()
|
||||
downloader := s3.New(awsSession)
|
||||
|
||||
_, err := downloader.HeadObject(&s3.HeadObjectInput{
|
||||
Bucket: aws.String(repo.Conf.AmazonS3Settings.Bucket),
|
||||
Key: aws.String(objectPath),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (repo *AwsS3FileStorageRepository) DeleteDirectory(_ context.Context, directoryPath string) error {
|
||||
if strings.HasPrefix(directoryPath, "/") {
|
||||
directoryPath = directoryPath[1:]
|
||||
}
|
||||
|
||||
awsSession, _ := repo.getAwsSession()
|
||||
svc := s3.New(awsSession)
|
||||
|
||||
listObjectsInput := &s3.ListObjectsV2Input{
|
||||
Bucket: aws.String(repo.Conf.AmazonS3Settings.Bucket),
|
||||
Prefix: aws.String(directoryPath),
|
||||
}
|
||||
listObjectsOutput, err := svc.ListObjectsV2(listObjectsInput)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, object := range listObjectsOutput.Contents {
|
||||
deleteObjectInput := &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(repo.Conf.AmazonS3Settings.Bucket),
|
||||
Key: object.Key,
|
||||
}
|
||||
go func(inp *s3.DeleteObjectInput) {
|
||||
_, _ = svc.DeleteObject(inp)
|
||||
}(deleteObjectInput)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *AwsS3FileStorageRepository) UploadWithTTL(_ context.Context, content io.Reader, objectPath string, expires *time.Time) error {
|
||||
awsSession, _ := repo.getAwsSession()
|
||||
uploader := s3manager.NewUploader(awsSession)
|
||||
|
||||
_, err := uploader.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String(repo.Conf.AmazonS3Settings.Bucket),
|
||||
Key: aws.String(objectPath),
|
||||
Body: content,
|
||||
ACL: aws.String(s3Storage.S3AclSetting),
|
||||
ContentDisposition: aws.String("attachment"),
|
||||
Expires: expires,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *AwsS3FileStorageRepository) UploadFromData(_ context.Context, data []byte, objectPath string, contentType string) error {
|
||||
reader := bytes.NewReader(data)
|
||||
awsSession, _ := repo.getAwsSession()
|
||||
uploader := s3manager.NewUploader(awsSession)
|
||||
|
||||
_, err := uploader.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String(repo.Conf.AmazonS3Settings.Bucket),
|
||||
Key: aws.String(objectPath),
|
||||
Body: reader,
|
||||
ACL: aws.String(s3Storage.S3AclSetting),
|
||||
ContentType: aws.String(contentType),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *AwsS3FileStorageRepository) UploadFromPath(_ context.Context, localFilePath string, objectPath string) error {
|
||||
file, err := os.Open(localFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
fileInfo, _ := file.Stat()
|
||||
buffer := make([]byte, fileInfo.Size())
|
||||
_, _ = file.Read(buffer)
|
||||
|
||||
awsSession, err := repo.getAwsSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uploader := s3manager.NewUploader(awsSession)
|
||||
|
||||
_, err = uploader.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String(repo.Conf.AmazonS3Settings.Bucket),
|
||||
Key: aws.String(objectPath),
|
||||
Body: bytes.NewReader(buffer),
|
||||
ContentType: aws.String(http.DetectContentType(buffer)),
|
||||
ACL: aws.String(s3Storage.S3AclSetting),
|
||||
ContentDisposition: aws.String("attachment"),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *AwsS3FileStorageRepository) GetPublicURL(_ context.Context, objectPath string) string {
|
||||
dirPart := strings.Split(objectPath, string(os.PathSeparator))
|
||||
|
||||
return utils.URLJoin(repo.Conf.AmazonS3Settings.CloudFrontURI, dirPart...)
|
||||
}
|
||||
|
||||
func (repo *AwsS3FileStorageRepository) Download(_ context.Context, objectPath string) ([]byte, error) {
|
||||
awsSession, _ := repo.getAwsSession()
|
||||
downloader := s3manager.NewDownloader(awsSession, func(d *s3manager.Downloader) {
|
||||
d.PartSize = 64 * 1024 * 1024 // 每部分 64MB
|
||||
d.Concurrency = 4
|
||||
d.BufferProvider = s3manager.NewPooledBufferedWriterReadFromProvider(1024 * 1024 * 8)
|
||||
})
|
||||
|
||||
buff := &aws.WriteAtBuffer{}
|
||||
_, err := downloader.Download(buff, &s3.GetObjectInput{
|
||||
Bucket: aws.String(repo.Conf.AmazonS3Settings.Bucket),
|
||||
Key: aws.String(objectPath),
|
||||
})
|
||||
data := buff.Bytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"backend/pkg/fileStorage/config"
|
||||
"backend/pkg/fileStorage/domain/repository"
|
||||
"backend/pkg/fileStorage/domain/usecase"
|
||||
errs "backend/pkg/library/errors"
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AwsS3FileStorageUseCaseParam struct {
|
||||
Conf *config.Config
|
||||
Logger errs.Logger
|
||||
Repo repository.FileStorageRepository
|
||||
}
|
||||
|
||||
type AwsS3FileStorageUseCase struct {
|
||||
AwsS3FileStorageUseCaseParam
|
||||
}
|
||||
|
||||
func MustAwsS3FileStorageUseCase(param AwsS3FileStorageUseCaseParam) usecase.FileStorageUseCase {
|
||||
return &AwsS3FileStorageUseCase{
|
||||
param,
|
||||
}
|
||||
}
|
||||
|
||||
func (use *AwsS3FileStorageUseCase) Download(ctx context.Context, objectPath string) ([]byte, error) {
|
||||
download, err := use.Repo.Download(ctx, objectPath)
|
||||
if err != nil {
|
||||
e := errs.SvcThirdPartyErrorL(use.Logger, []errs.LogField{
|
||||
{Key: "path", Val: objectPath},
|
||||
{Key: "action", Val: "AwsS3FileStorageUseCase.Download"},
|
||||
{Key: "error", Val: err.Error()},
|
||||
}, "s3 - download object failed").Wrap(err)
|
||||
|
||||
return nil, e
|
||||
}
|
||||
|
||||
return download, nil
|
||||
}
|
||||
|
||||
func (use *AwsS3FileStorageUseCase) Move(ctx context.Context, objectPath string, destinationPath string) error {
|
||||
err := use.Repo.Move(ctx, objectPath, destinationPath)
|
||||
if err != nil {
|
||||
e := errs.SvcThirdPartyErrorL(use.Logger, []errs.LogField{
|
||||
{Key: "source", Val: objectPath},
|
||||
{Key: "destination", Val: destinationPath},
|
||||
{Key: "action", Val: "AwsS3FileStorageUseCase.Move"},
|
||||
{Key: "error", Val: err.Error()},
|
||||
}, "s3 - s3 - move object failed").Wrap(err)
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (use *AwsS3FileStorageUseCase) Delete(ctx context.Context, objectPath string) error {
|
||||
err := use.Repo.Delete(ctx, objectPath)
|
||||
if err != nil {
|
||||
e := errs.SvcThirdPartyErrorL(use.Logger, []errs.LogField{
|
||||
{Key: "path", Val: objectPath},
|
||||
{Key: "action", Val: "AwsS3FileStorageUseCase.Delete"},
|
||||
{Key: "error", Val: err.Error()},
|
||||
}, "s3 - delete object failed").Wrap(err)
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (use *AwsS3FileStorageUseCase) Exists(ctx context.Context, objectPath string) (bool, error) {
|
||||
ex, err := use.Repo.Exists(ctx, objectPath)
|
||||
if err != nil {
|
||||
e := errs.SvcThirdPartyErrorL(use.Logger, []errs.LogField{
|
||||
{Key: "path", Val: objectPath},
|
||||
{Key: "error", Val: err.Error()},
|
||||
}, "s3 - check exists object failed").Wrap(err)
|
||||
|
||||
return false, e
|
||||
}
|
||||
|
||||
return ex, nil
|
||||
}
|
||||
|
||||
func (use *AwsS3FileStorageUseCase) DeleteDirectory(ctx context.Context, directoryPath string) error {
|
||||
err := use.Repo.DeleteDirectory(ctx, directoryPath)
|
||||
|
||||
return errs.SvcThirdPartyErrorL(
|
||||
use.Logger,
|
||||
[]errs.LogField{
|
||||
{Key: "directory", Val: directoryPath},
|
||||
{Key: "action", Val: "AwsS3FileStorageUseCase.DeleteDirectory"},
|
||||
{Key: "error", Val: err.Error()},
|
||||
},
|
||||
"s3 - failed to list objects in folder",
|
||||
).Wrap(err)
|
||||
}
|
||||
|
||||
func (use *AwsS3FileStorageUseCase) UploadWithTTL(ctx context.Context, content io.Reader, objectPath string, expires *time.Time) error {
|
||||
err := use.Repo.UploadWithTTL(ctx, content, objectPath, expires)
|
||||
if err != nil {
|
||||
e := errs.SvcThirdPartyErrorL(
|
||||
use.Logger,
|
||||
[]errs.LogField{
|
||||
{Key: "action", Val: "AwsS3FileStorageUseCase.DeleteDirectory"},
|
||||
{Key: "error", Val: err.Error()},
|
||||
},
|
||||
"s3 - failed to list objects in folder")
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (use *AwsS3FileStorageUseCase) UploadFromData(ctx context.Context, data []byte, objectPath string, contentType string) error {
|
||||
err := use.Repo.UploadFromData(ctx, data, objectPath, contentType)
|
||||
if err != nil {
|
||||
e := errs.SvcThirdPartyErrorL(
|
||||
use.Logger,
|
||||
[]errs.LogField{
|
||||
{Key: "path", Val: objectPath},
|
||||
{Key: "action", Val: "AwsS3FileStorageUseCase.UploadFromData"},
|
||||
{Key: "error", Val: err.Error()},
|
||||
},
|
||||
"s3 - upload object failed")
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (use *AwsS3FileStorageUseCase) UploadFromPath(ctx context.Context, localFilePath string, objectPath string) error {
|
||||
err := use.Repo.UploadFromPath(ctx, localFilePath, objectPath)
|
||||
if err != nil {
|
||||
e := errs.SvcThirdPartyErrorL(
|
||||
use.Logger,
|
||||
[]errs.LogField{
|
||||
{Key: "localPath", Val: localFilePath},
|
||||
{Key: "action", Val: "AwsS3FileStorageUseCase.UploadFromPath"},
|
||||
{Key: "error", Val: err.Error()},
|
||||
},
|
||||
"s3 - upload object failed")
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (use *AwsS3FileStorageUseCase) GetPublicURL(ctx context.Context, objectPath string) string {
|
||||
return use.Repo.GetPublicURL(ctx, objectPath)
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
)
|
||||
|
||||
func URLJoin(baseURL string, paths ...string) string {
|
||||
u, _ := url.Parse(baseURL)
|
||||
pathElements := append([]string{u.Path}, paths...)
|
||||
u.Path = path.Join(pathElements...)
|
||||
|
||||
return u.String()
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// 測試 URLJoin 函數
|
||||
func TestURLJoin(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
baseURL string
|
||||
paths []string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Base URL without trailing slash and single path",
|
||||
baseURL: "https://example.com",
|
||||
paths: []string{"path1"},
|
||||
expected: "https://example.com/path1",
|
||||
},
|
||||
{
|
||||
name: "Base URL with trailing slash and single path",
|
||||
baseURL: "https://example.com/",
|
||||
paths: []string{"path1"},
|
||||
expected: "https://example.com/path1",
|
||||
},
|
||||
{
|
||||
name: "Base URL without trailing slash and multiple paths",
|
||||
baseURL: "https://example.com",
|
||||
paths: []string{"path1", "path2", "path3"},
|
||||
expected: "https://example.com/path1/path2/path3",
|
||||
},
|
||||
{
|
||||
name: "Base URL with trailing slash and multiple paths",
|
||||
baseURL: "https://example.com/",
|
||||
paths: []string{"path1", "path2", "path3"},
|
||||
expected: "https://example.com/path1/path2/path3",
|
||||
},
|
||||
{
|
||||
name: "Empty path elements",
|
||||
baseURL: "https://example.com",
|
||||
paths: []string{},
|
||||
expected: "https://example.com",
|
||||
},
|
||||
{
|
||||
name: "Path elements with leading slashes",
|
||||
baseURL: "https://example.com",
|
||||
paths: []string{"/path1/", "/path2", "path3"},
|
||||
expected: "https://example.com/path1/path2/path3",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := URLJoin(tt.baseURL, tt.paths...)
|
||||
assert.Equal(t, tt.expected, result, "Expected URL to match")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
run:
|
||||
timeout: 3m
|
||||
issues-exit-code: 2
|
||||
tests: false # 不檢查測試檔案
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- govet # 官方靜態分析,抓潛在 bug
|
||||
- staticcheck # 最強 bug/反模式偵測
|
||||
- revive # golint 進化版,風格與註解規範
|
||||
- gofmt # 風格格式化檢查
|
||||
- goimports # import 排序
|
||||
- errcheck # error 忽略警告
|
||||
- ineffassign # 無效賦值
|
||||
- unused # 未使用變數
|
||||
- bodyclose # HTTP body close
|
||||
- gosimple # 靜態分析簡化警告(staticcheck 也包含,可選)
|
||||
- typecheck # 型別檢查
|
||||
- misspell # 拼字檢查
|
||||
- gocritic # bug-prone code
|
||||
- gosec # 資安檢查
|
||||
- prealloc # slice/array 預分配
|
||||
- unparam # 未使用參數
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- funlen
|
||||
- goconst
|
||||
- cyclop
|
||||
- gocognit
|
||||
- lll
|
||||
- wrapcheck
|
||||
- contextcheck
|
||||
|
||||
linters-settings:
|
||||
revive:
|
||||
severity: warning
|
||||
rules:
|
||||
- name: blank-imports
|
||||
severity: error
|
||||
gofmt:
|
||||
simplify: true
|
||||
lll:
|
||||
line-length: 140
|
||||
|
||||
# 可自訂目錄忽略(視專案需求加上)
|
||||
# skip-dirs:
|
||||
# - vendor
|
||||
# - third_party
|
||||
|
||||
# 可以設定本機與 CI 上都一致
|
||||
# env:
|
||||
# GOLANGCI_LINT_CACHE: ".golangci-lint-cache"
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
GOFMT ?= gofmt "-s"
|
||||
GOFILES := $(shell find . -name "*.go")
|
||||
|
||||
|
||||
.PHONY: test
|
||||
test: # 進行測試
|
||||
go test -v --cover ./...
|
||||
|
||||
.PHONY: fmt
|
||||
fmt: # 格式優化
|
||||
$(GOFMT) -w $(GOFILES)
|
||||
goimports -w ./
|
||||
golangci-lint run
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
# 錯誤碼 × HTTP 對照表
|
||||
|
||||
這份文件專門整理 **infra-core/errors** 的「錯誤碼 → HTTP Status」對照,並提供**實務範例**。
|
||||
錯誤系統採用 8 碼格式 `SSCCCDDD`:
|
||||
|
||||
- `SS` = Scope(服務/模組,兩位數)
|
||||
- `CCC` = Category(類別,三位數,影響 HTTP 狀態)
|
||||
- `DDD` = Detail(細節,三位數,自定義業務碼)
|
||||
|
||||
> 例如:`10101000` → Scope=10、Category=101(InputInvalidFormat)、Detail=000。
|
||||
|
||||
## 目錄
|
||||
- [1) 快速查表](#1-快速查表依類別整理)
|
||||
- [2) 使用範例](#2-使用範例)
|
||||
- [3) 小撇步與慣例](#3-小撇步與慣例)
|
||||
- [4) 安裝與測試](#4-安裝與測試)
|
||||
- [5) 變更日誌](#5-變更日誌)
|
||||
|
||||
---
|
||||
|
||||
## 1) 快速查表(依類別整理)
|
||||
|
||||
### A. Input(Category 1xx)
|
||||
|
||||
| Category 常數 | 說明 | HTTP | 原因/說明 |
|
||||
|---|---------------|:----:|---|
|
||||
| `InputInvalidFormat` (101) | 無效格式 | **400 Bad Request** | 格式不符、缺欄位、型別錯。 |
|
||||
| `InputNotValidImplementation` (102) | 非有效實作 | **422 Unprocessable Entity** | 語意正確但無法處理。 |
|
||||
| `InputInvalidRange` (103) | 無效範圍 | **422 Unprocessable Entity** | 值超域、邊界條件不合。 |
|
||||
|
||||
### B. DB(Category 2xx)
|
||||
|
||||
| Category 常數 | 說明 | HTTP | 原因/說明 |
|
||||
|---|-------------|:----:|---|
|
||||
| `DBError` (201) | 資料庫一般錯誤 | **500 Internal Server Error** | 後端故障/不可預期。 |
|
||||
| `DBDataConvert` (202) | 資料轉換錯誤 | **422 Unprocessable Entity** | 可修正的資料問題(格式/型別轉換失敗)。 |
|
||||
| `DBDuplicate` (203) | 資料重複 | **409 Conflict** | 唯一鍵衝突、重複建立。 |
|
||||
|
||||
### C. Resource(Category 3xx)
|
||||
|
||||
| Category 常數 | 說明 | HTTP | 原因/說明 |
|
||||
|---|-------------------|:----:|---|
|
||||
| `ResNotFound` (301) | 資源未找到 | **404 Not Found** | 目標不存在/無此 ID。 |
|
||||
| `ResInvalidFormat` (302) | 無效資源格式 | **422 Unprocessable Entity** | 表示層/Schema 不符。 |
|
||||
| `ResAlreadyExist` (303) | 資源已存在 | **409 Conflict** | 重複建立/命名衝突。 |
|
||||
| `ResInsufficient` (304) | 資源不足 | **400 Bad Request** | 數量/容量不足(用戶可改參數再試)。 |
|
||||
| `ResInsufficientPerm` (305) | 權限不足 | **403 Forbidden** | 已驗證但無權限。 |
|
||||
| `ResInvalidMeasureID` (306) | 無效測量ID | **400 Bad Request** | ID 本身不合法。 |
|
||||
| `ResExpired` (307) | 資源過期 | **410 Gone** | 已不可用(可於上層補 Location)。 |
|
||||
| `ResMigrated` (308) | 資源已遷移 | **410 Gone** | 同上,如需導引請於上層處理。 |
|
||||
| `ResInvalidState` (309) | 無效狀態 | **409 Conflict** | 當前狀態不允許此操作。 |
|
||||
| `ResInsufficientQuota` (310) | 配額不足 | **429 Too Many Requests** | 達配額/速率限制。 |
|
||||
| `ResMultiOwner` (311) | 多所有者 | **409 Conflict** | 所有權歧異造成衝突。 |
|
||||
|
||||
### D. Auth(Category 5xx)
|
||||
|
||||
| Category 常數 | 說明 | HTTP | 原因/說明 |
|
||||
|---|-------------------------|:----:|---|
|
||||
| `AuthUnauthorized` (501) | 未授權/未驗證 | **401 Unauthorized** | 缺 Token、無效 Token。 |
|
||||
| `AuthExpired` (502) | 授權過期 | **401 Unauthorized** | Token 過期或時效失效。 |
|
||||
| `AuthInvalidPosixTime` (503) | 無效 POSIX 時間 | **401 Unauthorized** | 時戳異常導致驗簽失敗。 |
|
||||
| `AuthSigPayloadMismatch` (504) | 簽名與載荷不符 | **401 Unauthorized** | 驗簽失敗。 |
|
||||
| `AuthForbidden` (505) | 禁止存取 | **403 Forbidden** | 已驗證但沒有操作權限。 |
|
||||
|
||||
### E. System(Category 6xx)
|
||||
|
||||
| Category 常數 | 說明 | HTTP | 原因/說明 |
|
||||
|---|---------------|:----:|---|
|
||||
| `SysInternal` (601) | 系統內部錯誤 | **500 Internal Server Error** | 未預期的系統錯。 |
|
||||
| `SysMaintain` (602) | 系統維護中 | **503 Service Unavailable** | 維護/停機。 |
|
||||
| `SysTimeout` (603) | 系統超時 | **504 Gateway Timeout** | 下游/處理逾時。 |
|
||||
| `SysTooManyRequest` (604) | 請求過多 | **429 Too Many Requests** | 節流/限流。 |
|
||||
|
||||
### F. PubSub(Category 7xx)
|
||||
|
||||
| Category 常數 | 說明 | HTTP | 原因/說明 |
|
||||
|---|---------|:----:|---|
|
||||
| `PSuPublish` (701) | 發佈失敗 | **502 Bad Gateway** | 中介或外部匯流排錯誤。 |
|
||||
| `PSuConsume` (702) | 消費失敗 | **502 Bad Gateway** | 同上。 |
|
||||
| `PSuTooLarge` (703) | 訊息過大 | **413 Payload Too Large** | 封包大小超限。 |
|
||||
|
||||
### G. Service(Category 8xx)
|
||||
|
||||
| Category 常數 | 說明 | HTTP | 原因/說明 |
|
||||
|---|---------------|:----:|---|
|
||||
| `SvcInternal` (801) | 服務內部錯誤 | **500 Internal Server Error** | 非基礎設施層的內錯。 |
|
||||
| `SvcThirdParty` (802) | 第三方失敗 | **502 Bad Gateway** | 呼叫外部服務失敗。 |
|
||||
| `SvcHTTP400` (803) | 明確指派 400 | **400 Bad Request** | 自行指定。 |
|
||||
| `SvcMaintenance` (804) | 服務維護中 | **503 Service Unavailable** | 模組級維運中。 |
|
||||
|
||||
---
|
||||
|
||||
## 2) 使用範例
|
||||
|
||||
### 2.1 在 Handler 中回傳錯誤
|
||||
|
||||
```go
|
||||
import (
|
||||
"net/http"
|
||||
errs "gitlab.supermicro.com/infra/infra-core/errors"
|
||||
"gitlab.supermicro.com/infra/infra-core/errors/code"
|
||||
)
|
||||
|
||||
func init() {
|
||||
errs.Scope = code.Gateway // 設定當前服務的 Scope
|
||||
}
|
||||
|
||||
func GetUser(w http.ResponseWriter, r *http.Request) error {
|
||||
id := r.URL.Query().Get("id")
|
||||
if id == "" {
|
||||
return errs.InputInvalidFormatError("缺少參數: id") // 現在是 8 位碼
|
||||
}
|
||||
|
||||
u, err := repo.Find(r.Context(), id)
|
||||
switch {
|
||||
case errors.Is(err, repo.ErrNotFound):
|
||||
return errs.ResNotFoundError("user", id)
|
||||
case err != nil:
|
||||
return errs.DBErrorError("查詢使用者失敗").Wrap(err) // Wrap 內部錯誤
|
||||
}
|
||||
|
||||
// … 寫入回應
|
||||
return nil
|
||||
}
|
||||
|
||||
// 統一寫出 HTTP 錯誤
|
||||
func writeHTTP(w http.ResponseWriter, e *errs.Error) {
|
||||
http.Error(w, e.Error(), e.HTTPStatus())
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 取出 Wrap 的內部錯誤
|
||||
|
||||
```go
|
||||
if internal := e.Unwrap(); internal != nil {
|
||||
log.Error("Internal error: ", internal)
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 搭配日誌裝飾器(`WithLog` / `WithLogWrap`)
|
||||
|
||||
```go
|
||||
log := logger.WithFields(errs.LogField{Key: "req_id", Val: rid})
|
||||
|
||||
if badInput {
|
||||
return errs.WithLog(log, nil, errs.InputInvalidFormatError, "email 無效")
|
||||
}
|
||||
|
||||
if err := repo.Save(ctx, u); err != nil {
|
||||
return errs.WithLogWrap(
|
||||
log,
|
||||
[]errs.LogField{{Key: "entity", Val: "user"}, {Key: "op", Val: "save"}},
|
||||
errs.DBErrorError,
|
||||
err,
|
||||
"儲存失敗",
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 只知道 Category+Detail 的動態場景(`EL` / `ELWrap`)
|
||||
|
||||
```go
|
||||
// 依流程動態產生
|
||||
return errs.EL(log, nil, code.SysTimeout, 123, "下游逾時") // 自定義 detail=123
|
||||
|
||||
// 或需保留 cause:
|
||||
return errs.ELWrap(log, nil, code.SvcThirdParty, 456, err, "金流商失敗")
|
||||
```
|
||||
|
||||
### 2.5 gRPC 互通
|
||||
|
||||
```go
|
||||
// 由 *errs.Error 轉為 gRPC status
|
||||
st := e.GRPCStatus() // *status.Status
|
||||
|
||||
// 客戶端收到 gRPC error → 轉回 *errs.Error
|
||||
e := errs.FromGRPCError(grpcErr)
|
||||
fmt.Println(e.DisplayCode(), e.Error()) // e.g., "10101000" "error msg"
|
||||
```
|
||||
|
||||
### 2.6 從 8 碼反解(`FromCode`)
|
||||
|
||||
```go
|
||||
e := errs.FromCode(10101000) // 10101000
|
||||
fmt.Println(e.Scope(), e.Category(), e.Detail()) // 10, 101, 000
|
||||
```
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
package code
|
||||
|
||||
type Scope uint32 // SS (00..99)
|
||||
type Category uint32 // CCC (000..999)
|
||||
type Detail uint32 // DDD (000..999) // Updated to 3 digits
|
||||
|
||||
const (
|
||||
Unset Scope = 0
|
||||
CategoryMultiplier uint32 = 1000
|
||||
ScopeMultiplier uint32 = 1000000
|
||||
NonCode uint32 = 0
|
||||
OK uint32 = 0 // Already exists, but merged for completeness; avoid duplication if needed
|
||||
SUCCESSCode = "00000000"
|
||||
SUCCESSMessage = "success"
|
||||
)
|
||||
|
||||
// Boundary constants for validation
|
||||
const (
|
||||
MaxCategory Category = 999 // Maximum allowed category value
|
||||
MaxDetail Detail = 999 // Maximum allowed detail value (updated)
|
||||
|
||||
DefaultCategory Category = 0
|
||||
DefaultDetail Detail = 0
|
||||
|
||||
// Reserved values - DO NOT USE in normal operations
|
||||
// These are used internally for overflow protection
|
||||
|
||||
ReservedMaxCategory Category = 999 // Used when category > 999
|
||||
ReservedMaxDetail Detail = 999 // Used when detail > 999 (updated)
|
||||
)
|
||||
|
||||
// New 3-digit categories (merged from original category + detail)
|
||||
// Input errors (100-109)
|
||||
const (
|
||||
InputInvalidFormat Category = 101
|
||||
InputNotValidImplementation Category = 102
|
||||
InputInvalidRange Category = 103
|
||||
)
|
||||
|
||||
// DB errors (200-209)
|
||||
const (
|
||||
DBError Category = 201
|
||||
DBDataConvert Category = 202
|
||||
DBDuplicate Category = 203
|
||||
)
|
||||
|
||||
// Resource errors (300-399)
|
||||
const (
|
||||
ResNotFound Category = 301
|
||||
ResInvalidFormat Category = 302
|
||||
ResAlreadyExist Category = 303
|
||||
ResInsufficient Category = 304
|
||||
ResInsufficientPerm Category = 305
|
||||
ResInvalidMeasureID Category = 306
|
||||
ResExpired Category = 307
|
||||
ResMigrated Category = 308
|
||||
ResInvalidState Category = 309
|
||||
ResInsufficientQuota Category = 310
|
||||
ResMultiOwner Category = 311
|
||||
)
|
||||
|
||||
// GRPC category
|
||||
|
||||
const (
|
||||
CatGRPC Category = 400
|
||||
)
|
||||
|
||||
// Auth errors (500-509)
|
||||
const (
|
||||
AuthUnauthorized Category = 501
|
||||
AuthExpired Category = 502
|
||||
AuthInvalidPosixTime Category = 503
|
||||
AuthSigPayloadMismatch Category = 504
|
||||
AuthForbidden Category = 505
|
||||
)
|
||||
|
||||
// System errors (600-609)
|
||||
const (
|
||||
SysInternal Category = 601
|
||||
SysMaintain Category = 602
|
||||
SysTimeout Category = 603
|
||||
SysTooManyRequest Category = 604
|
||||
)
|
||||
|
||||
// PubSub errors (700-709)
|
||||
const (
|
||||
PSuPublish Category = 701
|
||||
PSuConsume Category = 702
|
||||
PSuTooLarge Category = 703
|
||||
)
|
||||
|
||||
// Service errors (800-809)
|
||||
const (
|
||||
SvcInternal Category = 801
|
||||
SvcThirdParty Category = 802
|
||||
SvcHTTP400 Category = 803
|
||||
SvcMaintenance Category = 804
|
||||
)
|
||||
|
||||
const (
|
||||
Gateway Scope = 10
|
||||
)
|
||||
|
|
@ -1,234 +0,0 @@
|
|||
package errs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"backend/pkg/library/errors/code"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// Scope is a global variable that should be set by the service or module.
|
||||
var Scope = code.Unset
|
||||
|
||||
// Error represents a structured error with an 8-digit code.
|
||||
// The code is composed of a 2-digit scope, a 3-digit category, and a 3-digit detail.
|
||||
// Format: SSCCCDDD
|
||||
type Error struct {
|
||||
scope uint32 // 2-digit service scope
|
||||
category uint32 // 3-digit category
|
||||
detail uint32 // 3-digit detail
|
||||
msg string // Display message for the client
|
||||
internalErr error // The actual underlying error
|
||||
}
|
||||
|
||||
// New creates a new Error.
|
||||
// It ensures that category is within 0-999 and detail is within 0-999.
|
||||
func New(scope, category, detail uint32, displayMsg string) *Error {
|
||||
if category > uint32(code.MaxCategory) {
|
||||
category = uint32(code.ReservedMaxCategory)
|
||||
}
|
||||
if detail > uint32(code.MaxDetail) {
|
||||
detail = uint32(code.ReservedMaxDetail)
|
||||
}
|
||||
|
||||
return &Error{
|
||||
scope: scope,
|
||||
category: category,
|
||||
detail: detail,
|
||||
msg: displayMsg,
|
||||
}
|
||||
}
|
||||
|
||||
// Error returns the display message. This is intended for the client.
|
||||
// For internal logging and debugging, use Unwrap() to get the underlying error.
|
||||
func (e *Error) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return e.msg
|
||||
}
|
||||
|
||||
// Scope returns the 2-digit scope of the error.
|
||||
func (e *Error) Scope() uint32 {
|
||||
if e == nil {
|
||||
return uint32(code.Unset)
|
||||
}
|
||||
|
||||
return e.scope
|
||||
}
|
||||
|
||||
// Category returns the 3-digit category of the error.
|
||||
func (e *Error) Category() uint32 {
|
||||
if e == nil {
|
||||
return uint32(code.DefaultCategory)
|
||||
}
|
||||
|
||||
return e.category
|
||||
}
|
||||
|
||||
// Detail returns the 2-digit detail code of the error.
|
||||
func (e *Error) Detail() uint32 {
|
||||
if e == nil {
|
||||
return uint32(code.DefaultDetail)
|
||||
}
|
||||
|
||||
return e.detail
|
||||
}
|
||||
|
||||
// SubCode returns the 6-digit code (category + detail).
|
||||
func (e *Error) SubCode() uint32 {
|
||||
if e == nil {
|
||||
return code.OK
|
||||
}
|
||||
c := e.category*code.CategoryMultiplier + e.detail
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Code returns the full 8-digit error code (scope + category + detail).
|
||||
func (e *Error) Code() uint32 {
|
||||
if e == nil {
|
||||
return code.NonCode
|
||||
}
|
||||
|
||||
return e.Scope()*code.ScopeMultiplier + e.SubCode()
|
||||
}
|
||||
|
||||
// DisplayCode returns the 8-digit error code as a zero-padded string.
|
||||
func (e *Error) DisplayCode() string {
|
||||
if e == nil {
|
||||
return "00000000"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%08d", e.Code())
|
||||
}
|
||||
|
||||
// Is checks if the target error is of type *Error and has the same sub-code.
|
||||
// It is called by errors.Is(). Do not use it directly.
|
||||
func (e *Error) Is(target error) bool {
|
||||
var err *Error
|
||||
if !errors.As(target, &err) {
|
||||
return false
|
||||
}
|
||||
|
||||
return e.SubCode() == err.SubCode()
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying wrapped error.
|
||||
func (e *Error) Unwrap() error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return e.internalErr
|
||||
}
|
||||
|
||||
// Wrap sets the internal error for the current error.
|
||||
func (e *Error) Wrap(internalErr error) *Error {
|
||||
if e != nil {
|
||||
e.internalErr = internalErr
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// GRPCStatus converts the error to a gRPC status.
|
||||
func (e *Error) GRPCStatus() *status.Status {
|
||||
if e == nil {
|
||||
return status.New(codes.OK, "")
|
||||
}
|
||||
|
||||
return status.New(codes.Code(e.Code()), e.Error())
|
||||
}
|
||||
|
||||
// HTTPStatus returns the corresponding HTTP status code for the error.
|
||||
func (e *Error) HTTPStatus() int {
|
||||
if e == nil || e.SubCode() == code.OK {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
switch e.Category() {
|
||||
// Input
|
||||
case uint32(code.InputInvalidFormat):
|
||||
return http.StatusBadRequest // 400:輸入格式錯
|
||||
case uint32(code.InputNotValidImplementation),
|
||||
uint32(code.InputInvalidRange):
|
||||
return http.StatusUnprocessableEntity // 422:語意正確但無法處理(範圍/實作)
|
||||
|
||||
// DB
|
||||
case uint32(code.DBError):
|
||||
return http.StatusInternalServerError // 500:後端暫時性故障(若你偏好 503 可自行調整)
|
||||
case uint32(code.DBDataConvert):
|
||||
return http.StatusUnprocessableEntity // 422:可修正的資料轉換失敗
|
||||
case uint32(code.DBDuplicate):
|
||||
return http.StatusConflict // 409:唯一鍵/重複
|
||||
|
||||
// Resource
|
||||
case uint32(code.ResNotFound):
|
||||
return http.StatusNotFound // 404:資源不存在
|
||||
case uint32(code.ResInvalidFormat):
|
||||
return http.StatusUnprocessableEntity // 422:資源表示/格式不符
|
||||
case uint32(code.ResAlreadyExist):
|
||||
return http.StatusConflict // 409:已存在
|
||||
case uint32(code.ResInsufficient):
|
||||
return http.StatusBadRequest // 400:數量/容量/條件不足(可由客戶端修正)
|
||||
case uint32(code.ResInsufficientPerm):
|
||||
return http.StatusForbidden // 403:資源層面的權限不足
|
||||
case uint32(code.ResInvalidMeasureID):
|
||||
return http.StatusBadRequest // 400:ID 無效
|
||||
case uint32(code.ResExpired):
|
||||
return http.StatusGone // 410:資源已過期/不可用
|
||||
case uint32(code.ResMigrated):
|
||||
return http.StatusGone // 410:已遷移(若需導引可由上層加 Location)
|
||||
case uint32(code.ResInvalidState):
|
||||
return http.StatusConflict // 409:目前狀態不允許此操作
|
||||
case uint32(code.ResInsufficientQuota):
|
||||
return http.StatusTooManyRequests // 429:配額不足/達上限
|
||||
case uint32(code.ResMultiOwner):
|
||||
return http.StatusConflict // 409:多所有者衝突
|
||||
|
||||
// Auth
|
||||
case uint32(code.AuthUnauthorized),
|
||||
uint32(code.AuthExpired),
|
||||
uint32(code.AuthInvalidPosixTime),
|
||||
uint32(code.AuthSigPayloadMismatch):
|
||||
return http.StatusUnauthorized // 401:未驗證/無效憑證
|
||||
case uint32(code.AuthForbidden):
|
||||
return http.StatusForbidden // 403:有身分但沒權限
|
||||
|
||||
// System
|
||||
case uint32(code.SysTooManyRequest):
|
||||
return http.StatusTooManyRequests // 429:節流
|
||||
case uint32(code.SysInternal):
|
||||
return http.StatusInternalServerError // 500:系統內部錯
|
||||
case uint32(code.SysMaintain):
|
||||
return http.StatusServiceUnavailable // 503:維護中
|
||||
case uint32(code.SysTimeout):
|
||||
return http.StatusGatewayTimeout // 504:處理/下游逾時
|
||||
|
||||
// PubSub
|
||||
case uint32(code.PSuPublish),
|
||||
uint32(code.PSuConsume):
|
||||
return http.StatusBadGateway // 502:訊息中介/外部匯流排失敗
|
||||
case uint32(code.PSuTooLarge):
|
||||
return http.StatusRequestEntityTooLarge // 413:訊息太大
|
||||
|
||||
// Service
|
||||
case uint32(code.SvcMaintenance):
|
||||
return http.StatusServiceUnavailable // 503:服務維護
|
||||
case uint32(code.SvcInternal):
|
||||
return http.StatusInternalServerError // 500:服務內部錯
|
||||
case uint32(code.SvcThirdParty):
|
||||
return http.StatusBadGateway // 502:第三方依賴失敗
|
||||
case uint32(code.SvcHTTP400):
|
||||
return http.StatusBadRequest // 400:明確指派 400
|
||||
}
|
||||
|
||||
// fallback
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
|
|
@ -1,216 +0,0 @@
|
|||
package errs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"backend/pkg/library/errors/code"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
scope uint32
|
||||
category uint32
|
||||
detail uint32
|
||||
displayMsg string
|
||||
wantScope uint32
|
||||
wantCategory uint32
|
||||
wantDetail uint32
|
||||
wantMsg string
|
||||
}{
|
||||
{"basic", 10, 201, 123, "test", 10, 201, 123, "test"},
|
||||
{"clamp category", 10, 1000, 0, "clamp cat", 10, 999, 0, "clamp cat"},
|
||||
{"clamp detail", 10, 101, 1000, "clamp det", 10, 101, 999, "clamp det"},
|
||||
{"zero values", 0, 0, 0, "", 0, 0, 0, ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
e := New(tt.scope, tt.category, tt.detail, tt.displayMsg)
|
||||
if e.Scope() != tt.wantScope || e.Category() != tt.wantCategory || e.Detail() != tt.wantDetail || e.msg != tt.wantMsg {
|
||||
t.Errorf("New() = %+v, want scope=%d cat=%d det=%d msg=%q", e, tt.wantScope, tt.wantCategory, tt.wantDetail, tt.wantMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorMethods(t *testing.T) {
|
||||
e := New(10, 201, 123, "test error")
|
||||
tests := []struct {
|
||||
name string
|
||||
err *Error
|
||||
wantErr string
|
||||
wantScope uint32
|
||||
wantCat uint32
|
||||
wantDet uint32
|
||||
}{
|
||||
{"non-nil", e, "test error", 10, 201, 123},
|
||||
{"nil", nil, "", uint32(code.Unset), uint32(code.DefaultCategory), uint32(code.DefaultDetail)}, // Adjust if Default* not defined; use 0
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.err.Error(); got != tt.wantErr {
|
||||
t.Errorf("Error() = %q, want %q", got, tt.wantErr)
|
||||
}
|
||||
if got := tt.err.Scope(); got != tt.wantScope {
|
||||
t.Errorf("Scope() = %d, want %d", got, tt.wantScope)
|
||||
}
|
||||
if got := tt.err.Category(); got != tt.wantCat {
|
||||
t.Errorf("Category() = %d, want %d", got, tt.wantCat)
|
||||
}
|
||||
if got := tt.err.Detail(); got != tt.wantDet {
|
||||
t.Errorf("Detail() = %d, want %d", got, tt.wantDet)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err *Error
|
||||
wantSubCode uint32
|
||||
wantCode uint32
|
||||
wantDisplay string
|
||||
}{
|
||||
{"basic", New(10, 201, 123, ""), 201123, 10201123, "10201123"},
|
||||
{"nil", nil, code.OK, code.NonCode, "00000000"},
|
||||
{"max clamp", New(99, 999, 999, ""), 999999, 99999999, "99999999"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.err.SubCode(); got != tt.wantSubCode {
|
||||
t.Errorf("SubCode() = %d, want %d", got, tt.wantSubCode)
|
||||
}
|
||||
if got := tt.err.Code(); got != tt.wantCode {
|
||||
t.Errorf("Code() = %d, want %d", got, tt.wantCode)
|
||||
}
|
||||
if got := tt.err.DisplayCode(); got != tt.wantDisplay {
|
||||
t.Errorf("DisplayCode() = %q, want %q", got, tt.wantDisplay)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIs(t *testing.T) {
|
||||
e1 := New(10, 201, 123, "")
|
||||
e2 := New(10, 201, 123, "") // same subcode
|
||||
e3 := New(10, 202, 123, "") // different category
|
||||
stdErr := errors.New("std")
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
target error
|
||||
want bool
|
||||
}{
|
||||
{"match", e1, e2, true},
|
||||
{"mismatch", e1, e3, false},
|
||||
{"not Error type", e1, stdErr, false},
|
||||
{"nil err", nil, e2, false},
|
||||
{"nil target", e1, nil, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := errors.Is(tt.err, tt.target); got != tt.want {
|
||||
t.Errorf("Is() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapUnwrap(t *testing.T) {
|
||||
internal := errors.New("internal")
|
||||
tests := []struct {
|
||||
name string
|
||||
err *Error
|
||||
wrapErr error
|
||||
wantUnwrap error
|
||||
}{
|
||||
{"wrap non-nil", New(10, 201, 0, ""), internal, internal},
|
||||
{"wrap nil", nil, internal, nil}, // Wrap on nil does nothing
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.err.Wrap(tt.wrapErr)
|
||||
if unwrapped := got.Unwrap(); unwrapped != tt.wantUnwrap {
|
||||
t.Errorf("Unwrap() = %v, want %v", unwrapped, tt.wantUnwrap)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGRPCStatus(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err *Error
|
||||
wantCode codes.Code
|
||||
wantMsg string
|
||||
}{
|
||||
{"non-nil", New(10, 201, 123, "grpc err"), codes.Code(10201123), "grpc err"},
|
||||
{"nil", nil, codes.OK, ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := tt.err.GRPCStatus()
|
||||
if s.Code() != tt.wantCode || s.Message() != tt.wantMsg {
|
||||
t.Errorf("GRPCStatus() = code=%v msg=%q, want code=%v msg=%q", s.Code(), s.Message(), tt.wantCode, tt.wantMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPStatus(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err *Error
|
||||
want int
|
||||
}{
|
||||
{"nil", nil, http.StatusOK},
|
||||
{"OK subcode", New(10, 0, 0, ""), http.StatusOK},
|
||||
{"InputInvalidFormat", New(10, uint32(code.InputInvalidFormat), 0, ""), http.StatusBadRequest},
|
||||
{"InputNotValidImplementation", New(10, uint32(code.InputNotValidImplementation), 0, ""), http.StatusUnprocessableEntity},
|
||||
{"DBError", New(10, uint32(code.DBError), 0, ""), http.StatusInternalServerError},
|
||||
{"ResNotFound", New(10, uint32(code.ResNotFound), 0, ""), http.StatusNotFound},
|
||||
// Add all other categories to cover switch branches
|
||||
{"InputInvalidRange", New(10, uint32(code.InputInvalidRange), 0, ""), http.StatusUnprocessableEntity},
|
||||
{"DBDataConvert", New(10, uint32(code.DBDataConvert), 0, ""), http.StatusUnprocessableEntity},
|
||||
{"DBDuplicate", New(10, uint32(code.DBDuplicate), 0, ""), http.StatusConflict},
|
||||
{"ResInvalidFormat", New(10, uint32(code.ResInvalidFormat), 0, ""), http.StatusUnprocessableEntity},
|
||||
{"ResAlreadyExist", New(10, uint32(code.ResAlreadyExist), 0, ""), http.StatusConflict},
|
||||
{"ResInsufficient", New(10, uint32(code.ResInsufficient), 0, ""), http.StatusBadRequest},
|
||||
{"ResInsufficientPerm", New(10, uint32(code.ResInsufficientPerm), 0, ""), http.StatusForbidden},
|
||||
{"ResInvalidMeasureID", New(10, uint32(code.ResInvalidMeasureID), 0, ""), http.StatusBadRequest},
|
||||
{"ResExpired", New(10, uint32(code.ResExpired), 0, ""), http.StatusGone},
|
||||
{"ResMigrated", New(10, uint32(code.ResMigrated), 0, ""), http.StatusGone},
|
||||
{"ResInvalidState", New(10, uint32(code.ResInvalidState), 0, ""), http.StatusConflict},
|
||||
{"ResInsufficientQuota", New(10, uint32(code.ResInsufficientQuota), 0, ""), http.StatusTooManyRequests},
|
||||
{"ResMultiOwner", New(10, uint32(code.ResMultiOwner), 0, ""), http.StatusConflict},
|
||||
{"AuthUnauthorized", New(10, uint32(code.AuthUnauthorized), 0, ""), http.StatusUnauthorized},
|
||||
{"AuthExpired", New(10, uint32(code.AuthExpired), 0, ""), http.StatusUnauthorized},
|
||||
{"AuthInvalidPosixTime", New(10, uint32(code.AuthInvalidPosixTime), 0, ""), http.StatusUnauthorized},
|
||||
{"AuthSigPayloadMismatch", New(10, uint32(code.AuthSigPayloadMismatch), 0, ""), http.StatusUnauthorized},
|
||||
{"AuthForbidden", New(10, uint32(code.AuthForbidden), 0, ""), http.StatusForbidden},
|
||||
{"SysTooManyRequest", New(10, uint32(code.SysTooManyRequest), 0, ""), http.StatusTooManyRequests},
|
||||
{"SysInternal", New(10, uint32(code.SysInternal), 0, ""), http.StatusInternalServerError},
|
||||
{"SysMaintain", New(10, uint32(code.SysMaintain), 0, ""), http.StatusServiceUnavailable},
|
||||
{"SysTimeout", New(10, uint32(code.SysTimeout), 0, ""), http.StatusGatewayTimeout},
|
||||
{"PSuPublish", New(10, uint32(code.PSuPublish), 0, ""), http.StatusBadGateway},
|
||||
{"PSuConsume", New(10, uint32(code.PSuConsume), 0, ""), http.StatusBadGateway},
|
||||
{"PSuTooLarge", New(10, uint32(code.PSuTooLarge), 0, ""), http.StatusRequestEntityTooLarge},
|
||||
{"SvcMaintenance", New(10, uint32(code.SvcMaintenance), 0, ""), http.StatusServiceUnavailable},
|
||||
{"SvcInternal", New(10, uint32(code.SvcInternal), 0, ""), http.StatusInternalServerError},
|
||||
{"SvcThirdParty", New(10, uint32(code.SvcThirdParty), 0, ""), http.StatusBadGateway},
|
||||
{"SvcHTTP400", New(10, uint32(code.SvcHTTP400), 0, ""), http.StatusBadRequest},
|
||||
{"fallback unknown", New(10, 999, 0, ""), http.StatusInternalServerError},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.err.HTTPStatus(); got != tt.want {
|
||||
t.Errorf("HTTPStatus() = %d, want %d", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,467 +0,0 @@
|
|||
package errs
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"backend/pkg/library/errors/code"
|
||||
)
|
||||
|
||||
/* =========================
|
||||
日誌介面(與你現有 Logger 對齊)
|
||||
========================= */
|
||||
|
||||
// Logger 你現有的 logger 介面(與外部一致即可)
|
||||
type Logger interface {
|
||||
WithCallerSkip(n int) Logger
|
||||
WithFields(fields ...LogField) Logger
|
||||
Error(msg string)
|
||||
Warn(msg string)
|
||||
Info(msg string)
|
||||
}
|
||||
|
||||
// LogField 結構化欄位
|
||||
type LogField struct {
|
||||
Key string
|
||||
Val any
|
||||
}
|
||||
|
||||
/* =========================
|
||||
共用小工具
|
||||
========================= */
|
||||
|
||||
// joinMsg:把可變參數字串用空白串接(避免到處判斷 nil / 空 slice)
|
||||
func joinMsg(s []string) string {
|
||||
if len(s) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.Join(s, " ")
|
||||
}
|
||||
|
||||
// logErr:統一打一筆 error log(避免重複記錄)
|
||||
func logErr(l Logger, fields []LogField, e *Error) {
|
||||
if l == nil || e == nil {
|
||||
return
|
||||
}
|
||||
ll := l.WithCallerSkip(1)
|
||||
if len(fields) > 0 {
|
||||
ll = ll.WithFields(fields...)
|
||||
}
|
||||
// 需要更多欄位可在此擴充,例如:e.DisplayCode()、e.Category()、e.Detail()
|
||||
ll.Error(e.Error())
|
||||
}
|
||||
|
||||
/* =========================
|
||||
共用裝飾器(把任意 ez 建構器包成帶日誌版本)
|
||||
========================= */
|
||||
|
||||
// WithLog 將任一 *Error 建構器(如 SysTimeoutError)轉成帶日誌的版本
|
||||
func WithLog(l Logger, fields []LogField, ctor func(s ...string) *Error, s ...string) *Error {
|
||||
e := ctor(s...)
|
||||
logErr(l, fields, e)
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// WithLogWrap 同上,但會同時 Wrap 內部 cause
|
||||
func WithLogWrap(l Logger, fields []LogField, ctor func(s ...string) *Error, cause error, s ...string) *Error {
|
||||
e := ctor(s...).Wrap(cause)
|
||||
logErr(l, fields, e)
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
/* =========================
|
||||
泛用建構器(當你懶得記函式名時)
|
||||
========================= */
|
||||
|
||||
// EL 依 Category/Detail 直接建構並記錄日誌
|
||||
func EL(l Logger, fields []LogField, cat code.Category, det code.Detail, s ...string) *Error {
|
||||
e := New(uint32(Scope), uint32(cat), uint32(det), joinMsg(s))
|
||||
logErr(l, fields, e)
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// ELWrap 同上,並 Wrap cause
|
||||
func ELWrap(l Logger, fields []LogField, cat code.Category, det code.Detail, cause error, s ...string) *Error {
|
||||
e := New(uint32(Scope), uint32(cat), uint32(det), joinMsg(s)).Wrap(cause)
|
||||
logErr(l, fields, e)
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
一、基礎 ez 建構器(純建構 *Error,不帶日誌)
|
||||
分類順序:Input → DB → Resource → Auth → System → PubSub → Service
|
||||
======================================================================= */
|
||||
|
||||
/* ----- Input (CatInput) ----- */
|
||||
|
||||
func InputInvalidFormatError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.InputInvalidFormat), 0, joinMsg(s))
|
||||
}
|
||||
func InputNotValidImplementationError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.InputNotValidImplementation), 0, joinMsg(s))
|
||||
}
|
||||
func InputInvalidRangeError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.InputInvalidRange), 0, joinMsg(s))
|
||||
}
|
||||
|
||||
/* ----- DB (CatDB) ----- */
|
||||
|
||||
func DBErrorError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.DBError), 0, joinMsg(s))
|
||||
}
|
||||
func DBDataConvertError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.DBDataConvert), 0, joinMsg(s))
|
||||
}
|
||||
func DBDuplicateError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.DBDuplicate), 0, joinMsg(s))
|
||||
}
|
||||
|
||||
/* ----- Resource (CatResource) ----- */
|
||||
|
||||
func ResNotFoundError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.ResNotFound), 0, joinMsg(s))
|
||||
}
|
||||
func ResInvalidFormatError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.ResInvalidFormat), 0, joinMsg(s))
|
||||
}
|
||||
func ResAlreadyExistError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.ResAlreadyExist), 0, joinMsg(s))
|
||||
}
|
||||
func ResInsufficientError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.ResInsufficient), 0, joinMsg(s))
|
||||
}
|
||||
func ResInsufficientPermError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.ResInsufficientPerm), 0, joinMsg(s))
|
||||
}
|
||||
func ResInvalidMeasureIDError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.ResInvalidMeasureID), 0, joinMsg(s))
|
||||
}
|
||||
func ResExpiredError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.ResExpired), 0, joinMsg(s))
|
||||
}
|
||||
func ResMigratedError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.ResMigrated), 0, joinMsg(s))
|
||||
}
|
||||
func ResInvalidStateError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.ResInvalidState), 0, joinMsg(s))
|
||||
}
|
||||
func ResInsufficientQuotaError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.ResInsufficientQuota), 0, joinMsg(s))
|
||||
}
|
||||
func ResMultiOwnerError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.ResMultiOwner), 0, joinMsg(s))
|
||||
}
|
||||
|
||||
/* ----- Auth (CatAuth) ----- */
|
||||
|
||||
func AuthUnauthorizedError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.AuthUnauthorized), 0, joinMsg(s))
|
||||
}
|
||||
func AuthExpiredError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.AuthExpired), 0, joinMsg(s))
|
||||
}
|
||||
func AuthInvalidPosixTimeError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.AuthInvalidPosixTime), 0, joinMsg(s))
|
||||
}
|
||||
func AuthSigPayloadMismatchError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.AuthSigPayloadMismatch), 0, joinMsg(s))
|
||||
}
|
||||
func AuthForbiddenError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.AuthForbidden), 0, joinMsg(s))
|
||||
}
|
||||
|
||||
/* ----- System (CatSystem) ----- */
|
||||
|
||||
func SysInternalError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.SysInternal), 0, joinMsg(s))
|
||||
}
|
||||
func SysMaintainError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.SysMaintain), 0, joinMsg(s))
|
||||
}
|
||||
func SysTimeoutError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.SysTimeout), 0, joinMsg(s))
|
||||
}
|
||||
func SysTooManyRequestError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.SysTooManyRequest), 0, joinMsg(s))
|
||||
}
|
||||
|
||||
/* ----- PubSub (CatPubSub) ----- */
|
||||
|
||||
func PSuPublishError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.PSuPublish), 0, joinMsg(s))
|
||||
}
|
||||
func PSuConsumeError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.PSuConsume), 0, joinMsg(s))
|
||||
}
|
||||
func PSuTooLargeError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.PSuTooLarge), 0, joinMsg(s))
|
||||
}
|
||||
|
||||
/* ----- Service (CatService) ----- */
|
||||
|
||||
func SvcInternalError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.SvcInternal), 0, joinMsg(s))
|
||||
}
|
||||
func SvcThirdPartyError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.SvcThirdParty), 0, joinMsg(s))
|
||||
}
|
||||
func SvcHTTP400Error(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.SvcHTTP400), 0, joinMsg(s))
|
||||
}
|
||||
func SvcMaintenanceError(s ...string) *Error {
|
||||
return New(uint32(Scope), uint32(code.SvcMaintenance), 0, joinMsg(s))
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
二、帶日誌版本:L / WrapL(在「基礎 ez 建構器」之上包裝 WithLog / WithLogWrap)
|
||||
分類順序同上:Input → DB → Resource → Auth → System → PubSub → Service
|
||||
============================================================================= */
|
||||
|
||||
/* ----- Input (CatInput) ----- */
|
||||
|
||||
func InputInvalidFormatErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, InputInvalidFormatError, s...)
|
||||
}
|
||||
func InputInvalidFormatErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, InputInvalidFormatError, cause, s...)
|
||||
}
|
||||
|
||||
func InputNotValidImplementationErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, InputNotValidImplementationError, s...)
|
||||
}
|
||||
func InputNotValidImplementationErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, InputNotValidImplementationError, cause, s...)
|
||||
}
|
||||
|
||||
func InputInvalidRangeErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, InputInvalidRangeError, s...)
|
||||
}
|
||||
func InputInvalidRangeErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, InputInvalidRangeError, cause, s...)
|
||||
}
|
||||
|
||||
/* ----- DB (CatDB) ----- */
|
||||
|
||||
func DBErrorErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, DBErrorError, s...)
|
||||
}
|
||||
func DBErrorErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, DBErrorError, cause, s...)
|
||||
}
|
||||
|
||||
func DBDataConvertErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, DBDataConvertError, s...)
|
||||
}
|
||||
func DBDataConvertErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, DBDataConvertError, cause, s...)
|
||||
}
|
||||
|
||||
func DBDuplicateErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, DBDuplicateError, s...)
|
||||
}
|
||||
func DBDuplicateErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, DBDuplicateError, cause, s...)
|
||||
}
|
||||
|
||||
/* ----- Resource (CatResource) ----- */
|
||||
|
||||
func ResNotFoundErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, ResNotFoundError, s...)
|
||||
}
|
||||
func ResNotFoundErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, ResNotFoundError, cause, s...)
|
||||
}
|
||||
|
||||
func ResInvalidFormatErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, ResInvalidFormatError, s...)
|
||||
}
|
||||
func ResInvalidFormatErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, ResInvalidFormatError, cause, s...)
|
||||
}
|
||||
|
||||
func ResAlreadyExistErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, ResAlreadyExistError, s...)
|
||||
}
|
||||
func ResAlreadyExistErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, ResAlreadyExistError, cause, s...)
|
||||
}
|
||||
|
||||
func ResInsufficientErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, ResInsufficientError, s...)
|
||||
}
|
||||
func ResInsufficientErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, ResInsufficientError, cause, s...)
|
||||
}
|
||||
|
||||
func ResInsufficientPermErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, ResInsufficientPermError, s...)
|
||||
}
|
||||
func ResInsufficientPermErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, ResInsufficientPermError, cause, s...)
|
||||
}
|
||||
|
||||
func ResInvalidMeasureIDErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, ResInvalidMeasureIDError, s...)
|
||||
}
|
||||
func ResInvalidMeasureIDErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, ResInvalidMeasureIDError, cause, s...)
|
||||
}
|
||||
|
||||
func ResExpiredErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, ResExpiredError, s...)
|
||||
}
|
||||
func ResExpiredErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, ResExpiredError, cause, s...)
|
||||
}
|
||||
|
||||
func ResMigratedErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, ResMigratedError, s...)
|
||||
}
|
||||
func ResMigratedErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, ResMigratedError, cause, s...)
|
||||
}
|
||||
|
||||
func ResInvalidStateErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, ResInvalidStateError, s...)
|
||||
}
|
||||
func ResInvalidStateErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, ResInvalidStateError, cause, s...)
|
||||
}
|
||||
|
||||
func ResInsufficientQuotaErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, ResInsufficientQuotaError, s...)
|
||||
}
|
||||
func ResInsufficientQuotaErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, ResInsufficientQuotaError, cause, s...)
|
||||
}
|
||||
|
||||
func ResMultiOwnerErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, ResMultiOwnerError, s...)
|
||||
}
|
||||
func ResMultiOwnerErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, ResMultiOwnerError, cause, s...)
|
||||
}
|
||||
|
||||
/* ----- Auth (CatAuth) ----- */
|
||||
|
||||
func AuthUnauthorizedErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, AuthUnauthorizedError, s...)
|
||||
}
|
||||
func AuthUnauthorizedErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, AuthUnauthorizedError, cause, s...)
|
||||
}
|
||||
|
||||
func AuthExpiredErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, AuthExpiredError, s...)
|
||||
}
|
||||
func AuthExpiredErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, AuthExpiredError, cause, s...)
|
||||
}
|
||||
|
||||
func AuthInvalidPosixTimeErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, AuthInvalidPosixTimeError, s...)
|
||||
}
|
||||
func AuthInvalidPosixTimeErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, AuthInvalidPosixTimeError, cause, s...)
|
||||
}
|
||||
|
||||
func AuthSigPayloadMismatchErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, AuthSigPayloadMismatchError, s...)
|
||||
}
|
||||
func AuthSigPayloadMismatchErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, AuthSigPayloadMismatchError, cause, s...)
|
||||
}
|
||||
|
||||
func AuthForbiddenErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, AuthForbiddenError, s...)
|
||||
}
|
||||
func AuthForbiddenErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, AuthForbiddenError, cause, s...)
|
||||
}
|
||||
|
||||
/* ----- System (CatSystem) ----- */
|
||||
|
||||
func SysInternalErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, SysInternalError, s...)
|
||||
}
|
||||
func SysInternalErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, SysInternalError, cause, s...)
|
||||
}
|
||||
|
||||
func SysMaintainErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, SysMaintainError, s...)
|
||||
}
|
||||
func SysMaintainErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, SysMaintainError, cause, s...)
|
||||
}
|
||||
|
||||
func SysTimeoutErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, SysTimeoutError, s...)
|
||||
}
|
||||
func SysTimeoutErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, SysTimeoutError, cause, s...)
|
||||
}
|
||||
|
||||
func SysTooManyRequestErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, SysTooManyRequestError, s...)
|
||||
}
|
||||
func SysTooManyRequestErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, SysTooManyRequestError, cause, s...)
|
||||
}
|
||||
|
||||
/* ----- PubSub (CatPubSub) ----- */
|
||||
|
||||
func PSuPublishErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, PSuPublishError, s...)
|
||||
}
|
||||
func PSuPublishErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, PSuPublishError, cause, s...)
|
||||
}
|
||||
|
||||
func PSuConsumeErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, PSuConsumeError, s...)
|
||||
}
|
||||
func PSuConsumeErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, PSuConsumeError, cause, s...)
|
||||
}
|
||||
|
||||
func PSuTooLargeErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, PSuTooLargeError, s...)
|
||||
}
|
||||
func PSuTooLargeErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, PSuTooLargeError, cause, s...)
|
||||
}
|
||||
|
||||
/* ----- Service (CatService) ----- */
|
||||
|
||||
func SvcInternalErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, SvcInternalError, s...)
|
||||
}
|
||||
func SvcInternalErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, SvcInternalError, cause, s...)
|
||||
}
|
||||
|
||||
func SvcThirdPartyErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, SvcThirdPartyError, s...)
|
||||
}
|
||||
func SvcThirdPartyErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, SvcThirdPartyError, cause, s...)
|
||||
}
|
||||
|
||||
func SvcHTTP400ErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, SvcHTTP400Error, s...)
|
||||
}
|
||||
func SvcHTTP400ErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, SvcHTTP400Error, cause, s...)
|
||||
}
|
||||
|
||||
func SvcMaintenanceErrorL(l Logger, fields []LogField, s ...string) *Error {
|
||||
return WithLog(l, fields, SvcMaintenanceError, s...)
|
||||
}
|
||||
func SvcMaintenanceErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error {
|
||||
return WithLogWrap(l, fields, SvcMaintenanceError, cause, s...)
|
||||
}
|
||||
|
|
@ -1,347 +0,0 @@
|
|||
package errs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"backend/pkg/library/errors/code"
|
||||
)
|
||||
|
||||
// fakeLogger as before
|
||||
type fakeLogger struct {
|
||||
calls []string
|
||||
lastMsg string
|
||||
fieldsStack [][]LogField
|
||||
callerSkips []int
|
||||
}
|
||||
|
||||
func (l *fakeLogger) WithCallerSkip(n int) Logger { l.callerSkips = append(l.callerSkips, n); return l }
|
||||
func (l *fakeLogger) WithFields(fields ...LogField) Logger {
|
||||
cp := make([]LogField, len(fields))
|
||||
copy(cp, fields)
|
||||
l.fieldsStack = append(l.fieldsStack, cp)
|
||||
return l
|
||||
}
|
||||
func (l *fakeLogger) Error(msg string) { l.calls = append(l.calls, "ERROR"); l.lastMsg = msg }
|
||||
func (l *fakeLogger) Warn(msg string) { l.calls = append(l.calls, "WARN"); l.lastMsg = msg }
|
||||
func (l *fakeLogger) Info(msg string) { l.calls = append(l.calls, "INFO"); l.lastMsg = msg }
|
||||
func (l *fakeLogger) reset() {
|
||||
l.calls, l.lastMsg, l.fieldsStack, l.callerSkips = nil, "", nil, nil
|
||||
}
|
||||
|
||||
func init() { Scope = code.Gateway }
|
||||
|
||||
func TestJoinMsg(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in []string
|
||||
want string
|
||||
}{
|
||||
{"nil", nil, ""},
|
||||
{"empty", []string{}, ""},
|
||||
{"single", []string{"a"}, "a"},
|
||||
{"multi", []string{"a", "b", "c"}, "a b c"},
|
||||
{"with spaces", []string{"hello", "world"}, "hello world"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := joinMsg(tt.in); got != tt.want {
|
||||
t.Errorf("joinMsg() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogErr(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
l Logger
|
||||
fields []LogField
|
||||
e *Error
|
||||
wantCall string
|
||||
wantFields bool
|
||||
wantCallerSkip int
|
||||
}{
|
||||
{"nil logger", nil, nil, New(10, 101, 0, "err"), "", false, 0},
|
||||
{"nil error", &fakeLogger{}, nil, nil, "", false, 0},
|
||||
{"basic log", &fakeLogger{}, nil, New(10, 101, 0, "err"), "ERROR", false, 1},
|
||||
{"with fields", &fakeLogger{}, []LogField{{Key: "k", Val: "v"}}, New(10, 101, 0, "err"), "ERROR", true, 1},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var fl *fakeLogger
|
||||
if tt.l != nil {
|
||||
var ok bool
|
||||
fl, ok = tt.l.(*fakeLogger)
|
||||
if !ok {
|
||||
t.Fatalf("logger is not *fakeLogger")
|
||||
}
|
||||
fl.reset()
|
||||
}
|
||||
logErr(tt.l, tt.fields, tt.e)
|
||||
if fl == nil {
|
||||
if tt.wantCall != "" {
|
||||
t.Errorf("expected log but logger is nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
if tt.wantCall == "" && len(fl.calls) > 0 {
|
||||
t.Errorf("unexpected log call")
|
||||
}
|
||||
if tt.wantCall != "" && (len(fl.calls) == 0 || fl.calls[0] != tt.wantCall) {
|
||||
t.Errorf("expected call %q, got %v", tt.wantCall, fl.calls)
|
||||
}
|
||||
if tt.wantFields && (len(fl.fieldsStack) == 0 || !reflect.DeepEqual(fl.fieldsStack[0], tt.fields)) {
|
||||
t.Errorf("fields mismatch: got %v, want %v", fl.fieldsStack, tt.fields)
|
||||
}
|
||||
if tt.wantCallerSkip != 0 && (len(fl.callerSkips) == 0 || fl.callerSkips[0] != tt.wantCallerSkip) {
|
||||
t.Errorf("callerSkip = %v, want %d", fl.callerSkips, tt.wantCallerSkip)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cat code.Category
|
||||
det code.Detail
|
||||
s []string
|
||||
wantCat uint32
|
||||
wantDet uint32
|
||||
wantMsg string
|
||||
wantLog bool
|
||||
}{
|
||||
{"basic", code.ResNotFound, 123, []string{"not found"}, uint32(code.ResNotFound), 123, "not found", true},
|
||||
{"nil logger", code.ResNotFound, 0, []string{}, uint32(code.ResNotFound), 0, "", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := &fakeLogger{}
|
||||
e := EL(l, nil, tt.cat, tt.det, tt.s...)
|
||||
if e.Category() != tt.wantCat || e.Detail() != tt.wantDet || e.Error() != tt.wantMsg {
|
||||
t.Errorf("EL = cat=%d det=%d msg=%q, want %d %d %q", e.Category(), e.Detail(), e.Error(), tt.wantCat, tt.wantDet, tt.wantMsg)
|
||||
}
|
||||
if tt.wantLog && len(l.calls) == 0 {
|
||||
t.Errorf("expected log")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestELWrap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cat code.Category
|
||||
det code.Detail
|
||||
cause error
|
||||
s []string
|
||||
wantCat uint32
|
||||
wantDet uint32
|
||||
wantMsg string
|
||||
wantUnwrap string
|
||||
wantLog bool
|
||||
}{
|
||||
{"basic", code.SysInternal, 456, errors.New("internal"), []string{"sys err"}, uint32(code.SysInternal), 456, "sys err", "internal", true},
|
||||
{"no log", code.SysInternal, 0, nil, []string{}, uint32(code.SysInternal), 0, "", "", false}, // nil cause ok
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := &fakeLogger{}
|
||||
e := ELWrap(l, nil, tt.cat, tt.det, tt.cause, tt.s...)
|
||||
if e.Category() != tt.wantCat || e.Detail() != tt.wantDet || e.Error() != tt.wantMsg {
|
||||
t.Errorf("ELWrap = cat=%d det=%d msg=%q, want %d %d %q", e.Category(), e.Detail(), e.Error(), tt.wantCat, tt.wantDet, tt.wantMsg)
|
||||
}
|
||||
unw := e.Unwrap()
|
||||
gotUnwrap := ""
|
||||
if unw != nil {
|
||||
gotUnwrap = unw.Error()
|
||||
}
|
||||
if gotUnwrap != tt.wantUnwrap {
|
||||
t.Errorf("Unwrap = %q, want %q", gotUnwrap, tt.wantUnwrap)
|
||||
}
|
||||
if tt.wantLog && len(l.calls) == 0 {
|
||||
t.Errorf("expected log")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Expand TestBaseConstructors with all base funcs
|
||||
func TestBaseConstructors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fn func(...string) *Error
|
||||
wantCat uint32
|
||||
wantDet uint32
|
||||
wantMsg string
|
||||
}{
|
||||
{"InputInvalidFormatError", InputInvalidFormatError, uint32(code.InputInvalidFormat), 0, "test msg"},
|
||||
{"InputNotValidImplementationError", InputNotValidImplementationError, uint32(code.InputNotValidImplementation), 0, "test msg"},
|
||||
{"InputInvalidRangeError", InputInvalidRangeError, uint32(code.InputInvalidRange), 0, "test msg"},
|
||||
{"DBErrorError", DBErrorError, uint32(code.DBError), 0, "test msg"},
|
||||
{"DBDataConvertError", DBDataConvertError, uint32(code.DBDataConvert), 0, "test msg"},
|
||||
{"DBDuplicateError", DBDuplicateError, uint32(code.DBDuplicate), 0, "test msg"},
|
||||
{"ResNotFoundError", ResNotFoundError, uint32(code.ResNotFound), 0, "test msg"},
|
||||
{"ResInvalidFormatError", ResInvalidFormatError, uint32(code.ResInvalidFormat), 0, "test msg"},
|
||||
{"ResAlreadyExistError", ResAlreadyExistError, uint32(code.ResAlreadyExist), 0, "test msg"},
|
||||
{"ResInsufficientError", ResInsufficientError, uint32(code.ResInsufficient), 0, "test msg"},
|
||||
{"ResInsufficientPermError", ResInsufficientPermError, uint32(code.ResInsufficientPerm), 0, "test msg"},
|
||||
{"ResInvalidMeasureIDError", ResInvalidMeasureIDError, uint32(code.ResInvalidMeasureID), 0, "test msg"},
|
||||
{"ResExpiredError", ResExpiredError, uint32(code.ResExpired), 0, "test msg"},
|
||||
{"ResMigratedError", ResMigratedError, uint32(code.ResMigrated), 0, "test msg"},
|
||||
{"ResInvalidStateError", ResInvalidStateError, uint32(code.ResInvalidState), 0, "test msg"},
|
||||
{"ResInsufficientQuotaError", ResInsufficientQuotaError, uint32(code.ResInsufficientQuota), 0, "test msg"},
|
||||
{"ResMultiOwnerError", ResMultiOwnerError, uint32(code.ResMultiOwner), 0, "test msg"},
|
||||
{"AuthUnauthorizedError", AuthUnauthorizedError, uint32(code.AuthUnauthorized), 0, "test msg"},
|
||||
{"AuthExpiredError", AuthExpiredError, uint32(code.AuthExpired), 0, "test msg"},
|
||||
{"AuthInvalidPosixTimeError", AuthInvalidPosixTimeError, uint32(code.AuthInvalidPosixTime), 0, "test msg"},
|
||||
{"AuthSigPayloadMismatchError", AuthSigPayloadMismatchError, uint32(code.AuthSigPayloadMismatch), 0, "test msg"},
|
||||
{"AuthForbiddenError", AuthForbiddenError, uint32(code.AuthForbidden), 0, "test msg"},
|
||||
{"SysInternalError", SysInternalError, uint32(code.SysInternal), 0, "test msg"},
|
||||
{"SysMaintainError", SysMaintainError, uint32(code.SysMaintain), 0, "test msg"},
|
||||
{"SysTimeoutError", SysTimeoutError, uint32(code.SysTimeout), 0, "test msg"},
|
||||
{"SysTooManyRequestError", SysTooManyRequestError, uint32(code.SysTooManyRequest), 0, "test msg"},
|
||||
{"PSuPublishError", PSuPublishError, uint32(code.PSuPublish), 0, "test msg"},
|
||||
{"PSuConsumeError", PSuConsumeError, uint32(code.PSuConsume), 0, "test msg"},
|
||||
{"PSuTooLargeError", PSuTooLargeError, uint32(code.PSuTooLarge), 0, "test msg"},
|
||||
{"SvcInternalError", SvcInternalError, uint32(code.SvcInternal), 0, "test msg"},
|
||||
{"SvcThirdPartyError", SvcThirdPartyError, uint32(code.SvcThirdParty), 0, "test msg"},
|
||||
{"SvcHTTP400Error", SvcHTTP400Error, uint32(code.SvcHTTP400), 0, "test msg"},
|
||||
{"SvcMaintenanceError", SvcMaintenanceError, uint32(code.SvcMaintenance), 0, "test msg"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
e := tt.fn("test", "msg")
|
||||
if e == nil || e.Category() != tt.wantCat || e.Detail() != tt.wantDet || e.Error() != "test msg" {
|
||||
t.Errorf("%s = cat=%d det=%d msg=%q, want %d 0 %q", tt.name, e.Category(), e.Detail(), e.Error(), tt.wantCat, "test msg")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Expand TestLConstructors with all L funcs
|
||||
func TestLConstructors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fn func(Logger, []LogField, ...string) *Error
|
||||
wantCat uint32
|
||||
wantDet uint32
|
||||
wantMsg string
|
||||
wantLog bool
|
||||
}{
|
||||
{"InputInvalidFormatErrorL", InputInvalidFormatErrorL, uint32(code.InputInvalidFormat), 0, "test msg", true},
|
||||
{"InputNotValidImplementationErrorL", InputNotValidImplementationErrorL, uint32(code.InputNotValidImplementation), 0, "test msg", true},
|
||||
{"InputInvalidRangeErrorL", InputInvalidRangeErrorL, uint32(code.InputInvalidRange), 0, "test msg", true},
|
||||
{"DBErrorErrorL", DBErrorErrorL, uint32(code.DBError), 0, "test msg", true},
|
||||
{"DBDataConvertErrorL", DBDataConvertErrorL, uint32(code.DBDataConvert), 0, "test msg", true},
|
||||
{"DBDuplicateErrorL", DBDuplicateErrorL, uint32(code.DBDuplicate), 0, "test msg", true},
|
||||
{"ResNotFoundErrorL", ResNotFoundErrorL, uint32(code.ResNotFound), 0, "test msg", true},
|
||||
{"ResInvalidFormatErrorL", ResInvalidFormatErrorL, uint32(code.ResInvalidFormat), 0, "test msg", true},
|
||||
{"ResAlreadyExistErrorL", ResAlreadyExistErrorL, uint32(code.ResAlreadyExist), 0, "test msg", true},
|
||||
{"ResInsufficientErrorL", ResInsufficientErrorL, uint32(code.ResInsufficient), 0, "test msg", true},
|
||||
{"ResInsufficientPermErrorL", ResInsufficientPermErrorL, uint32(code.ResInsufficientPerm), 0, "test msg", true},
|
||||
{"ResInvalidMeasureIDErrorL", ResInvalidMeasureIDErrorL, uint32(code.ResInvalidMeasureID), 0, "test msg", true},
|
||||
{"ResExpiredErrorL", ResExpiredErrorL, uint32(code.ResExpired), 0, "test msg", true},
|
||||
{"ResMigratedErrorL", ResMigratedErrorL, uint32(code.ResMigrated), 0, "test msg", true},
|
||||
{"ResInvalidStateErrorL", ResInvalidStateErrorL, uint32(code.ResInvalidState), 0, "test msg", true},
|
||||
{"ResInsufficientQuotaErrorL", ResInsufficientQuotaErrorL, uint32(code.ResInsufficientQuota), 0, "test msg", true},
|
||||
{"ResMultiOwnerErrorL", ResMultiOwnerErrorL, uint32(code.ResMultiOwner), 0, "test msg", true},
|
||||
{"AuthUnauthorizedErrorL", AuthUnauthorizedErrorL, uint32(code.AuthUnauthorized), 0, "test msg", true},
|
||||
{"AuthExpiredErrorL", AuthExpiredErrorL, uint32(code.AuthExpired), 0, "test msg", true},
|
||||
{"AuthInvalidPosixTimeErrorL", AuthInvalidPosixTimeErrorL, uint32(code.AuthInvalidPosixTime), 0, "test msg", true},
|
||||
{"AuthSigPayloadMismatchErrorL", AuthSigPayloadMismatchErrorL, uint32(code.AuthSigPayloadMismatch), 0, "test msg", true},
|
||||
{"AuthForbiddenErrorL", AuthForbiddenErrorL, uint32(code.AuthForbidden), 0, "test msg", true},
|
||||
{"SysInternalErrorL", SysInternalErrorL, uint32(code.SysInternal), 0, "test msg", true},
|
||||
{"SysMaintainErrorL", SysMaintainErrorL, uint32(code.SysMaintain), 0, "test msg", true},
|
||||
{"SysTimeoutErrorL", SysTimeoutErrorL, uint32(code.SysTimeout), 0, "test msg", true},
|
||||
{"SysTooManyRequestErrorL", SysTooManyRequestErrorL, uint32(code.SysTooManyRequest), 0, "test msg", true},
|
||||
{"PSuPublishErrorL", PSuPublishErrorL, uint32(code.PSuPublish), 0, "test msg", true},
|
||||
{"PSuConsumeErrorL", PSuConsumeErrorL, uint32(code.PSuConsume), 0, "test msg", true},
|
||||
{"PSuTooLargeErrorL", PSuTooLargeErrorL, uint32(code.PSuTooLarge), 0, "test msg", true},
|
||||
{"SvcInternalErrorL", SvcInternalErrorL, uint32(code.SvcInternal), 0, "test msg", true},
|
||||
{"SvcThirdPartyErrorL", SvcThirdPartyErrorL, uint32(code.SvcThirdParty), 0, "test msg", true},
|
||||
{"SvcHTTP400ErrorL", SvcHTTP400ErrorL, uint32(code.SvcHTTP400), 0, "test msg", true},
|
||||
{"SvcMaintenanceErrorL", SvcMaintenanceErrorL, uint32(code.SvcMaintenance), 0, "test msg", true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := &fakeLogger{}
|
||||
fields := []LogField{}
|
||||
e := tt.fn(l, fields, "test", "msg")
|
||||
if e == nil || e.Category() != tt.wantCat || e.Detail() != tt.wantDet || e.Error() != "test msg" {
|
||||
t.Errorf("%s = cat=%d det=%d msg=%q, want %d 0 %q", tt.name, e.Category(), e.Detail(), e.Error(), tt.wantCat, "test msg")
|
||||
}
|
||||
if tt.wantLog && len(l.calls) == 0 {
|
||||
t.Errorf("expected log call")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Add TestWrapLConstructors similarly
|
||||
func TestWrapLConstructors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fn func(Logger, []LogField, error, ...string) *Error
|
||||
wantCat uint32
|
||||
wantDet uint32
|
||||
wantMsg string
|
||||
wantUnwrap string
|
||||
wantLog bool
|
||||
}{
|
||||
{"InputInvalidFormatErrorWrapL", InputInvalidFormatErrorWrapL, uint32(code.InputInvalidFormat), 0, "test msg", "cause err", true},
|
||||
{"InputNotValidImplementationErrorWrapL", InputNotValidImplementationErrorWrapL, uint32(code.InputNotValidImplementation), 0, "test msg", "cause err", true},
|
||||
{"InputInvalidRangeErrorWrapL", InputInvalidRangeErrorWrapL, uint32(code.InputInvalidRange), 0, "test msg", "cause err", true},
|
||||
{"DBErrorErrorWrapL", DBErrorErrorWrapL, uint32(code.DBError), 0, "test msg", "cause err", true},
|
||||
{"DBDataConvertErrorWrapL", DBDataConvertErrorWrapL, uint32(code.DBDataConvert), 0, "test msg", "cause err", true},
|
||||
{"DBDuplicateErrorWrapL", DBDuplicateErrorWrapL, uint32(code.DBDuplicate), 0, "test msg", "cause err", true},
|
||||
{"ResNotFoundErrorWrapL", ResNotFoundErrorWrapL, uint32(code.ResNotFound), 0, "test msg", "cause err", true},
|
||||
{"ResInvalidFormatErrorWrapL", ResInvalidFormatErrorWrapL, uint32(code.ResInvalidFormat), 0, "test msg", "cause err", true},
|
||||
{"ResAlreadyExistErrorWrapL", ResAlreadyExistErrorWrapL, uint32(code.ResAlreadyExist), 0, "test msg", "cause err", true},
|
||||
{"ResInsufficientErrorWrapL", ResInsufficientErrorWrapL, uint32(code.ResInsufficient), 0, "test msg", "cause err", true},
|
||||
{"ResInsufficientPermErrorWrapL", ResInsufficientPermErrorWrapL, uint32(code.ResInsufficientPerm), 0, "test msg", "cause err", true},
|
||||
{"ResInvalidMeasureIDErrorWrapL", ResInvalidMeasureIDErrorWrapL, uint32(code.ResInvalidMeasureID), 0, "test msg", "cause err", true},
|
||||
{"ResExpiredErrorWrapL", ResExpiredErrorWrapL, uint32(code.ResExpired), 0, "test msg", "cause err", true},
|
||||
{"ResMigratedErrorWrapL", ResMigratedErrorWrapL, uint32(code.ResMigrated), 0, "test msg", "cause err", true},
|
||||
{"ResInvalidStateErrorWrapL", ResInvalidStateErrorWrapL, uint32(code.ResInvalidState), 0, "test msg", "cause err", true},
|
||||
{"ResInsufficientQuotaErrorWrapL", ResInsufficientQuotaErrorWrapL, uint32(code.ResInsufficientQuota), 0, "test msg", "cause err", true},
|
||||
{"ResMultiOwnerErrorWrapL", ResMultiOwnerErrorWrapL, uint32(code.ResMultiOwner), 0, "test msg", "cause err", true},
|
||||
{"AuthUnauthorizedErrorWrapL", AuthUnauthorizedErrorWrapL, uint32(code.AuthUnauthorized), 0, "test msg", "cause err", true},
|
||||
{"AuthExpiredErrorWrapL", AuthExpiredErrorWrapL, uint32(code.AuthExpired), 0, "test msg", "cause err", true},
|
||||
{"AuthInvalidPosixTimeErrorWrapL", AuthInvalidPosixTimeErrorWrapL, uint32(code.AuthInvalidPosixTime), 0, "test msg", "cause err", true},
|
||||
{"AuthSigPayloadMismatchErrorWrapL", AuthSigPayloadMismatchErrorWrapL, uint32(code.AuthSigPayloadMismatch), 0, "test msg", "cause err", true},
|
||||
{"AuthForbiddenErrorWrapL", AuthForbiddenErrorWrapL, uint32(code.AuthForbidden), 0, "test msg", "cause err", true},
|
||||
{"SysInternalErrorWrapL", SysInternalErrorWrapL, uint32(code.SysInternal), 0, "test msg", "cause err", true},
|
||||
{"SysMaintainErrorWrapL", SysMaintainErrorWrapL, uint32(code.SysMaintain), 0, "test msg", "cause err", true},
|
||||
{"SysTimeoutErrorWrapL", SysTimeoutErrorWrapL, uint32(code.SysTimeout), 0, "test msg", "cause err", true},
|
||||
{"SysTooManyRequestErrorWrapL", SysTooManyRequestErrorWrapL, uint32(code.SysTooManyRequest), 0, "test msg", "cause err", true},
|
||||
{"PSuPublishErrorWrapL", PSuPublishErrorWrapL, uint32(code.PSuPublish), 0, "test msg", "cause err", true},
|
||||
{"PSuConsumeErrorWrapL", PSuConsumeErrorWrapL, uint32(code.PSuConsume), 0, "test msg", "cause err", true},
|
||||
{"PSuTooLargeErrorWrapL", PSuTooLargeErrorWrapL, uint32(code.PSuTooLarge), 0, "test msg", "cause err", true},
|
||||
{"SvcInternalErrorWrapL", SvcInternalErrorWrapL, uint32(code.SvcInternal), 0, "test msg", "cause err", true},
|
||||
{"SvcThirdPartyErrorWrapL", SvcThirdPartyErrorWrapL, uint32(code.SvcThirdParty), 0, "test msg", "cause err", true},
|
||||
{"SvcHTTP400ErrorWrapL", SvcHTTP400ErrorWrapL, uint32(code.SvcHTTP400), 0, "test msg", "cause err", true},
|
||||
{"SvcMaintenanceErrorWrapL", SvcMaintenanceErrorWrapL, uint32(code.SvcMaintenance), 0, "test msg", "cause err", true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := &fakeLogger{}
|
||||
fields := []LogField{}
|
||||
cause := errors.New("cause err")
|
||||
e := tt.fn(l, fields, cause, "test", "msg")
|
||||
if e == nil || e.Category() != tt.wantCat || e.Detail() != tt.wantDet || e.Error() != "test msg" {
|
||||
t.Errorf("%s = cat=%d det=%d msg=%q, want %d 0 %q", tt.name, e.Category(), e.Detail(), e.Error(), tt.wantCat, "test msg")
|
||||
}
|
||||
if tt.wantUnwrap != "" && e.Unwrap().Error() != tt.wantUnwrap {
|
||||
t.Errorf("Unwrap() = %q, want %q", e.Unwrap().Error(), tt.wantUnwrap)
|
||||
}
|
||||
if tt.wantLog && len(l.calls) == 0 {
|
||||
t.Errorf("expected log call")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ... Add more tests for edge cases, like empty strings, multiple args in joinMsg, etc.
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
package errs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"backend/pkg/library/errors/code"
|
||||
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func newBuiltinGRPCErr(scope, detail uint32, msg string) *Error {
|
||||
return &Error{
|
||||
category: uint32(code.CatGRPC),
|
||||
detail: detail,
|
||||
scope: scope,
|
||||
msg: msg,
|
||||
}
|
||||
}
|
||||
|
||||
// FromError tries to let error as Err
|
||||
// it supports to unwrap error that has Error
|
||||
// return nil if failed to transfer
|
||||
func FromError(err error) *Error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var e *Error
|
||||
if errors.As(err, &e) {
|
||||
return e
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FromCode parses code as following 8 碼
|
||||
// Decimal: 10201000
|
||||
// 10 represents Scope
|
||||
// 201 represents Category
|
||||
// 000 represents Detail error code
|
||||
func FromCode(code uint32) *Error {
|
||||
const CodeMultiplier = 1000000
|
||||
const SubMultiplier = 1000
|
||||
// 獲取 scope,前兩位數
|
||||
scope := code / CodeMultiplier
|
||||
|
||||
// 獲取 detail,最後三位數
|
||||
detail := code % SubMultiplier
|
||||
|
||||
// 獲取 category,中間三位數
|
||||
category := (code / SubMultiplier) % SubMultiplier
|
||||
|
||||
return &Error{
|
||||
category: category,
|
||||
detail: detail,
|
||||
scope: scope,
|
||||
msg: "",
|
||||
}
|
||||
}
|
||||
|
||||
// FromGRPCError transfer error to Err
|
||||
// useful for gRPC client
|
||||
func FromGRPCError(err error) *Error {
|
||||
s, _ := status.FromError(err)
|
||||
e := FromCode(uint32(s.Code()))
|
||||
e.msg = s.Message()
|
||||
|
||||
// For GRPC built-in code
|
||||
if e.Scope() == uint32(code.Unset) && e.Category() == 0 && e.Code() != code.OK {
|
||||
e = newBuiltinGRPCErr(uint32(Scope), e.detail, s.Message()) // Note: detail is now 3-digit, but built-in codes are small
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue