Compare commits
16 Commits
c7b11dca35
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
292c37a177 | ||
|
|
2b2fd162a2 | ||
|
|
1aece314c6 | ||
|
|
b3ac9e8060 | ||
|
|
8eb9adda68 | ||
|
|
a33e6be08f | ||
|
|
919ef704af | ||
|
|
5cfbbffb77 | ||
|
|
1cbd043297 | ||
|
|
5da272af2e | ||
|
|
25e566450e | ||
|
|
32584e25b2 | ||
|
|
66c1ee9620 | ||
|
|
cc8dfdad80 | ||
|
|
65353ee4a2 | ||
|
|
2ae47c5049 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,6 +11,7 @@ xcuserdata/
|
|||||||
# Icon must end with two \r
|
# Icon must end with two \r
|
||||||
Icon
|
Icon
|
||||||
|
|
||||||
|
|
||||||
# Thumbnails
|
# Thumbnails
|
||||||
._*
|
._*
|
||||||
|
|
||||||
@@ -509,3 +510,4 @@ dist
|
|||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.pnp.*
|
||||||
|
|
||||||
|
trading_assistant_api/services/shared_data/
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"common/utils"
|
"common/utils"
|
||||||
|
"common/logger"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,7 +28,11 @@ func GenerateToken(userID string) (string, error) {
|
|||||||
ExpiresAt: jwt.NewNumericDate(now.Add(ttl)),
|
ExpiresAt: jwt.NewNumericDate(now.Add(ttl)),
|
||||||
}
|
}
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
return token.SignedString([]byte(secret))
|
signed, err := token.SignedString([]byte(secret))
|
||||||
|
if err == nil {
|
||||||
|
logger.L().Printf("jwt generate success user=%s exp=%s", userID, claims.ExpiresAt.Time.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
return signed, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseToken(tokenStr string) (string, error) {
|
func ParseToken(tokenStr string) (string, error) {
|
||||||
@@ -40,10 +45,13 @@ func ParseToken(tokenStr string) (string, error) {
|
|||||||
return []byte(secret), nil
|
return []byte(secret), nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.L().Printf("jwt parse error: %v", err)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if !tkn.Valid {
|
if !tkn.Valid {
|
||||||
|
logger.L().Printf("jwt invalid")
|
||||||
return "", errors.New("token_invalid")
|
return "", errors.New("token_invalid")
|
||||||
}
|
}
|
||||||
|
logger.L().Printf("jwt parse success user=%s exp=%s", claims.Subject, claims.ExpiresAt.Time.Format(time.RFC3339))
|
||||||
return claims.Subject, nil
|
return claims.Subject, nil
|
||||||
}
|
}
|
||||||
|
|||||||
10
trading_assistant_api/common/codes/values.go
Normal file
10
trading_assistant_api/common/codes/values.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package codes
|
||||||
|
|
||||||
|
const (
|
||||||
|
ValueOK = 0
|
||||||
|
ValueInvalidInput = 1001
|
||||||
|
ValueUnauthorized = 1002
|
||||||
|
ValueConflict = 1003
|
||||||
|
ValueMethodNotAllowed = 1004
|
||||||
|
ValueInternalError = 1500
|
||||||
|
)
|
||||||
42
trading_assistant_api/common/httpx/auth.go
Normal file
42
trading_assistant_api/common/httpx/auth.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package httpx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"common/auth"
|
||||||
|
"common/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type userIDKey struct{}
|
||||||
|
|
||||||
|
func AuthRequired() func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ah := r.Header.Get("Authorization")
|
||||||
|
if ah == "" || !strings.HasPrefix(ah, "Bearer ") {
|
||||||
|
logger.WithPrefix("rid="+RequestIDFromContext(r)).Printf("auth missing header path=%s", r.URL.Path)
|
||||||
|
Unauthorized(w, r, "unauthorized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token := strings.TrimSpace(strings.TrimPrefix(ah, "Bearer "))
|
||||||
|
sub, err := auth.ParseToken(token)
|
||||||
|
if err != nil || sub == "" {
|
||||||
|
logger.WithPrefix("rid="+RequestIDFromContext(r)).Printf("auth invalid token path=%s", r.URL.Path)
|
||||||
|
Unauthorized(w, r, "unauthorized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx := context.WithValue(r.Context(), userIDKey{}, sub)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UserID(r *http.Request) string {
|
||||||
|
v := r.Context().Value(userIDKey{})
|
||||||
|
if id, ok := v.(string); ok {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ func CORS() func(http.Handler) http.Handler {
|
|||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
|
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With, X-Request-ID")
|
||||||
w.Header().Set("Access-Control-Max-Age", "86400")
|
w.Header().Set("Access-Control-Max-Age", "86400")
|
||||||
if r.Method == http.MethodOptions {
|
if r.Method == http.MethodOptions {
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
|||||||
@@ -8,36 +8,36 @@ import (
|
|||||||
"common/codes"
|
"common/codes"
|
||||||
)
|
)
|
||||||
|
|
||||||
func WriteJSON(w http.ResponseWriter, status int, ok bool, msg string, data interface{}) {
|
func WriteJSON(w http.ResponseWriter, r *http.Request, status int, ok bool, code int, msg string, data interface{}) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
json.NewEncoder(w).Encode(types.Response{Status: ok, Message: msg, Data: data})
|
json.NewEncoder(w).Encode(types.Response{Status: ok, Message: msg, Code: code, RequestID: RequestIDFromContext(r), Data: data})
|
||||||
}
|
}
|
||||||
|
|
||||||
func OK(w http.ResponseWriter, data interface{}) {
|
func OK(w http.ResponseWriter, r *http.Request, data interface{}) {
|
||||||
WriteJSON(w, http.StatusOK, true, string(codes.OK), data)
|
WriteJSON(w, r, http.StatusOK, true, codes.ValueOK, string(codes.OK), data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Created(w http.ResponseWriter, data interface{}) {
|
func Created(w http.ResponseWriter, r *http.Request, data interface{}) {
|
||||||
WriteJSON(w, http.StatusCreated, true, string(codes.OK), data)
|
WriteJSON(w, r, http.StatusCreated, true, codes.ValueOK, string(codes.OK), data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func BadRequest(w http.ResponseWriter, msg string) {
|
func BadRequest(w http.ResponseWriter, r *http.Request, msg string) {
|
||||||
WriteJSON(w, http.StatusBadRequest, false, msg, map[string]string{"code": string(codes.InvalidInput)})
|
WriteJSON(w, r, http.StatusBadRequest, false, codes.ValueInvalidInput, msg, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Unauthorized(w http.ResponseWriter, msg string) {
|
func Unauthorized(w http.ResponseWriter, r *http.Request, msg string) {
|
||||||
WriteJSON(w, http.StatusUnauthorized, false, msg, map[string]string{"code": string(codes.Unauthorized)})
|
WriteJSON(w, r, http.StatusUnauthorized, false, codes.ValueUnauthorized, msg, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Conflict(w http.ResponseWriter, msg string) {
|
func Conflict(w http.ResponseWriter, r *http.Request, msg string) {
|
||||||
WriteJSON(w, http.StatusConflict, false, msg, map[string]string{"code": string(codes.Conflict)})
|
WriteJSON(w, r, http.StatusConflict, false, codes.ValueConflict, msg, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func MethodNotAllowed(w http.ResponseWriter, msg string) {
|
func MethodNotAllowed(w http.ResponseWriter, r *http.Request, msg string) {
|
||||||
WriteJSON(w, http.StatusMethodNotAllowed, false, msg, map[string]string{"code": string(codes.MethodNotAllowed)})
|
WriteJSON(w, r, http.StatusMethodNotAllowed, false, codes.ValueMethodNotAllowed, msg, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func InternalError(w http.ResponseWriter) {
|
func InternalError(w http.ResponseWriter, r *http.Request) {
|
||||||
WriteJSON(w, http.StatusInternalServerError, false, string(codes.InternalError), map[string]string{"code": string(codes.InternalError)})
|
WriteJSON(w, r, http.StatusInternalServerError, false, codes.ValueInternalError, string(codes.InternalError), nil)
|
||||||
}
|
}
|
||||||
|
|||||||
47
trading_assistant_api/common/httpx/request_id.go
Normal file
47
trading_assistant_api/common/httpx/request_id.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package httpx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type reqIDKey struct{}
|
||||||
|
|
||||||
|
func RequestID() func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
rid := incomingRID(r)
|
||||||
|
if rid == "" {
|
||||||
|
rid = genRID()
|
||||||
|
}
|
||||||
|
w.Header().Set("X-Request-ID", rid)
|
||||||
|
ctx := context.WithValue(r.Context(), reqIDKey{}, rid)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RequestIDFromContext(r *http.Request) string {
|
||||||
|
v := r.Context().Value(reqIDKey{})
|
||||||
|
if s, ok := v.(string); ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func incomingRID(r *http.Request) string {
|
||||||
|
h := r.Header.Get("X-Request-ID")
|
||||||
|
if h == "" {
|
||||||
|
h = r.Header.Get("X-Request-Id")
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func genRID() string {
|
||||||
|
var b [16]byte
|
||||||
|
_, _ = rand.Read(b[:])
|
||||||
|
return hex.EncodeToString(b[:])
|
||||||
|
}
|
||||||
@@ -3,6 +3,9 @@ package logger
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Logger interface {
|
type Logger interface {
|
||||||
@@ -30,3 +33,54 @@ func SetLogger(l Logger) {
|
|||||||
defaultLogger = l
|
defaultLogger = l
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type prefLogger struct {
|
||||||
|
prefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *prefLogger) Printf(format string, v ...any) {
|
||||||
|
defaultLogger.Printf("%s %s", p.prefix, fmt.Sprintf(format, v...))
|
||||||
|
}
|
||||||
|
func (p *prefLogger) Fatalf(format string, v ...any) {
|
||||||
|
defaultLogger.Fatalf("%s %s", p.prefix, fmt.Sprintf(format, v...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPrefix(prefix string) Logger {
|
||||||
|
return &prefLogger{prefix: prefix}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetupFile(path string) error {
|
||||||
|
if path == "" {
|
||||||
|
return fmt.Errorf("log file path empty")
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defaultLogger = &stdLogger{
|
||||||
|
l: log.New(f, "[app] ", log.LstdFlags|log.Lshortfile),
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetupTimezone(tz string) error {
|
||||||
|
if tz == "" {
|
||||||
|
return fmt.Errorf("timezone empty")
|
||||||
|
}
|
||||||
|
loc, err := time.LoadLocation(tz)
|
||||||
|
if err != nil {
|
||||||
|
// fallback for alpine without tzdata
|
||||||
|
switch tz {
|
||||||
|
case "Asia/Shanghai", "PRC", "CST":
|
||||||
|
time.Local = time.FixedZone("CST", 8*3600)
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time.Local = loc
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ package types
|
|||||||
type Response struct {
|
type Response struct {
|
||||||
Status bool `json:"status"`
|
Status bool `json:"status"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
|
Code int `json:"code"`
|
||||||
|
RequestID string `json:"request_id"`
|
||||||
Data interface{} `json:"data,omitempty"`
|
Data interface{} `json:"data,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
20
trading_assistant_api/services/country/Dockerfile
Normal file
20
trading_assistant_api/services/country/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
FROM golang:1.25.7-alpine3.23 AS builder
|
||||||
|
WORKDIR /workspace
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
|
||||||
|
# copy monorepo workspace and modules needed for country service
|
||||||
|
COPY go.work .
|
||||||
|
COPY common ./common
|
||||||
|
COPY services/country ./services/country
|
||||||
|
|
||||||
|
# download deps and build binary
|
||||||
|
RUN cd services/country && go mod download
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /workspace/bin/country-service ./services/country
|
||||||
|
|
||||||
|
FROM alpine:3.19
|
||||||
|
RUN apk add --no-cache tzdata && adduser -D -u 10002 app
|
||||||
|
ENV TZ=Asia/Shanghai
|
||||||
|
USER app
|
||||||
|
COPY --from=builder /workspace/bin/country-service /usr/local/bin/country-service
|
||||||
|
EXPOSE 8081
|
||||||
|
ENTRYPOINT ["/usr/local/bin/country-service"]
|
||||||
@@ -15,6 +15,12 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
port := utils.GetEnv("PORT", "8081")
|
port := utils.GetEnv("PORT", "8081")
|
||||||
|
if err := logger.SetupTimezone("Asia/Shanghai"); err != nil {
|
||||||
|
logger.L().Printf("setup timezone error: %v", err)
|
||||||
|
}
|
||||||
|
if err := logger.SetupFile(utils.GetEnv("LOG_FILE", "/var/log/app/country.log")); err != nil {
|
||||||
|
logger.L().Printf("setup file logger error: %v", err)
|
||||||
|
}
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Addr: ":" + port,
|
Addr: ":" + port,
|
||||||
Handler: routes(),
|
Handler: routes(),
|
||||||
|
|||||||
64
trading_assistant_api/services/docker-compose.yml
Normal file
64
trading_assistant_api/services/docker-compose.yml
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:18.1-alpine
|
||||||
|
container_name: api_postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres123123
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 20
|
||||||
|
volumes:
|
||||||
|
- ./shared_data/db/:/var/lib/postgresql/data
|
||||||
|
- ../services/user/db/schema.sql:/docker-entrypoint-initdb.d/10_user_schema.sql:ro
|
||||||
|
|
||||||
|
user:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: services/user/Dockerfile
|
||||||
|
image: user-service:latest
|
||||||
|
container_name: user_service
|
||||||
|
environment:
|
||||||
|
TZ: "Asia/Shanghai"
|
||||||
|
PG_HOST: postgres
|
||||||
|
PG_PORT: "5432"
|
||||||
|
PG_USER: postgres
|
||||||
|
PG_PASSWORD: postgres123123
|
||||||
|
PG_DBNAME: postgres
|
||||||
|
PG_SSLMODE: disable
|
||||||
|
MIGRATE_ON_START: "0"
|
||||||
|
PORT: "8080"
|
||||||
|
JWT_SECRET: "change_me_dev_secret"
|
||||||
|
JWT_TTL: "24h"
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- ./shared_data/logs/:/var/log/app/
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# country:
|
||||||
|
# build:
|
||||||
|
# context: ..
|
||||||
|
# dockerfile: services/country/Dockerfile
|
||||||
|
# image: country-service:latest
|
||||||
|
# container_name: country_service
|
||||||
|
# environment:
|
||||||
|
# PORT: "8081"
|
||||||
|
# ports:
|
||||||
|
# - "8081:8081"
|
||||||
|
# volumes:
|
||||||
|
# - ../shared_data/logs/:/var/log/app/
|
||||||
|
# restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
# syntax=docker/dockerfile:1
|
|
||||||
|
|
||||||
FROM golang:1.25.7-alpine3.23 AS builder
|
FROM golang:1.25.7-alpine3.23 AS builder
|
||||||
WORKDIR /workspace
|
WORKDIR /workspace
|
||||||
RUN apk add --no-cache git
|
RUN apk add --no-cache git
|
||||||
@@ -14,7 +12,8 @@ RUN cd services/user && go mod download
|
|||||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /workspace/bin/user-service ./services/user
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /workspace/bin/user-service ./services/user
|
||||||
|
|
||||||
FROM alpine:3.19
|
FROM alpine:3.19
|
||||||
RUN adduser -D -u 10001 app
|
RUN apk add --no-cache tzdata && adduser -D -u 10001 app
|
||||||
|
ENV TZ=Asia/Shanghai
|
||||||
USER app
|
USER app
|
||||||
COPY --from=builder /workspace/bin/user-service /usr/local/bin/user-service
|
COPY --from=builder /workspace/bin/user-service /usr/local/bin/user-service
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ CREATE TABLE IF NOT EXISTS users (
|
|||||||
update_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
update_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
COMMENT ON TABLE users IS '用户基础信息表';
|
COMMENT ON TABLE users IS '用户唯一标识信息表';
|
||||||
COMMENT ON COLUMN users.user_id IS '用户唯一标识,主键,自动生成UUIDv7';
|
COMMENT ON COLUMN users.user_id IS '用户唯一标识,主键,自动生成UUIDv7';
|
||||||
COMMENT ON COLUMN users.deleted IS '逻辑删除标识:false-未删除,true-已删除';
|
COMMENT ON COLUMN users.deleted IS '逻辑删除标识:false-未删除,true-已删除';
|
||||||
COMMENT ON COLUMN users.create_time IS '记录创建时间(带时区)';
|
COMMENT ON COLUMN users.create_time IS '记录创建时间(带时区)';
|
||||||
@@ -24,7 +24,7 @@ CREATE TABLE IF NOT EXISTS user_login_accounts (
|
|||||||
CONSTRAINT uk_user_login_accounts_value UNIQUE (value, deleted)
|
CONSTRAINT uk_user_login_accounts_value UNIQUE (value, deleted)
|
||||||
);
|
);
|
||||||
|
|
||||||
COMMENT ON TABLE user_login_accounts IS '用户登录账户表(手机号/邮箱/用户名等)';
|
COMMENT ON TABLE user_login_accounts IS '用户登录账户表';
|
||||||
COMMENT ON COLUMN user_login_accounts.follow_id IS '记录唯一标识,主键,自动生成UUIDv7';
|
COMMENT ON COLUMN user_login_accounts.follow_id IS '记录唯一标识,主键,自动生成UUIDv7';
|
||||||
COMMENT ON COLUMN user_login_accounts.user_id IS '关联用户ID,外键关联users表';
|
COMMENT ON COLUMN user_login_accounts.user_id IS '关联用户ID,外键关联users表';
|
||||||
COMMENT ON COLUMN user_login_accounts.value IS '登录账户值(手机号/邮箱/用户名)';
|
COMMENT ON COLUMN user_login_accounts.value IS '登录账户值(手机号/邮箱/用户名)';
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
set -e
|
|
||||||
ROOT_DIR="$(cd "$(dirname "$0")"/../.. && pwd)"
|
|
||||||
cd "$ROOT_DIR"
|
|
||||||
docker build -f services/user/Dockerfile -t user-service:latest .
|
|
||||||
docker save user-service:latest -o services/user/user-service.tar
|
|
||||||
docker load -i services/user/user-service.tar
|
|
||||||
cd services/user
|
|
||||||
docker compose up -d
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: postgres:18.1-alpine
|
|
||||||
container_name: user_postgres
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: app
|
|
||||||
POSTGRES_PASSWORD: app123
|
|
||||||
POSTGRES_DB: appdb
|
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 20
|
|
||||||
volumes:
|
|
||||||
- pgdata:/var/lib/postgresql/data
|
|
||||||
- ./db/schema.sql:/docker-entrypoint-initdb.d/10_user_schema.sql:ro
|
|
||||||
|
|
||||||
user:
|
|
||||||
image: user-service:latest
|
|
||||||
container_name: user_service
|
|
||||||
environment:
|
|
||||||
PG_HOST: postgres
|
|
||||||
PG_PORT: "5432"
|
|
||||||
PG_USER: app
|
|
||||||
PG_PASSWORD: app123
|
|
||||||
PG_DBNAME: appdb
|
|
||||||
PG_SSLMODE: disable
|
|
||||||
MIGRATE_ON_START: "0"
|
|
||||||
PORT: "8080"
|
|
||||||
JWT_SECRET: "change_me_dev_secret"
|
|
||||||
JWT_TTL: "24h"
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
depends_on:
|
|
||||||
postgres:
|
|
||||||
condition: service_healthy
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
pgdata:
|
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"common/httpx"
|
"common/httpx"
|
||||||
"common/codes"
|
"common/codes"
|
||||||
|
"common/logger"
|
||||||
"user/internal/model"
|
"user/internal/model"
|
||||||
"user/internal/service"
|
"user/internal/service"
|
||||||
)
|
)
|
||||||
@@ -20,74 +21,92 @@ func New(s *service.Service) *Handler {
|
|||||||
|
|
||||||
func (h *Handler) Register(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) Register(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
httpx.MethodNotAllowed(w, string(codes.MethodNotAllowed))
|
logger.WithPrefix("rid="+httpx.RequestIDFromContext(r)).Printf("method not allowed path=%s", r.URL.Path)
|
||||||
|
httpx.MethodNotAllowed(w, r, string(codes.MethodNotAllowed))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var req model.RegisterReq
|
var req model.RegisterReq
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
httpx.BadRequest(w, "invalid json")
|
logger.WithPrefix("rid="+httpx.RequestIDFromContext(r)).Printf("invalid json path=%s", r.URL.Path)
|
||||||
|
httpx.BadRequest(w, r, "invalid json")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
userID, token, err := h.S.Register(req.Account, req.Password)
|
userID, token, err := h.S.Register(req.Account, req.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
switch err {
|
||||||
case service.ErrInvalidInput:
|
case service.ErrInvalidInput:
|
||||||
httpx.BadRequest(w, "invalid account or password")
|
logger.WithPrefix("rid="+httpx.RequestIDFromContext(r)).Printf("invalid input account=%s", req.Account)
|
||||||
|
httpx.BadRequest(w, r, "invalid account or password")
|
||||||
case service.ErrAccountExists:
|
case service.ErrAccountExists:
|
||||||
httpx.Conflict(w, "account exists")
|
logger.WithPrefix("rid="+httpx.RequestIDFromContext(r)).Printf("account exists account=%s", req.Account)
|
||||||
|
httpx.Conflict(w, r, "account exists")
|
||||||
default:
|
default:
|
||||||
httpx.InternalError(w)
|
logger.WithPrefix("rid="+httpx.RequestIDFromContext(r)).Printf("internal error account=%s", req.Account)
|
||||||
|
httpx.InternalError(w, r)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
httpx.Created(w, map[string]string{"user_id": userID, "token": token})
|
logger.WithPrefix("rid="+httpx.RequestIDFromContext(r)).Printf("register success user=%s", userID)
|
||||||
|
httpx.Created(w, r, map[string]string{"user_id": userID, "token": token})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
httpx.MethodNotAllowed(w, string(codes.MethodNotAllowed))
|
logger.WithPrefix("rid="+httpx.RequestIDFromContext(r)).Printf("method not allowed path=%s", r.URL.Path)
|
||||||
|
httpx.MethodNotAllowed(w, r, string(codes.MethodNotAllowed))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var req model.LoginReq
|
var req model.LoginReq
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
httpx.BadRequest(w, "invalid json")
|
logger.WithPrefix("rid="+httpx.RequestIDFromContext(r)).Printf("invalid json path=%s", r.URL.Path)
|
||||||
|
httpx.BadRequest(w, r, "invalid json")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
userID, token, err := h.S.Login(req.Account, req.Password)
|
userID, token, err := h.S.Login(req.Account, req.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
switch err {
|
||||||
case service.ErrInvalidInput:
|
case service.ErrInvalidInput:
|
||||||
httpx.BadRequest(w, "invalid account or password")
|
logger.WithPrefix("rid="+httpx.RequestIDFromContext(r)).Printf("invalid input account=%s", req.Account)
|
||||||
|
httpx.BadRequest(w, r, "invalid account or password")
|
||||||
case service.ErrUnauthorized:
|
case service.ErrUnauthorized:
|
||||||
httpx.Unauthorized(w, "unauthorized")
|
logger.WithPrefix("rid="+httpx.RequestIDFromContext(r)).Printf("unauthorized account=%s", req.Account)
|
||||||
|
httpx.Unauthorized(w, r, "unauthorized")
|
||||||
default:
|
default:
|
||||||
httpx.InternalError(w)
|
logger.WithPrefix("rid="+httpx.RequestIDFromContext(r)).Printf("internal error account=%s", req.Account)
|
||||||
|
httpx.InternalError(w, r)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
httpx.OK(w, map[string]string{"user_id": userID, "token": token})
|
logger.WithPrefix("rid="+httpx.RequestIDFromContext(r)).Printf("login success user=%s", userID)
|
||||||
|
httpx.OK(w, r, map[string]string{"user_id": userID, "token": token})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) Healthz(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) Healthz(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
httpx.MethodNotAllowed(w, string(codes.MethodNotAllowed))
|
logger.WithPrefix("rid="+httpx.RequestIDFromContext(r)).Printf("method not allowed path=%s", r.URL.Path)
|
||||||
|
httpx.MethodNotAllowed(w, r, string(codes.MethodNotAllowed))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
httpx.OK(w, nil)
|
logger.WithPrefix("rid="+httpx.RequestIDFromContext(r)).Printf("healthz ok")
|
||||||
|
httpx.OK(w, r, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) Version(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) Version(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
httpx.MethodNotAllowed(w, string(codes.MethodNotAllowed))
|
logger.WithPrefix("rid="+httpx.RequestIDFromContext(r)).Printf("method not allowed path=%s", r.URL.Path)
|
||||||
|
httpx.MethodNotAllowed(w, r, string(codes.MethodNotAllowed))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
httpx.OK(w, map[string]string{"version": "user-service v0.1.0"})
|
logger.WithPrefix("rid="+httpx.RequestIDFromContext(r)).Printf("version ok")
|
||||||
|
httpx.OK(w, r, map[string]string{"version": "user-service v0.1.0"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) Root(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) Root(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
httpx.MethodNotAllowed(w, string(codes.MethodNotAllowed))
|
logger.WithPrefix("rid="+httpx.RequestIDFromContext(r)).Printf("method not allowed path=%s", r.URL.Path)
|
||||||
|
httpx.MethodNotAllowed(w, r, string(codes.MethodNotAllowed))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
httpx.OK(w, map[string]string{"service": "user"})
|
logger.WithPrefix("rid="+httpx.RequestIDFromContext(r)).Printf("root ok user=%s", httpx.UserID(r))
|
||||||
|
httpx.OK(w, r, map[string]string{"service": "user", "user_id": httpx.UserID(r)})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package router
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"common/httpx"
|
||||||
"user/internal/handler"
|
"user/internal/handler"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ func New(h *handler.Handler) http.Handler {
|
|||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/healthz", h.Healthz)
|
mux.HandleFunc("/healthz", h.Healthz)
|
||||||
mux.HandleFunc("/version", h.Version)
|
mux.HandleFunc("/version", h.Version)
|
||||||
mux.HandleFunc("/", h.Root)
|
mux.Handle("/", httpx.AuthRequired()(http.HandlerFunc(h.Root)))
|
||||||
mux.HandleFunc("/register", h.Register)
|
mux.HandleFunc("/register", h.Register)
|
||||||
mux.HandleFunc("/login", h.Login)
|
mux.HandleFunc("/login", h.Login)
|
||||||
return mux
|
return mux
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"errors"
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"common/auth"
|
"common/auth"
|
||||||
"github.com/jackc/pgconn"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"user/internal/repository"
|
"user/internal/repository"
|
||||||
)
|
)
|
||||||
@@ -93,9 +92,9 @@ func validPassword(p string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isUniqueViolation(err error) bool {
|
func isUniqueViolation(err error) bool {
|
||||||
var pe *pgconn.PgError
|
if err == nil {
|
||||||
if errors.As(err, &pe) {
|
|
||||||
return pe.Code == "23505"
|
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
s := err.Error()
|
||||||
|
return strings.Contains(s, "duplicate key") || strings.Contains(s, "unique constraint") || strings.Contains(s, "23505")
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ var pg *sql.DB
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
port := utils.GetEnv("PORT", "8080")
|
port := utils.GetEnv("PORT", "8080")
|
||||||
srv := &http.Server{
|
if err := logger.SetupTimezone("Asia/Shanghai"); err != nil {
|
||||||
Addr: ":" + port,
|
logger.L().Printf("setup timezone error: %v", err)
|
||||||
Handler: routes(),
|
}
|
||||||
|
if err := logger.SetupFile(utils.GetEnv("LOG_FILE", "/var/log/app/user.log")); err != nil {
|
||||||
|
logger.L().Printf("setup file logger error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
pg, err = db.InitPostgres()
|
pg, err = db.InitPostgres()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -39,6 +40,11 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: ":" + port,
|
||||||
|
Handler: routes(),
|
||||||
|
}
|
||||||
|
|
||||||
logger.L().Printf("user service starting on :%s", port)
|
logger.L().Printf("user service starting on :%s", port)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -66,7 +72,8 @@ func routes() http.Handler {
|
|||||||
svc := service.New(repo)
|
svc := service.New(repo)
|
||||||
h := handler.New(svc)
|
h := handler.New(svc)
|
||||||
cors := httpx.CORS()
|
cors := httpx.CORS()
|
||||||
return cors(router.New(h))
|
reqID := httpx.RequestID()
|
||||||
|
return reqID(cors(router.New(h)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func applySchema(path string) error {
|
func applySchema(path string) error {
|
||||||
|
|||||||
Reference in New Issue
Block a user