模块化拆分
This commit is contained in:
parent
82806c73a6
commit
b449737bd0
10
Makefile
10
Makefile
@ -4,7 +4,7 @@ SERVICE_TOKEN := $(firstword $(filter $(SERVICE_ALIASES),$(MAKECMDGOALS)))
|
|||||||
SERVICE_ID := $(strip $(or $(SERVICE),$(if $(filter gs gateway,$(SERVICE_TOKEN)),gateway-service,$(if $(filter rs room,$(SERVICE_TOKEN)),room-service,$(if $(filter ws wallet,$(SERVICE_TOKEN)),wallet-service,$(if $(filter us user,$(SERVICE_TOKEN)),user-service,$(if $(filter as activity,$(SERVICE_TOKEN)),activity-service,$(if $(filter cs cron,$(SERVICE_TOKEN)),cron-service,$(SERVICE_TOKEN)))))))))
|
SERVICE_ID := $(strip $(or $(SERVICE),$(if $(filter gs gateway,$(SERVICE_TOKEN)),gateway-service,$(if $(filter rs room,$(SERVICE_TOKEN)),room-service,$(if $(filter ws wallet,$(SERVICE_TOKEN)),wallet-service,$(if $(filter us user,$(SERVICE_TOKEN)),user-service,$(if $(filter as activity,$(SERVICE_TOKEN)),activity-service,$(if $(filter cs cron,$(SERVICE_TOKEN)),cron-service,$(SERVICE_TOKEN)))))))))
|
||||||
ADMIN_CONFIG ?= configs/config.yaml
|
ADMIN_CONFIG ?= configs/config.yaml
|
||||||
|
|
||||||
.PHONY: proto test build admin admin-up admin-deps admin-bootstrap admin-test admin-build up db-init rebuild restart stop logs ps addr down up-service check-service resolve-compose-conflicts $(SERVICE_ALIASES)
|
.PHONY: proto vet test build admin admin-up admin-deps admin-bootstrap admin-test admin-build up db-init rebuild restart stop logs ps addr down up-service check-service resolve-compose-conflicts $(SERVICE_ALIASES)
|
||||||
|
|
||||||
# `proto` 负责把独立 api module 内的 .proto 重新生成成 Go 代码。
|
# `proto` 负责把独立 api module 内的 .proto 重新生成成 Go 代码。
|
||||||
proto:
|
proto:
|
||||||
@ -22,8 +22,14 @@ proto:
|
|||||||
proto/wallet/v1/wallet.proto \
|
proto/wallet/v1/wallet.proto \
|
||||||
proto/events/room/v1/events.proto
|
proto/events/room/v1/events.proto
|
||||||
|
|
||||||
|
# `vet` 使用当前 Go 工具链检查 workspace 内所有后端模块,升级 Go 版本时先暴露静态风险。
|
||||||
|
vet:
|
||||||
|
go vet ./...
|
||||||
|
go vet ./api/...
|
||||||
|
go vet ./server/admin/...
|
||||||
|
|
||||||
# `test` 统一跑 workspace 内的 app 后端、api 契约和后台管理服务测试。
|
# `test` 统一跑 workspace 内的 app 后端、api 契约和后台管理服务测试。
|
||||||
test:
|
test: vet
|
||||||
go test ./...
|
go test ./...
|
||||||
go test ./api/...
|
go test ./api/...
|
||||||
go test ./server/admin/...
|
go test ./server/admin/...
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
module hyapp.local/api
|
module hyapp.local/api
|
||||||
|
|
||||||
go 1.23.1
|
go 1.26.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
google.golang.org/grpc v1.68.0
|
google.golang.org/grpc v1.68.0
|
||||||
|
|||||||
2
go.mod
2
go.mod
@ -1,6 +1,6 @@
|
|||||||
module hyapp
|
module hyapp
|
||||||
|
|
||||||
go 1.23.1
|
go 1.26.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-sql-driver/mysql v1.9.3
|
github.com/go-sql-driver/mysql v1.9.3
|
||||||
|
|||||||
@ -129,7 +129,7 @@ func initSchema(t testing.TB, db *sql.DB, initDBPath string) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("read initdb %s failed: %v", initDBPath, err)
|
t.Fatalf("read initdb %s failed: %v", initDBPath, err)
|
||||||
}
|
}
|
||||||
for _, statement := range strings.Split(stripSQLLineComments(string(script)), ";") {
|
for statement := range strings.SplitSeq(stripSQLLineComments(string(script)), ";") {
|
||||||
statement = strings.TrimSpace(statement)
|
statement = strings.TrimSpace(statement)
|
||||||
if statement == "" {
|
if statement == "" {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@ -84,8 +84,8 @@ func Parse(groupID string) ParsedGroup {
|
|||||||
return ParsedGroup{Kind: KindUnknown}
|
return ParsedGroup{Kind: KindUnknown}
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(groupID, "hy_") {
|
if after, ok := strings.CutPrefix(groupID, "hy_"); ok {
|
||||||
body := strings.TrimPrefix(groupID, "hy_")
|
body := after
|
||||||
tokenIndex := strings.LastIndex(body, regionToken)
|
tokenIndex := strings.LastIndex(body, regionToken)
|
||||||
if tokenIndex >= 0 {
|
if tokenIndex >= 0 {
|
||||||
app := body[:tokenIndex]
|
app := body[:tokenIndex]
|
||||||
|
|||||||
@ -584,8 +584,8 @@ func clientIP(request *http.Request) string {
|
|||||||
if value == "" {
|
if value == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if comma := strings.IndexByte(value, ','); comma >= 0 {
|
if before, _, ok := strings.Cut(value, ","); ok {
|
||||||
return strings.TrimSpace(value[:comma])
|
return strings.TrimSpace(before)
|
||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
module hyapp-admin-server
|
module hyapp-admin-server
|
||||||
|
|
||||||
go 1.23.1
|
go 1.26.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.10.0
|
||||||
|
|||||||
@ -95,8 +95,8 @@ func (r *Runner) Close() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Runner) Status() map[string]interface{} {
|
func (r *Runner) Status() map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"artifactDir": r.artifactDir,
|
"artifactDir": r.artifactDir,
|
||||||
"interval": r.interval.String(),
|
"interval": r.interval.String(),
|
||||||
"leaseTTL": r.leaseTTL.String(),
|
"leaseTTL": r.leaseTTL.String(),
|
||||||
@ -175,7 +175,7 @@ func (r *Runner) exportAdminUsers(_ context.Context, job *model.AdminJob) error
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
result, err := json.Marshal(map[string]interface{}{
|
result, err := json.Marshal(map[string]any{
|
||||||
"count": len(users),
|
"count": len(users),
|
||||||
"downloadPath": fmt.Sprintf("/api/v1/jobs/%d/artifact", job.ID),
|
"downloadPath": fmt.Sprintf("/api/v1/jobs/%d/artifact", job.ID),
|
||||||
})
|
})
|
||||||
|
|||||||
@ -85,8 +85,8 @@ func shouldAudit(method string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func auditResource(path string) string {
|
func auditResource(path string) string {
|
||||||
parts := strings.Split(strings.Trim(path, "/"), "/")
|
parts := strings.SplitSeq(strings.Trim(path, "/"), "/")
|
||||||
for _, part := range parts {
|
for part := range parts {
|
||||||
if part != "" && !strings.HasPrefix(part, ":") && part != "api" && part != "v1" {
|
if part != "" && !strings.HasPrefix(part, ":") && part != "api" && part != "v1" {
|
||||||
return part
|
return part
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"hyapp-admin-server/internal/appctx"
|
"hyapp-admin-server/internal/appctx"
|
||||||
@ -116,12 +117,7 @@ func HasAnyPermission(c *gin.Context, codes ...string) bool {
|
|||||||
// HasPermission reads only the JWT-derived permission list. Database state is
|
// HasPermission reads only the JWT-derived permission list. Database state is
|
||||||
// refreshed by login/refresh, so every request uses one consistent permission snapshot.
|
// refreshed by login/refresh, so every request uses one consistent permission snapshot.
|
||||||
func HasPermission(c *gin.Context, code string) bool {
|
func HasPermission(c *gin.Context, code string) bool {
|
||||||
for _, permission := range CurrentPermissions(c) {
|
return slices.Contains(CurrentPermissions(c), code)
|
||||||
if permission == code {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func CurrentUserID(c *gin.Context) uint {
|
func CurrentUserID(c *gin.Context) uint {
|
||||||
|
|||||||
@ -103,7 +103,7 @@ func (s *AdminUserService) GetUser(id uint) (*model.User, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *AdminUserService) UpdateUser(id uint, input UpdateUserInput) (*model.User, error) {
|
func (s *AdminUserService) UpdateUser(id uint, input UpdateUserInput) (*model.User, error) {
|
||||||
updates := map[string]interface{}{}
|
updates := map[string]any{}
|
||||||
if input.Name != nil {
|
if input.Name != nil {
|
||||||
updates["name"] = strings.TrimSpace(*input.Name)
|
updates["name"] = strings.TrimSpace(*input.Name)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -517,12 +517,12 @@ func offset(page int, pageSize int) int {
|
|||||||
return (page - 1) * pageSize
|
return (page - 1) * pageSize
|
||||||
}
|
}
|
||||||
|
|
||||||
func nullableString(value string) sql.NullString {
|
func nullableString(value string) sql.Null[string] {
|
||||||
return sql.NullString{String: value, Valid: value != ""}
|
return sql.Null[string]{V: value, Valid: value != ""}
|
||||||
}
|
}
|
||||||
|
|
||||||
func nullableInt64(value int64) sql.NullInt64 {
|
func nullableInt64(value int64) sql.Null[int64] {
|
||||||
return sql.NullInt64{Int64: value, Valid: value > 0}
|
return sql.Null[int64]{V: value, Valid: value > 0}
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeCountryCode(value string) string {
|
func normalizeCountryCode(value string) string {
|
||||||
|
|||||||
@ -12,7 +12,7 @@ type RedisHealth interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type JobStatusProvider interface {
|
type JobStatusProvider interface {
|
||||||
Status() map[string]interface{}
|
Status() map[string]any
|
||||||
}
|
}
|
||||||
|
|
||||||
type HealthService struct {
|
type HealthService struct {
|
||||||
@ -26,16 +26,16 @@ func NewService(store *repository.Store, redis RedisHealth, jobs JobStatusProvid
|
|||||||
return &HealthService{store: store, redis: redis, jobs: jobs, timeout: timeout}
|
return &HealthService{store: store, redis: redis, jobs: jobs, timeout: timeout}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *HealthService) Health() map[string]interface{} {
|
func (s *HealthService) Health() map[string]any {
|
||||||
return map[string]interface{}{"status": "ok"}
|
return map[string]any{"status": "ok"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *HealthService) Ready(ctx context.Context) (map[string]interface{}, error) {
|
func (s *HealthService) Ready(ctx context.Context) (map[string]any, error) {
|
||||||
ctx, cancel := context.WithTimeout(ctx, s.timeout)
|
ctx, cancel := context.WithTimeout(ctx, s.timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
status := map[string]interface{}{"status": "ready", "dependencies": map[string]interface{}{}}
|
status := map[string]any{"status": "ready", "dependencies": map[string]any{}}
|
||||||
deps := status["dependencies"].(map[string]interface{})
|
deps := status["dependencies"].(map[string]any)
|
||||||
if err := s.store.Ping(); err != nil {
|
if err := s.store.Ping(); err != nil {
|
||||||
deps["mysql"] = "down"
|
deps["mysql"] = "down"
|
||||||
return status, err
|
return status, err
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"hyapp-admin-server/internal/appctx"
|
"hyapp-admin-server/internal/appctx"
|
||||||
@ -104,9 +104,7 @@ func normalizeRoomConfig(config RoomConfig) (RoomConfig, error) {
|
|||||||
if len(allowed) == 0 {
|
if len(allowed) == 0 {
|
||||||
return RoomConfig{}, fmt.Errorf("%w: 至少启用一个座位数", ErrInvalidArgument)
|
return RoomConfig{}, fmt.Errorf("%w: 至少启用一个座位数", ErrInvalidArgument)
|
||||||
}
|
}
|
||||||
sort.Slice(allowed, func(i int, j int) bool {
|
slices.Sort(allowed)
|
||||||
return allowed[i] < allowed[j]
|
|
||||||
})
|
|
||||||
if !seen[config.DefaultSeatCount] {
|
if !seen[config.DefaultSeatCount] {
|
||||||
return RoomConfig{}, fmt.Errorf("%w: 默认座位数必须在已启用座位数中", ErrInvalidArgument)
|
return RoomConfig{}, fmt.Errorf("%w: 默认座位数必须在已启用座位数中", ErrInvalidArgument)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,15 +7,15 @@ import (
|
|||||||
"hyapp-admin-server/internal/model"
|
"hyapp-admin-server/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func UserDTOs(users []model.User) []map[string]interface{} {
|
func UserDTOs(users []model.User) []map[string]any {
|
||||||
out := make([]map[string]interface{}, 0, len(users))
|
out := make([]map[string]any, 0, len(users))
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
out = append(out, UserDTO(user))
|
out = append(out, UserDTO(user))
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func UserDTO(user model.User) map[string]interface{} {
|
func UserDTO(user model.User) map[string]any {
|
||||||
roleNames := make([]string, 0, len(user.Roles))
|
roleNames := make([]string, 0, len(user.Roles))
|
||||||
roleIDs := make([]uint, 0, len(user.Roles))
|
roleIDs := make([]uint, 0, len(user.Roles))
|
||||||
for _, role := range user.Roles {
|
for _, role := range user.Roles {
|
||||||
@ -32,7 +32,7 @@ func UserDTO(user model.User) map[string]interface{} {
|
|||||||
lastLogin = time.UnixMilli(*user.LastLoginAtMS).UTC().Format("01-02 15:04")
|
lastLogin = time.UnixMilli(*user.LastLoginAtMS).UTC().Format("01-02 15:04")
|
||||||
}
|
}
|
||||||
|
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"id": user.ID,
|
"id": user.ID,
|
||||||
"account": user.Username,
|
"account": user.Username,
|
||||||
"username": user.Username,
|
"username": user.Username,
|
||||||
|
|||||||
@ -157,7 +157,7 @@ func (s *Store) LeaseNextJob(workerID string, lease time.Duration, maxAttempts i
|
|||||||
if job.MaxAttempts <= 0 {
|
if job.MaxAttempts <= 0 {
|
||||||
job.MaxAttempts = maxAttempts
|
job.MaxAttempts = maxAttempts
|
||||||
}
|
}
|
||||||
updates := map[string]interface{}{
|
updates := map[string]any{
|
||||||
"status": model.JobStatusRunning,
|
"status": model.JobStatusRunning,
|
||||||
"attempt": job.Attempt + 1,
|
"attempt": job.Attempt + 1,
|
||||||
"max_attempts": job.MaxAttempts,
|
"max_attempts": job.MaxAttempts,
|
||||||
@ -185,7 +185,7 @@ func (s *Store) LeaseNextJob(workerID string, lease time.Duration, maxAttempts i
|
|||||||
|
|
||||||
func (s *Store) CompleteJobWithArtifact(id uint, resultJSON string, artifactPath string) error {
|
func (s *Store) CompleteJobWithArtifact(id uint, resultJSON string, artifactPath string) error {
|
||||||
nowMS := time.Now().UTC().UnixMilli()
|
nowMS := time.Now().UTC().UnixMilli()
|
||||||
return s.db.Model(&model.AdminJob{}).Where("id = ?", id).Updates(map[string]interface{}{
|
return s.db.Model(&model.AdminJob{}).Where("id = ?", id).Updates(map[string]any{
|
||||||
"status": model.JobStatusSucceeded,
|
"status": model.JobStatusSucceeded,
|
||||||
"result_json": resultJSON,
|
"result_json": resultJSON,
|
||||||
"artifact_path": artifactPath,
|
"artifact_path": artifactPath,
|
||||||
@ -202,12 +202,12 @@ func (s *Store) FailJobAttempt(id uint, message string) error {
|
|||||||
}
|
}
|
||||||
nowMS := time.Now().UTC().UnixMilli()
|
nowMS := time.Now().UTC().UnixMilli()
|
||||||
status := model.JobStatusPending
|
status := model.JobStatusPending
|
||||||
finishedAt := interface{}(nil)
|
finishedAt := any(nil)
|
||||||
if job.Attempt >= job.MaxAttempts {
|
if job.Attempt >= job.MaxAttempts {
|
||||||
status = model.JobStatusFailed
|
status = model.JobStatusFailed
|
||||||
finishedAt = &nowMS
|
finishedAt = &nowMS
|
||||||
}
|
}
|
||||||
return s.db.Model(&model.AdminJob{}).Where("id = ?", id).Updates(map[string]interface{}{
|
return s.db.Model(&model.AdminJob{}).Where("id = ?", id).Updates(map[string]any{
|
||||||
"status": status,
|
"status": status,
|
||||||
"error": message,
|
"error": message,
|
||||||
"locked_by": "",
|
"locked_by": "",
|
||||||
@ -219,7 +219,7 @@ func (s *Store) FailJobAttempt(id uint, message string) error {
|
|||||||
func (s *Store) ReleaseJobLease(id uint) error {
|
func (s *Store) ReleaseJobLease(id uint) error {
|
||||||
return s.db.Model(&model.AdminJob{}).
|
return s.db.Model(&model.AdminJob{}).
|
||||||
Where("id = ? AND status = ?", id, model.JobStatusRunning).
|
Where("id = ? AND status = ?", id, model.JobStatusRunning).
|
||||||
Updates(map[string]interface{}{
|
Updates(map[string]any{
|
||||||
"status": model.JobStatusPending,
|
"status": model.JobStatusPending,
|
||||||
"locked_by": "",
|
"locked_by": "",
|
||||||
"locked_until_ms": nil,
|
"locked_until_ms": nil,
|
||||||
@ -230,7 +230,7 @@ func (s *Store) CancelJob(id uint) error {
|
|||||||
nowMS := time.Now().UTC().UnixMilli()
|
nowMS := time.Now().UTC().UnixMilli()
|
||||||
result := s.db.Model(&model.AdminJob{}).
|
result := s.db.Model(&model.AdminJob{}).
|
||||||
Where("id = ? AND status IN ?", id, []string{model.JobStatusPending, model.JobStatusRunning}).
|
Where("id = ? AND status IN ?", id, []string{model.JobStatusPending, model.JobStatusRunning}).
|
||||||
Updates(map[string]interface{}{
|
Updates(map[string]any{
|
||||||
"status": model.JobStatusCanceled,
|
"status": model.JobStatusCanceled,
|
||||||
"locked_by": "",
|
"locked_by": "",
|
||||||
"locked_until_ms": nil,
|
"locked_until_ms": nil,
|
||||||
|
|||||||
@ -51,7 +51,7 @@ func (s *Store) CreateUser(user *model.User, roleIDs []uint) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) UpdateUser(userID uint, updates map[string]interface{}, roleIDs *[]uint) (*model.User, error) {
|
func (s *Store) UpdateUser(userID uint, updates map[string]any, roleIDs *[]uint) (*model.User, error) {
|
||||||
var user model.User
|
var user model.User
|
||||||
if err := s.db.First(&user, userID).Error; err != nil {
|
if err := s.db.First(&user, userID).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -81,7 +81,7 @@ func (s *Store) UpdateUserStatus(userID uint, status string) (*model.User, error
|
|||||||
if !validUserStatus(status) {
|
if !validUserStatus(status) {
|
||||||
return nil, errors.New("invalid user status")
|
return nil, errors.New("invalid user status")
|
||||||
}
|
}
|
||||||
return s.UpdateUser(userID, map[string]interface{}{"status": status}, nil)
|
return s.UpdateUser(userID, map[string]any{"status": status}, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) BatchUpdateUserStatus(ids []uint, status string) error {
|
func (s *Store) BatchUpdateUserStatus(ids []uint, status string) error {
|
||||||
|
|||||||
@ -43,7 +43,7 @@ func PermissionsForUser(user model.User) []string {
|
|||||||
|
|
||||||
func (s *Store) TouchUserLogin(id uint) error {
|
func (s *Store) TouchUserLogin(id uint) error {
|
||||||
nowMS := time.Now().UTC().UnixMilli()
|
nowMS := time.Now().UTC().UnixMilli()
|
||||||
return s.db.Model(&model.User{}).Where("id = ?", id).Updates(map[string]interface{}{
|
return s.db.Model(&model.User{}).Where("id = ?", id).Updates(map[string]any{
|
||||||
"last_login_at_ms": &nowMS,
|
"last_login_at_ms": &nowMS,
|
||||||
"updated_at_ms": nowMS,
|
"updated_at_ms": nowMS,
|
||||||
}).Error
|
}).Error
|
||||||
|
|||||||
@ -55,7 +55,7 @@ func applyUserDataAccess(query *gorm.DB, access DataAccess) *gorm.DB {
|
|||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
conditions := make([]string, 0, 2)
|
conditions := make([]string, 0, 2)
|
||||||
args := make([]interface{}, 0, 2)
|
args := make([]any, 0, 2)
|
||||||
if len(access.Teams) > 0 {
|
if len(access.Teams) > 0 {
|
||||||
conditions = append(conditions, "admin_users.team IN ?")
|
conditions = append(conditions, "admin_users.team IN ?")
|
||||||
args = append(args, access.Teams)
|
args = append(args, access.Teams)
|
||||||
|
|||||||
@ -51,7 +51,7 @@ func (s *Store) UpdateMenu(id uint, patch model.Menu) (*model.Menu, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 菜单字段直接影响前端路由和权限入口,更新时保持 code/path/permission_code 同步落库。
|
// 菜单字段直接影响前端路由和权限入口,更新时保持 code/path/permission_code 同步落库。
|
||||||
updates := map[string]interface{}{
|
updates := map[string]any{
|
||||||
"parent_id": normalized.ParentID,
|
"parent_id": normalized.ParentID,
|
||||||
"title": normalized.Title,
|
"title": normalized.Title,
|
||||||
"code": normalized.Code,
|
"code": normalized.Code,
|
||||||
|
|||||||
@ -35,7 +35,7 @@ func (s *Store) UpdatePermission(id uint, patch model.Permission) (*model.Permis
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 权限码会写入 JWT 和前端按钮判断,更新时必须整体校验后一次性落库。
|
// 权限码会写入 JWT 和前端按钮判断,更新时必须整体校验后一次性落库。
|
||||||
updates := map[string]interface{}{
|
updates := map[string]any{
|
||||||
"name": normalized.Name,
|
"name": normalized.Name,
|
||||||
"code": normalized.Code,
|
"code": normalized.Code,
|
||||||
"kind": normalized.Kind,
|
"kind": normalized.Kind,
|
||||||
@ -136,7 +136,7 @@ func (s *Store) UpdateRole(roleID uint, patch model.Role, permissionIDs *[]uint)
|
|||||||
}
|
}
|
||||||
|
|
||||||
err := s.db.Transaction(func(tx *gorm.DB) error {
|
err := s.db.Transaction(func(tx *gorm.DB) error {
|
||||||
updates := map[string]interface{}{
|
updates := map[string]any{
|
||||||
"name": patch.Name,
|
"name": patch.Name,
|
||||||
"code": patch.Code,
|
"code": patch.Code,
|
||||||
"description": patch.Description,
|
"description": patch.Description,
|
||||||
|
|||||||
@ -319,7 +319,7 @@ func (s *Store) syncDefaultPermissions(tx *gorm.DB) error {
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := tx.Model(&permission).Updates(map[string]interface{}{
|
if err := tx.Model(&permission).Updates(map[string]any{
|
||||||
"name": definition.Name,
|
"name": definition.Name,
|
||||||
"kind": definition.Kind,
|
"kind": definition.Kind,
|
||||||
"description": definition.Description,
|
"description": definition.Description,
|
||||||
@ -343,7 +343,7 @@ func (s *Store) syncDefaultMenu(tx *gorm.DB, definition model.Menu) (model.Menu,
|
|||||||
}
|
}
|
||||||
return definition, nil
|
return definition, nil
|
||||||
}
|
}
|
||||||
if err := tx.Model(&menu).Updates(map[string]interface{}{
|
if err := tx.Model(&menu).Updates(map[string]any{
|
||||||
"parent_id": definition.ParentID,
|
"parent_id": definition.ParentID,
|
||||||
"title": definition.Title,
|
"title": definition.Title,
|
||||||
"path": definition.Path,
|
"path": definition.Path,
|
||||||
|
|||||||
@ -3,13 +3,13 @@ package response
|
|||||||
import "github.com/gin-gonic/gin"
|
import "github.com/gin-gonic/gin"
|
||||||
|
|
||||||
type Body struct {
|
type Body struct {
|
||||||
Code int `json:"code"`
|
Code int `json:"code"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Data interface{} `json:"data"`
|
Data any `json:"data"`
|
||||||
RequestID string `json:"requestId,omitempty"`
|
RequestID string `json:"requestId,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func body(c *gin.Context, code int, message string, data interface{}) Body {
|
func body(c *gin.Context, code int, message string, data any) Body {
|
||||||
requestID, _ := c.Get("requestID")
|
requestID, _ := c.Get("requestID")
|
||||||
value, _ := requestID.(string)
|
value, _ := requestID.(string)
|
||||||
return Body{Code: code, Message: message, Data: data, RequestID: value}
|
return Body{Code: code, Message: message, Data: data, RequestID: value}
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
package response
|
package response
|
||||||
|
|
||||||
type Page struct {
|
type Page struct {
|
||||||
Items interface{} `json:"items"`
|
Items any `json:"items"`
|
||||||
Page int `json:"page"`
|
Page int `json:"page"`
|
||||||
PageSize int `json:"pageSize"`
|
PageSize int `json:"pageSize"`
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,10 +6,10 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func OK(c *gin.Context, data interface{}) {
|
func OK(c *gin.Context, data any) {
|
||||||
c.JSON(http.StatusOK, body(c, CodeOK, "ok", data))
|
c.JSON(http.StatusOK, body(c, CodeOK, "ok", data))
|
||||||
}
|
}
|
||||||
|
|
||||||
func Created(c *gin.Context, data interface{}) {
|
func Created(c *gin.Context, data any) {
|
||||||
c.JSON(http.StatusCreated, body(c, CodeOK, "ok", data))
|
c.JSON(http.StatusCreated, body(c, CodeOK, "ok", data))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,7 +57,7 @@ func (s *AuthService) GenerateAccessToken(userID uint, username string, permissi
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthService) ParseAccessToken(tokenString string) (*Claims, error) {
|
func (s *AuthService) ParseAccessToken(tokenString string) (*Claims, error) {
|
||||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (any, error) {
|
||||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
return nil, errors.New("unexpected jwt signing method")
|
return nil, errors.New("unexpected jwt signing method")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.23-alpine AS builder
|
FROM golang:1.26.3-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
|
|||||||
@ -133,11 +133,9 @@ func New(cfg config.Config) (*App, error) {
|
|||||||
func (a *App) Run() error {
|
func (a *App) Run() error {
|
||||||
a.health.MarkServing()
|
a.health.MarkServing()
|
||||||
if a.broadcastWorkerEnabled && a.broadcast != nil && a.workerCtx != nil {
|
if a.broadcastWorkerEnabled && a.broadcast != nil && a.workerCtx != nil {
|
||||||
a.workerWG.Add(1)
|
a.workerWG.Go(func() {
|
||||||
go func() {
|
|
||||||
defer a.workerWG.Done()
|
|
||||||
a.broadcast.RunWorker(a.workerCtx, broadcastservice.WorkerOptions{})
|
a.broadcast.RunWorker(a.workerCtx, broadcastservice.WorkerOptions{})
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
err := a.server.Serve(a.listener)
|
err := a.server.Serve(a.listener)
|
||||||
if a.workerStop != nil {
|
if a.workerStop != nil {
|
||||||
|
|||||||
@ -21,7 +21,7 @@ func TestBroadcastServiceUsesRealMySQLOutbox(t *testing.T) {
|
|||||||
service.SetClock(func() time.Time { return time.UnixMilli(1_700_000_123_456) })
|
service.SetClock(func() time.Time { return time.UnixMilli(1_700_000_123_456) })
|
||||||
ctx := appcode.WithContext(context.Background(), "lalu")
|
ctx := appcode.WithContext(context.Background(), "lalu")
|
||||||
|
|
||||||
envelope := mustGiftEnvelope(t, roomeventsv1.RoomGiftSent{
|
envelope := mustGiftEnvelope(t, &roomeventsv1.RoomGiftSent{
|
||||||
SenderUserId: 42,
|
SenderUserId: 42,
|
||||||
TargetUserId: 43,
|
TargetUserId: 43,
|
||||||
GiftId: "super_rocket",
|
GiftId: "super_rocket",
|
||||||
|
|||||||
@ -18,7 +18,7 @@ func TestHandleRoomGiftSentCreatesRegionBroadcast(t *testing.T) {
|
|||||||
repository := newFakeRepository()
|
repository := newFakeRepository()
|
||||||
service := New(Config{NodeID: "node-a", SuperGiftMinValue: 100}, repository, nil, nil)
|
service := New(Config{NodeID: "node-a", SuperGiftMinValue: 100}, repository, nil, nil)
|
||||||
service.SetClock(func() time.Time { return time.UnixMilli(1_700_000_000_000) })
|
service.SetClock(func() time.Time { return time.UnixMilli(1_700_000_000_000) })
|
||||||
envelope := mustGiftEnvelope(t, roomeventsv1.RoomGiftSent{
|
envelope := mustGiftEnvelope(t, &roomeventsv1.RoomGiftSent{
|
||||||
SenderUserId: 42,
|
SenderUserId: 42,
|
||||||
TargetUserId: 43,
|
TargetUserId: 43,
|
||||||
GiftId: "rocket",
|
GiftId: "rocket",
|
||||||
@ -58,7 +58,7 @@ func TestHandleRoomGiftSentCreatesRegionBroadcast(t *testing.T) {
|
|||||||
|
|
||||||
func TestHandleRoomGiftSentSkipsNonBroadcastCases(t *testing.T) {
|
func TestHandleRoomGiftSentSkipsNonBroadcastCases(t *testing.T) {
|
||||||
service := New(Config{SuperGiftMinValue: 1000}, newFakeRepository(), nil, nil)
|
service := New(Config{SuperGiftMinValue: 1000}, newFakeRepository(), nil, nil)
|
||||||
belowThreshold := mustGiftEnvelope(t, roomeventsv1.RoomGiftSent{GiftValue: 999, VisibleRegionId: 1001})
|
belowThreshold := mustGiftEnvelope(t, &roomeventsv1.RoomGiftSent{GiftValue: 999, VisibleRegionId: 1001})
|
||||||
result, err := service.HandleRoomEvent(context.Background(), belowThreshold)
|
result, err := service.HandleRoomEvent(context.Background(), belowThreshold)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("below threshold failed: %v", err)
|
t.Fatalf("below threshold failed: %v", err)
|
||||||
@ -67,7 +67,7 @@ func TestHandleRoomGiftSentSkipsNonBroadcastCases(t *testing.T) {
|
|||||||
t.Fatalf("below threshold should skip: %+v", result)
|
t.Fatalf("below threshold should skip: %+v", result)
|
||||||
}
|
}
|
||||||
|
|
||||||
globalRegion := mustGiftEnvelope(t, roomeventsv1.RoomGiftSent{GiftValue: 1000, VisibleRegionId: 0})
|
globalRegion := mustGiftEnvelope(t, &roomeventsv1.RoomGiftSent{GiftValue: 1000, VisibleRegionId: 0})
|
||||||
result, err = service.HandleRoomEvent(context.Background(), globalRegion)
|
result, err = service.HandleRoomEvent(context.Background(), globalRegion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("global region failed: %v", err)
|
t.Fatalf("global region failed: %v", err)
|
||||||
@ -129,9 +129,9 @@ func TestRemoveRegionBroadcastMemberDeletesOnlyUserMembership(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustGiftEnvelope(t *testing.T, gift roomeventsv1.RoomGiftSent) *roomeventsv1.EventEnvelope {
|
func mustGiftEnvelope(t *testing.T, gift *roomeventsv1.RoomGiftSent) *roomeventsv1.EventEnvelope {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
body, err := proto.Marshal(&gift)
|
body, err := proto.Marshal(gift)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("marshal gift failed: %v", err)
|
t.Fatalf("marshal gift failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -180,15 +181,12 @@ func userIDsTargetPage(userIDs []int64, cursorUserID int64, pageSize int) ([]int
|
|||||||
unique = append(unique, userID)
|
unique = append(unique, userID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sort.Slice(unique, func(i, j int) bool { return unique[i] < unique[j] })
|
slices.Sort(unique)
|
||||||
start := sort.Search(len(unique), func(i int) bool { return unique[i] > cursorUserID })
|
start := sort.Search(len(unique), func(i int) bool { return unique[i] > cursorUserID })
|
||||||
if start >= len(unique) {
|
if start >= len(unique) {
|
||||||
return nil, cursorUserID, true, nil
|
return nil, cursorUserID, true, nil
|
||||||
}
|
}
|
||||||
end := start + pageSize
|
end := min(start+pageSize, len(unique))
|
||||||
if end > len(unique) {
|
|
||||||
end = len(unique)
|
|
||||||
}
|
|
||||||
page := append([]int64(nil), unique[start:end]...)
|
page := append([]int64(nil), unique[start:end]...)
|
||||||
return page, page[len(page)-1], end >= len(unique), nil
|
return page, page[len(page)-1], end >= len(unique), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -545,12 +545,12 @@ func scanInboxMessage(scanner inboxScanner) (messagedomain.InboxMessage, error)
|
|||||||
return message, err
|
return message, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func nullInt64(value int64) sql.NullInt64 {
|
func nullInt64(value int64) sql.Null[int64] {
|
||||||
return sql.NullInt64{Int64: value, Valid: value > 0}
|
return sql.Null[int64]{V: value, Valid: value > 0}
|
||||||
}
|
}
|
||||||
|
|
||||||
func nullString(value string) sql.NullString {
|
func nullString(value string) sql.Null[string] {
|
||||||
return sql.NullString{String: value, Valid: value != ""}
|
return sql.Null[string]{V: value, Valid: value != ""}
|
||||||
}
|
}
|
||||||
|
|
||||||
func truncateFanoutError(message string) string {
|
func truncateFanoutError(message string) string {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.23-alpine AS builder
|
FROM golang:1.26.3-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
|
|||||||
@ -103,11 +103,9 @@ func (a *App) Run() error {
|
|||||||
a.workerWG.Wait()
|
a.workerWG.Wait()
|
||||||
}()
|
}()
|
||||||
if a.scheduler != nil {
|
if a.scheduler != nil {
|
||||||
a.workerWG.Add(1)
|
a.workerWG.Go(func() {
|
||||||
go func() {
|
|
||||||
defer a.workerWG.Done()
|
|
||||||
a.scheduler.Run(a.workerCtx)
|
a.scheduler.Run(a.workerCtx)
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
err := a.server.Serve(a.listener)
|
err := a.server.Serve(a.listener)
|
||||||
|
|||||||
@ -94,11 +94,13 @@ func (s *Scheduler) Run(ctx context.Context) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, appCode := range task.AppCodes {
|
for _, appCode := range task.AppCodes {
|
||||||
wg.Add(1)
|
taskName := taskName
|
||||||
go func(taskName string, appCode string, task config.TaskConfig, handler Handler) {
|
appCode := appCode
|
||||||
defer wg.Done()
|
task := task
|
||||||
|
handler := handler
|
||||||
|
wg.Go(func() {
|
||||||
s.runTaskLoop(ctx, taskName, appCode, task, handler)
|
s.runTaskLoop(ctx, taskName, appCode, task, handler)
|
||||||
}(taskName, appCode, task, handler)
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|||||||
@ -0,0 +1,101 @@
|
|||||||
|
package scheduler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"testing/synctest"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hyapp/services/cron-service/internal/config"
|
||||||
|
mysqlstorage "hyapp/services/cron-service/internal/storage/mysql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type synctestStore struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
leases int
|
||||||
|
started []mysqlstorage.TaskRun
|
||||||
|
finished []mysqlstorage.TaskRun
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *synctestStore) TryAcquireTaskLease(_ context.Context, _ string, _ string, _ string, _ int64, _ time.Duration) (bool, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
s.leases++
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *synctestStore) StartTaskRun(_ context.Context, run mysqlstorage.TaskRun) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
s.started = append(s.started, run)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *synctestStore) FinishTaskRun(_ context.Context, run mysqlstorage.TaskRun) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
s.finished = append(s.finished, run)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSchedulerRunUsesSynctestClock 锁定 cron loop 的首轮立即执行和 interval 退避。
|
||||||
|
// synctest 的虚拟时钟让 worker/timer 测试不依赖真实 sleep,避免调度抖动污染 CI。
|
||||||
|
func TestSchedulerRunUsesSynctestClock(t *testing.T) {
|
||||||
|
synctest.Test(t, func(t *testing.T) {
|
||||||
|
store := &synctestStore{}
|
||||||
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var mu sync.Mutex
|
||||||
|
runTimes := make([]time.Time, 0, 2)
|
||||||
|
s := New("node-a", map[string]config.TaskConfig{
|
||||||
|
"room_outbox": {
|
||||||
|
Enabled: true,
|
||||||
|
AppCodes: []string{"lalu"},
|
||||||
|
Interval: "1s",
|
||||||
|
Timeout: "100ms",
|
||||||
|
LockTTL: "5s",
|
||||||
|
BatchSize: 8,
|
||||||
|
},
|
||||||
|
}, store, map[string]Handler{
|
||||||
|
"room_outbox": func(_ context.Context, request BatchRequest) (BatchResult, error) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if request.BatchSize != 8 || request.AppCode != "lalu" || request.WorkerID != "node-a" {
|
||||||
|
t.Fatalf("unexpected request: %+v", request)
|
||||||
|
}
|
||||||
|
runTimes = append(runTimes, time.Now())
|
||||||
|
return BatchResult{ProcessedCount: 1}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
go s.Run(ctx)
|
||||||
|
synctest.Wait()
|
||||||
|
mu.Lock()
|
||||||
|
if len(runTimes) != 1 {
|
||||||
|
t.Fatalf("first run count = %d, want 1", len(runTimes))
|
||||||
|
}
|
||||||
|
firstRunAt := runTimes[0]
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
synctest.Wait()
|
||||||
|
mu.Lock()
|
||||||
|
if len(runTimes) != 2 {
|
||||||
|
t.Fatalf("second run count = %d, want 2", len(runTimes))
|
||||||
|
}
|
||||||
|
if got := runTimes[1].Sub(firstRunAt); got != time.Second {
|
||||||
|
t.Fatalf("interval = %s, want 1s", got)
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
synctest.Wait()
|
||||||
|
store.mu.Lock()
|
||||||
|
defer store.mu.Unlock()
|
||||||
|
if store.leases != 2 || len(store.started) != 2 || len(store.finished) != 2 {
|
||||||
|
t.Fatalf("store calls: leases=%d started=%d finished=%d", store.leases, len(store.started), len(store.finished))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.23-alpine AS builder
|
FROM golang:1.26.3-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package http
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -38,7 +39,7 @@ type appBootstrapResponse struct {
|
|||||||
// getAppBootstrap 返回 App 启动状态机需要的轻量配置摘要。
|
// getAppBootstrap 返回 App 启动状态机需要的轻量配置摘要。
|
||||||
// bootstrap 只下发版本、开关和配置版本号,不返回 banner、礼物或资源大列表。
|
// bootstrap 只下发版本、开关和配置版本号,不返回 banner、礼物或资源大列表。
|
||||||
func (h *Handler) getAppBootstrap(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) getAppBootstrap(writer http.ResponseWriter, request *http.Request) {
|
||||||
writeOK(writer, request, appBootstrapResponse{
|
httpkit.WriteOK(writer, request, appBootstrapResponse{
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
ServerTimeMS: time.Now().UnixMilli(),
|
ServerTimeMS: time.Now().UnixMilli(),
|
||||||
ForceUpgrade: false,
|
ForceUpgrade: false,
|
||||||
@ -72,24 +73,24 @@ func (h *Handler) getAppBootstrap(writer http.ResponseWriter, request *http.Requ
|
|||||||
func (h *Handler) listH5Links(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) listH5Links(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.appConfigReader == nil {
|
if h.appConfigReader == nil {
|
||||||
// H5 地址来自后台配置表;未注入 reader 时不能返回空假数据。
|
// H5 地址来自后台配置表;未注入 reader 时不能返回空假数据。
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
items, err := h.appConfigReader.ListH5Links(request.Context())
|
items, err := h.appConfigReader.ListH5Links(request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 配置读取失败属于服务端依赖异常,客户端只需要根据 request_id 排查。
|
// 配置读取失败属于服务端依赖异常,客户端只需要根据 request_id 排查。
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeOK(writer, request, map[string]any{"items": items, "total": len(items)})
|
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items)})
|
||||||
}
|
}
|
||||||
|
|
||||||
// listAppBanners 返回后台 APP配置/BANNER配置 中维护的首页 banner。
|
// listAppBanners 返回后台 APP配置/BANNER配置 中维护的首页 banner。
|
||||||
func (h *Handler) listAppBanners(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) listAppBanners(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.appConfigReader == nil {
|
if h.appConfigReader == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,11 +101,11 @@ func (h *Handler) listAppBanners(writer http.ResponseWriter, request *http.Reque
|
|||||||
Country: firstQueryOrHeader(request, "country", "X-Country-Code", "X-App-Country"),
|
Country: firstQueryOrHeader(request, "country", "X-Country-Code", "X-App-Country"),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeOK(writer, request, map[string]any{"items": items, "total": len(items)})
|
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items)})
|
||||||
}
|
}
|
||||||
|
|
||||||
func firstQueryOrHeader(request *http.Request, queryKey string, headerNames ...string) string {
|
func firstQueryOrHeader(request *http.Request, queryKey string, headerNames ...string) string {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -107,25 +108,25 @@ type vipRewardItemData struct {
|
|||||||
// getWalletOverview 返回钱包二级页摘要;我的页首屏使用更轻的 WalletValueSummary。
|
// getWalletOverview 返回钱包二级页摘要;我的页首屏使用更轻的 WalletValueSummary。
|
||||||
func (h *Handler) getWalletOverview(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) getWalletOverview(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.walletClient == nil {
|
if h.walletClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.walletClient.GetWalletOverview(request.Context(), &walletv1.GetWalletOverviewRequest{
|
resp, err := h.walletClient.GetWalletOverview(request.Context(), &walletv1.GetWalletOverviewRequest{
|
||||||
RequestId: requestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(writer, request, walletOverviewFromProto(resp))
|
httpkit.WriteOK(writer, request, walletOverviewFromProto(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
// listRechargeProducts 返回用户当前区域可用的充值档位。
|
// listRechargeProducts 返回用户当前区域可用的充值档位。
|
||||||
func (h *Handler) listRechargeProducts(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) listRechargeProducts(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.walletClient == nil || h.userProfileClient == nil {
|
if h.walletClient == nil || h.userProfileClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
userID := auth.UserIDFromContext(request.Context())
|
userID := auth.UserIDFromContext(request.Context())
|
||||||
@ -134,66 +135,66 @@ func (h *Handler) listRechargeProducts(writer http.ResponseWriter, request *http
|
|||||||
UserId: userID,
|
UserId: userID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.walletClient.ListRechargeProducts(request.Context(), &walletv1.ListRechargeProductsRequest{
|
resp, err := h.walletClient.ListRechargeProducts(request.Context(), &walletv1.ListRechargeProductsRequest{
|
||||||
RequestId: requestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
UserId: userID,
|
UserId: userID,
|
||||||
RegionId: profileResp.GetUser().GetRegionId(),
|
RegionId: profileResp.GetUser().GetRegionId(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
products := make([]rechargeProductData, 0, len(resp.GetProducts()))
|
products := make([]rechargeProductData, 0, len(resp.GetProducts()))
|
||||||
for _, product := range resp.GetProducts() {
|
for _, product := range resp.GetProducts() {
|
||||||
products = append(products, rechargeProductFromProto(product))
|
products = append(products, rechargeProductFromProto(product))
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"channels": resp.GetChannels(), "products": products})
|
httpkit.WriteOK(writer, request, map[string]any{"channels": resp.GetChannels(), "products": products})
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDiamondExchangeConfig 返回钻石兑换配置。
|
// getDiamondExchangeConfig 返回钻石兑换配置。
|
||||||
func (h *Handler) getDiamondExchangeConfig(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) getDiamondExchangeConfig(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.walletClient == nil {
|
if h.walletClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.walletClient.GetDiamondExchangeConfig(request.Context(), &walletv1.GetDiamondExchangeConfigRequest{
|
resp, err := h.walletClient.GetDiamondExchangeConfig(request.Context(), &walletv1.GetDiamondExchangeConfigRequest{
|
||||||
RequestId: requestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rules := make([]diamondExchangeRuleData, 0, len(resp.GetRules()))
|
rules := make([]diamondExchangeRuleData, 0, len(resp.GetRules()))
|
||||||
for _, rule := range resp.GetRules() {
|
for _, rule := range resp.GetRules() {
|
||||||
rules = append(rules, diamondExchangeRuleFromProto(rule))
|
rules = append(rules, diamondExchangeRuleFromProto(rule))
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"rules": rules})
|
httpkit.WriteOK(writer, request, map[string]any{"rules": rules})
|
||||||
}
|
}
|
||||||
|
|
||||||
// listWalletTransactions 返回当前用户钱包流水分页。
|
// listWalletTransactions 返回当前用户钱包流水分页。
|
||||||
func (h *Handler) listWalletTransactions(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) listWalletTransactions(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.walletClient == nil {
|
if h.walletClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
page, ok := parsePositiveInt32Query(request, "page", 1)
|
page, ok := parsePositiveInt32Query(request, "page", 1)
|
||||||
if !ok {
|
if !ok {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pageSize, ok := parsePositiveInt32Query(request, "page_size", 20)
|
pageSize, ok := parsePositiveInt32Query(request, "page_size", 20)
|
||||||
if !ok {
|
if !ok {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.walletClient.ListWalletTransactions(request.Context(), &walletv1.ListWalletTransactionsRequest{
|
resp, err := h.walletClient.ListWalletTransactions(request.Context(), &walletv1.ListWalletTransactionsRequest{
|
||||||
RequestId: requestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
AssetType: strings.TrimSpace(request.URL.Query().Get("asset_type")),
|
AssetType: strings.TrimSpace(request.URL.Query().Get("asset_type")),
|
||||||
@ -201,20 +202,20 @@ func (h *Handler) listWalletTransactions(writer http.ResponseWriter, request *ht
|
|||||||
PageSize: pageSize,
|
PageSize: pageSize,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
items := make([]walletTransactionData, 0, len(resp.GetTransactions()))
|
items := make([]walletTransactionData, 0, len(resp.GetTransactions()))
|
||||||
for _, item := range resp.GetTransactions() {
|
for _, item := range resp.GetTransactions() {
|
||||||
items = append(items, walletTransactionFromProto(item))
|
items = append(items, walletTransactionFromProto(item))
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"items": items, "total": resp.GetTotal(), "page": page, "page_size": pageSize})
|
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": resp.GetTotal(), "page": page, "page_size": pageSize})
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyWithdrawal 创建待人工审核的提现申请。
|
// applyWithdrawal 创建待人工审核的提现申请。
|
||||||
func (h *Handler) applyWithdrawal(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) applyWithdrawal(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.walletClient == nil {
|
if h.walletClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var body withdrawalApplyRequestBody
|
var body withdrawalApplyRequestBody
|
||||||
@ -230,7 +231,7 @@ func (h *Handler) applyWithdrawal(writer http.ResponseWriter, request *http.Requ
|
|||||||
payoutAccount = strings.TrimSpace(body.PayoutAlt)
|
payoutAccount = strings.TrimSpace(body.PayoutAlt)
|
||||||
}
|
}
|
||||||
if commandID == "" || body.Amount <= 0 || payoutAccount == "" {
|
if commandID == "" || body.Amount <= 0 || payoutAccount == "" {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.walletClient.ApplyWithdrawal(request.Context(), &walletv1.ApplyWithdrawalRequest{
|
resp, err := h.walletClient.ApplyWithdrawal(request.Context(), &walletv1.ApplyWithdrawalRequest{
|
||||||
@ -242,10 +243,10 @@ func (h *Handler) applyWithdrawal(writer http.ResponseWriter, request *http.Requ
|
|||||||
Reason: strings.TrimSpace(body.Reason),
|
Reason: strings.TrimSpace(body.Reason),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{
|
httpkit.WriteOK(writer, request, map[string]any{
|
||||||
"withdrawal": withdrawalFromProto(resp.GetWithdrawal()),
|
"withdrawal": withdrawalFromProto(resp.GetWithdrawal()),
|
||||||
"balance": balanceFromProto(resp.GetBalance()),
|
"balance": balanceFromProto(resp.GetBalance()),
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
@ -60,7 +61,7 @@ func (h *Handler) loginPassword(writer http.ResponseWriter, request *http.Reques
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if h.userClient == nil {
|
if h.userClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,11 +71,11 @@ func (h *Handler) loginPassword(writer http.ResponseWriter, request *http.Reques
|
|||||||
Password: body.Password,
|
Password: body.Password,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeOK(writer, request, authData(resp.GetToken(), nil))
|
httpkit.WriteOK(writer, request, authData(resp.GetToken(), nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
// loginThirdParty 处理三方登录即注册。
|
// loginThirdParty 处理三方登录即注册。
|
||||||
@ -119,7 +120,7 @@ func (h *Handler) loginThirdParty(writer http.ResponseWriter, request *http.Requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if h.userClient == nil {
|
if h.userClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,12 +147,12 @@ func (h *Handler) loginThirdParty(writer http.ResponseWriter, request *http.Requ
|
|||||||
Timezone: body.Timezone,
|
Timezone: body.Timezone,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isNewUser := resp.GetIsNewUser()
|
isNewUser := resp.GetIsNewUser()
|
||||||
writeOK(writer, request, authData(resp.GetToken(), &isNewUser))
|
httpkit.WriteOK(writer, request, authData(resp.GetToken(), &isNewUser))
|
||||||
}
|
}
|
||||||
|
|
||||||
// setPassword 处理已登录用户首次设置密码。
|
// setPassword 处理已登录用户首次设置密码。
|
||||||
@ -163,7 +164,7 @@ func (h *Handler) setPassword(writer http.ResponseWriter, request *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if h.userClient == nil {
|
if h.userClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,11 +174,11 @@ func (h *Handler) setPassword(writer http.ResponseWriter, request *http.Request)
|
|||||||
Password: body.Password,
|
Password: body.Password,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeOK(writer, request, map[string]bool{"password_set": resp.GetPasswordSet()})
|
httpkit.WriteOK(writer, request, map[string]bool{"password_set": resp.GetPasswordSet()})
|
||||||
}
|
}
|
||||||
|
|
||||||
// refreshToken 处理 refresh token 轮换。
|
// refreshToken 处理 refresh token 轮换。
|
||||||
@ -196,7 +197,7 @@ func (h *Handler) refreshToken(writer http.ResponseWriter, request *http.Request
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if h.userClient == nil {
|
if h.userClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,11 +206,11 @@ func (h *Handler) refreshToken(writer http.ResponseWriter, request *http.Request
|
|||||||
RefreshToken: body.RefreshToken,
|
RefreshToken: body.RefreshToken,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeOK(writer, request, authData(resp.GetToken(), nil))
|
httpkit.WriteOK(writer, request, authData(resp.GetToken(), nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
// logout 处理 refresh session 失效。
|
// logout 处理 refresh session 失效。
|
||||||
@ -228,7 +229,7 @@ func (h *Handler) logout(writer http.ResponseWriter, request *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if h.userClient == nil {
|
if h.userClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,11 +239,11 @@ func (h *Handler) logout(writer http.ResponseWriter, request *http.Request) {
|
|||||||
RefreshToken: body.RefreshToken,
|
RefreshToken: body.RefreshToken,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeOK(writer, request, map[string]bool{"revoked": resp.GetRevoked()})
|
httpkit.WriteOK(writer, request, map[string]bool{"revoked": resp.GetRevoked()})
|
||||||
}
|
}
|
||||||
|
|
||||||
// authRequestMeta 生成 user-service auth RPC 的统一追踪和审计元信息。
|
// authRequestMeta 生成 user-service auth RPC 的统一追踪和审计元信息。
|
||||||
@ -252,7 +253,7 @@ func authRequestMeta(request *http.Request, deviceID string) *userv1.RequestMeta
|
|||||||
|
|
||||||
func authRequestMetaWithLoginContext(request *http.Request, deviceID string, platform string, language string, timezone string) *userv1.RequestMeta {
|
func authRequestMetaWithLoginContext(request *http.Request, deviceID string, platform string, language string, timezone string) *userv1.RequestMeta {
|
||||||
return &userv1.RequestMeta{
|
return &userv1.RequestMeta{
|
||||||
RequestId: requestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
Caller: "gateway-service",
|
Caller: "gateway-service",
|
||||||
GatewayNodeId: "gateway-local",
|
GatewayNodeId: "gateway-local",
|
||||||
SentAtMs: time.Now().UnixMilli(),
|
SentAtMs: time.Now().UnixMilli(),
|
||||||
@ -301,8 +302,8 @@ func clientIP(request *http.Request) string {
|
|||||||
}
|
}
|
||||||
forwarded := strings.TrimSpace(request.Header.Get("X-Forwarded-For"))
|
forwarded := strings.TrimSpace(request.Header.Get("X-Forwarded-For"))
|
||||||
if forwarded != "" {
|
if forwarded != "" {
|
||||||
parts := strings.Split(forwarded, ",")
|
parts := strings.SplitSeq(forwarded, ",")
|
||||||
for _, part := range parts {
|
for part := range parts {
|
||||||
ip := normalizeIP(part)
|
ip := normalizeIP(part)
|
||||||
if ip == "" {
|
if ip == "" {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -285,14 +286,14 @@ func (h *Handler) allowPublicAuthRequest(writer http.ResponseWriter, request *ht
|
|||||||
}
|
}
|
||||||
allowed, err := h.authRateLimiter.allow(request.Context(), h.authRateKeys(request, input))
|
allowed, err := h.authRateLimiter.allow(request.Context(), h.authRateKeys(request, input))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if allowed {
|
if allowed {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
writeError(writer, request, http.StatusTooManyRequests, codeRateLimited, "rate limited")
|
httpkit.WriteError(writer, request, http.StatusTooManyRequests, httpkit.CodeRateLimited, "rate limited")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -17,13 +18,13 @@ func (h *Handler) handlePushToken(writer http.ResponseWriter, request *http.Requ
|
|||||||
case http.MethodDelete:
|
case http.MethodDelete:
|
||||||
h.deletePushToken(writer, request)
|
h.deletePushToken(writer, request)
|
||||||
default:
|
default:
|
||||||
writeError(writer, request, http.StatusMethodNotAllowed, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusMethodNotAllowed, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) bindPushToken(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) bindPushToken(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.userDeviceClient == nil {
|
if h.userDeviceClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,15 +55,15 @@ func (h *Handler) bindPushToken(writer http.ResponseWriter, request *http.Reques
|
|||||||
Timezone: body.Timezone,
|
Timezone: body.Timezone,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(writer, request, bindPushTokenDataFromProto(resp))
|
httpkit.WriteOK(writer, request, bindPushTokenDataFromProto(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) deletePushToken(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) deletePushToken(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.userDeviceClient == nil {
|
if h.userDeviceClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,10 +83,10 @@ func (h *Handler) deletePushToken(writer http.ResponseWriter, request *http.Requ
|
|||||||
PushToken: strings.TrimSpace(body.PushToken),
|
PushToken: strings.TrimSpace(body.PushToken),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(writer, request, deletePushTokenDataFromProto(resp))
|
httpkit.WriteOK(writer, request, deletePushTokenDataFromProto(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
// bindPushTokenData/deletePushTokenData 固定外部 JSON 字段,避免 proto3 false 被省略。
|
// bindPushTokenData/deletePushTokenData 固定外部 JSON 字段,避免 proto3 false 被省略。
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package http
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@ -185,7 +186,7 @@ func (h *Handler) SetAppConfigReader(reader appConfigReader) {
|
|||||||
// request_id 只做链路追踪;command_id 才是 room-service command log 使用的幂等键。
|
// request_id 只做链路追踪;command_id 才是 room-service command log 使用的幂等键。
|
||||||
func meta(request *http.Request, roomID string, commandID string) *roomv1.RequestMeta {
|
func meta(request *http.Request, roomID string, commandID string) *roomv1.RequestMeta {
|
||||||
return &roomv1.RequestMeta{
|
return &roomv1.RequestMeta{
|
||||||
RequestId: requestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
CommandId: commandIDOrNew(commandID),
|
CommandId: commandIDOrNew(commandID),
|
||||||
ActorUserId: auth.UserIDFromContext(request.Context()),
|
ActorUserId: auth.UserIDFromContext(request.Context()),
|
||||||
RoomId: roomID,
|
RoomId: roomID,
|
||||||
@ -210,8 +211,8 @@ func commandIDOrNew(commandID string) string {
|
|||||||
// decode 统一处理 HTTP JSON 入参解析失败分支。
|
// decode 统一处理 HTTP JSON 入参解析失败分支。
|
||||||
// gateway 在这里拒绝非法 JSON,不做房间业务校验,避免把 Room Cell 规则复制到入口层。
|
// gateway 在这里拒绝非法 JSON,不做房间业务校验,避免把 Room Cell 规则复制到入口层。
|
||||||
func decode(writer http.ResponseWriter, request *http.Request, out any) bool {
|
func decode(writer http.ResponseWriter, request *http.Request, out any) bool {
|
||||||
if err := decodeJSON(request.Body, out); err != nil {
|
if err := httpkit.DecodeJSON(request.Body, out); err != nil {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidJSON, "invalid request body")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidJSON, "invalid request body")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,23 +223,23 @@ func decode(writer http.ResponseWriter, request *http.Request, out any) bool {
|
|||||||
// 首版不在 gateway 细分 gRPC 错误码,避免入口层提前耦合 room-service 的业务错误模型。
|
// 首版不在 gateway 细分 gRPC 错误码,避免入口层提前耦合 room-service 的业务错误模型。
|
||||||
func write(writer http.ResponseWriter, request *http.Request, body any, err error) {
|
func write(writer http.ResponseWriter, request *http.Request, body any, err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeOK(writer, request, body)
|
httpkit.WriteOK(writer, request, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) issueTencentIMUserSig(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) issueTencentIMUserSig(writer http.ResponseWriter, request *http.Request) {
|
||||||
userID := auth.UserIDFromContext(request.Context())
|
userID := auth.UserIDFromContext(request.Context())
|
||||||
if userID <= 0 {
|
if userID <= 0 {
|
||||||
// 正常路径已由 auth middleware 拦截;这里兜底避免签发匿名 IM 票据。
|
// 正常路径已由 auth middleware 拦截;这里兜底避免签发匿名 IM 票据。
|
||||||
writeError(writer, request, http.StatusUnauthorized, codeUnauthorized, "unauthorized")
|
httpkit.WriteError(writer, request, http.StatusUnauthorized, httpkit.CodeUnauthorized, "unauthorized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if h.userProfileClient == nil {
|
if h.userProfileClient == nil {
|
||||||
// 播报群 join 列表必须来自 user-service 当前资料,不能靠 token 快照猜区域。
|
// 播报群 join 列表必须来自 user-service 当前资料,不能靠 token 快照猜区域。
|
||||||
writeError(writer, request, http.StatusInternalServerError, codeInternalError, "internal error")
|
httpkit.WriteError(writer, request, http.StatusInternalServerError, httpkit.CodeInternalError, "internal error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,22 +248,22 @@ func (h *Handler) issueTencentIMUserSig(writer http.ResponseWriter, request *htt
|
|||||||
UserId: userID,
|
UserId: userID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user := userResp.GetUser()
|
user := userResp.GetUser()
|
||||||
if user == nil {
|
if user == nil {
|
||||||
writeError(writer, request, http.StatusInternalServerError, codeInternalError, "internal error")
|
httpkit.WriteError(writer, request, http.StatusInternalServerError, httpkit.CodeInternalError, "internal error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !user.GetProfileCompleted() {
|
if !user.GetProfileCompleted() {
|
||||||
writeError(writer, request, http.StatusForbidden, codeProfileRequired, "profile required")
|
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodeProfileRequired, "profile required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app := appcode.FromContext(request.Context())
|
app := appcode.FromContext(request.Context())
|
||||||
if user.GetAppCode() != "" && appcode.Normalize(user.GetAppCode()) != app {
|
if user.GetAppCode() != "" && appcode.Normalize(user.GetAppCode()) != app {
|
||||||
// UserSig 是 IM 登录凭证,不能给跨 app_code 用户签发,否则会绕过播报群租户隔离。
|
// UserSig 是 IM 登录凭证,不能给跨 app_code 用户签发,否则会绕过播报群租户隔离。
|
||||||
writeError(writer, request, http.StatusForbidden, codePermissionDenied, "permission denied")
|
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,18 +274,18 @@ func (h *Handler) issueTencentIMUserSig(writer http.ResponseWriter, request *htt
|
|||||||
}, tencentim.FormatUserID(userID), time.Now().UTC())
|
}, tencentim.FormatUserID(userID), time.Now().UTC())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 缺少 SDKAppID 或 SecretKey 属于服务端配置错误,不能返回假票据给客户端。
|
// 缺少 SDKAppID 或 SecretKey 属于服务端配置错误,不能返回假票据给客户端。
|
||||||
writeError(writer, request, http.StatusInternalServerError, codeInternalError, "internal error")
|
httpkit.WriteError(writer, request, http.StatusInternalServerError, httpkit.CodeInternalError, "internal error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
joinGroups, ok := imJoinGroupsForUser(app, user.GetRegionId())
|
joinGroups, ok := imJoinGroupsForUser(app, user.GetRegionId())
|
||||||
if !ok {
|
if !ok {
|
||||||
// 生成 join_groups 失败代表服务端 app_code/region_id 配置异常,不能返回缺失群列表让客户端自行猜。
|
// 生成 join_groups 失败代表服务端 app_code/region_id 配置异常,不能返回缺失群列表让客户端自行猜。
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeOK(writer, request, tencentIMUserSigResponse{
|
httpkit.WriteOK(writer, request, tencentIMUserSigResponse{
|
||||||
SDKAppID: result.SDKAppID,
|
SDKAppID: result.SDKAppID,
|
||||||
UserID: result.UserID,
|
UserID: result.UserID,
|
||||||
UserSig: result.UserSig,
|
UserSig: result.UserSig,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
userv1 "hyapp.local/api/proto/user/v1"
|
userv1 "hyapp.local/api/proto/user/v1"
|
||||||
@ -28,7 +29,7 @@ type userHostIdentityData struct {
|
|||||||
// getMyHostIdentity 聚合当前用户在 host domain 中的 App 端身份开关。
|
// getMyHostIdentity 聚合当前用户在 host domain 中的 App 端身份开关。
|
||||||
func (h *Handler) getMyHostIdentity(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) getMyHostIdentity(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.userHostClient == nil {
|
if h.userHostClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,7 +42,7 @@ func (h *Handler) getMyHostIdentity(writer http.ResponseWriter, request *http.Re
|
|||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if xerr.ReasonFromGRPC(err) != xerr.NotFound {
|
if xerr.ReasonFromGRPC(err) != xerr.NotFound {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -54,7 +55,7 @@ func (h *Handler) getMyHostIdentity(writer http.ResponseWriter, request *http.Re
|
|||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if xerr.ReasonFromGRPC(err) != xerr.NotFound {
|
if xerr.ReasonFromGRPC(err) != xerr.NotFound {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -67,20 +68,20 @@ func (h *Handler) getMyHostIdentity(writer http.ResponseWriter, request *http.Re
|
|||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if xerr.ReasonFromGRPC(err) != xerr.NotFound {
|
if xerr.ReasonFromGRPC(err) != xerr.NotFound {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
applyCoinSellerIdentity(&identity, coinSellerResp.GetCoinSellerProfile())
|
applyCoinSellerIdentity(&identity, coinSellerResp.GetCoinSellerProfile())
|
||||||
}
|
}
|
||||||
|
|
||||||
writeOK(writer, request, identity)
|
httpkit.WriteOK(writer, request, identity)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getMyRoleSummary 返回我的页入口显隐所需的完整角色摘要。
|
// getMyRoleSummary 返回我的页入口显隐所需的完整角色摘要。
|
||||||
func (h *Handler) getMyRoleSummary(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) getMyRoleSummary(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.userHostClient == nil {
|
if h.userHostClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.userHostClient.GetUserRoleSummary(request.Context(), &userv1.GetUserRoleSummaryRequest{
|
resp, err := h.userHostClient.GetUserRoleSummary(request.Context(), &userv1.GetUserRoleSummaryRequest{
|
||||||
@ -88,11 +89,11 @@ func (h *Handler) getMyRoleSummary(writer http.ResponseWriter, request *http.Req
|
|||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
summary := resp.GetSummary()
|
summary := resp.GetSummary()
|
||||||
writeOK(writer, request, overviewRolesData{
|
httpkit.WriteOK(writer, request, overviewRolesData{
|
||||||
IsHost: summary.GetIsHost(),
|
IsHost: summary.GetIsHost(),
|
||||||
IsAgency: summary.GetIsAgency(),
|
IsAgency: summary.GetIsAgency(),
|
||||||
IsBD: summary.GetIsBd(),
|
IsBD: summary.GetIsBd(),
|
||||||
|
|||||||
@ -0,0 +1,158 @@
|
|||||||
|
package httpkit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"hyapp/pkg/idgen"
|
||||||
|
"hyapp/pkg/xerr"
|
||||||
|
"hyapp/services/gateway-service/internal/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
type requestIDContextKey struct{}
|
||||||
|
|
||||||
|
const (
|
||||||
|
CodeOK = "OK"
|
||||||
|
CodeInvalidJSON = "INVALID_JSON"
|
||||||
|
CodeInvalidArgument = "INVALID_ARGUMENT"
|
||||||
|
CodeUnauthorized = "UNAUTHORIZED"
|
||||||
|
CodePermissionDenied = "PERMISSION_DENIED"
|
||||||
|
CodeProfileRequired = "PROFILE_REQUIRED"
|
||||||
|
CodeAuthLoginBlocked = "AUTH_LOGIN_BLOCKED"
|
||||||
|
CodeNotFound = "NOT_FOUND"
|
||||||
|
CodeRateLimited = "RATE_LIMITED"
|
||||||
|
CodeUpstreamError = "UPSTREAM_ERROR"
|
||||||
|
CodeInternalError = "INTERNAL_ERROR"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResponseEnvelope 是 gateway 暴露给客户端的稳定 JSON 外壳。
|
||||||
|
// HTTP status 表示传输层结果,code 表示客户端可依赖的错误语义。
|
||||||
|
type ResponseEnvelope struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
RequestID string `json:"request_id"`
|
||||||
|
Data any `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRequestID 为每个业务 API 请求建立追踪 ID。
|
||||||
|
// 客户端传入 X-Request-ID 时沿用,未传时由 gateway 生成,后续 gRPC RequestMeta 复用同一个值。
|
||||||
|
func WithRequestID(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
requestID := strings.TrimSpace(request.Header.Get("X-Request-ID"))
|
||||||
|
if requestID == "" {
|
||||||
|
requestID = idgen.New("req")
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Header().Set("X-Request-ID", requestID)
|
||||||
|
ctx := context.WithValue(request.Context(), requestIDContextKey{}, requestID)
|
||||||
|
next.ServeHTTP(writer, request.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestIDFromContext 读取当前 HTTP 请求的追踪 ID。
|
||||||
|
// 理论上业务路由都会先经过 WithRequestID;兜底生成值只用于防御错误调用。
|
||||||
|
func RequestIDFromContext(ctx context.Context) string {
|
||||||
|
requestID, _ := ctx.Value(requestIDContextKey{}).(string)
|
||||||
|
if requestID != "" {
|
||||||
|
return requestID
|
||||||
|
}
|
||||||
|
|
||||||
|
return idgen.New("req")
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeJSON 隔离 JSON 解码细节,避免 handler 直接依赖 envelope 写法。
|
||||||
|
func DecodeJSON(reader io.Reader, out any) error {
|
||||||
|
return json.NewDecoder(reader).Decode(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteOK 写成功 envelope。
|
||||||
|
func WriteOK(writer http.ResponseWriter, request *http.Request, data any) {
|
||||||
|
WriteEnvelope(writer, http.StatusOK, ResponseEnvelope{
|
||||||
|
Code: CodeOK,
|
||||||
|
Message: "ok",
|
||||||
|
RequestID: ResponseRequestID(request),
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteError 写失败 envelope。
|
||||||
|
// 错误 message 保持稳定、短小,排障依赖 request_id 追日志,而不是把内部错误直接暴露给客户端。
|
||||||
|
func WriteError(writer http.ResponseWriter, request *http.Request, statusCode int, code string, message string) {
|
||||||
|
WriteEnvelope(writer, statusCode, ResponseEnvelope{
|
||||||
|
Code: code,
|
||||||
|
Message: message,
|
||||||
|
RequestID: ResponseRequestID(request),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteRPCError 把内部 gRPC status + ErrorInfo.reason 转成外部稳定 envelope。
|
||||||
|
func WriteRPCError(writer http.ResponseWriter, request *http.Request, err error) {
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := status.FromError(err); !ok {
|
||||||
|
WriteError(writer, request, http.StatusBadGateway, CodeUpstreamError, "upstream service error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reason := xerr.ReasonFromGRPC(err)
|
||||||
|
statusCode, code, message := MapReasonToHTTP(reason)
|
||||||
|
WriteError(writer, request, statusCode, code, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapReasonToHTTP(reason xerr.Code) (int, string, string) {
|
||||||
|
spec := xerr.SpecOf(reason)
|
||||||
|
return spec.HTTPStatus, spec.PublicCode, spec.PublicMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteEnvelope 是 gateway 业务 API 的唯一 JSON 响应出口。
|
||||||
|
func WriteEnvelope(writer http.ResponseWriter, statusCode int, response ResponseEnvelope) {
|
||||||
|
writer.Header().Set("Content-Type", "application/json")
|
||||||
|
writer.WriteHeader(statusCode)
|
||||||
|
if err := json.NewEncoder(writer).Encode(response); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseRequestID 统一确定响应中的 request_id。
|
||||||
|
func ResponseRequestID(request *http.Request) string {
|
||||||
|
return RequestIDFromContext(request.Context())
|
||||||
|
}
|
||||||
|
|
||||||
|
// PositiveInt64PathValue 只接受 ServeMux 已命中的单段正整数路径变量。
|
||||||
|
// 非法路径参数在 transport 层返回 4xx,避免把坏 ID 继续透传到内部 gRPC。
|
||||||
|
func PositiveInt64PathValue(request *http.Request, name string) (int64, bool) {
|
||||||
|
raw := strings.TrimSpace(request.PathValue(name))
|
||||||
|
value, err := strconv.ParseInt(raw, 10, 64)
|
||||||
|
return value, err == nil && value > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequireMethod 在业务 handler 之前固定 HTTP method,避免 path-only mux 让错误 method 进入命令链路。
|
||||||
|
func RequireMethod(method string, next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
if request.Method != method {
|
||||||
|
WriteError(writer, request, http.StatusMethodNotAllowed, CodeInvalidArgument, "invalid argument")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next(writer, request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequireCompletedProfile 在 gateway 层执行外部准入门禁。
|
||||||
|
// 这里故意不访问 user-service,依赖 access token 快照,避免所有高频房间入口同步打用户服务。
|
||||||
|
func RequireCompletedProfile(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
if !auth.ProfileCompletedFromContext(request.Context()) {
|
||||||
|
WriteError(writer, request, http.StatusForbidden, CodeProfileRequired, "profile required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(writer, request)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,293 @@
|
|||||||
|
package httproutes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
const APIV1Prefix = "/api/v1"
|
||||||
|
|
||||||
|
type Wrapper func(http.HandlerFunc) http.Handler
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
PublicWrap Wrapper
|
||||||
|
AuthWrap Wrapper
|
||||||
|
ProfileWrap Wrapper
|
||||||
|
Auth AuthHandlers
|
||||||
|
App AppHandlers
|
||||||
|
User UserHandlers
|
||||||
|
Manager ManagerHandlers
|
||||||
|
Room RoomHandlers
|
||||||
|
Message MessageHandlers
|
||||||
|
Task TaskHandlers
|
||||||
|
Wallet WalletHandlers
|
||||||
|
VIP VIPHandlers
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthHandlers struct {
|
||||||
|
LoginPassword http.HandlerFunc
|
||||||
|
SetPassword http.HandlerFunc
|
||||||
|
LoginThirdParty http.HandlerFunc
|
||||||
|
RefreshToken http.HandlerFunc
|
||||||
|
Logout http.HandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppHandlers struct {
|
||||||
|
ListRegistrationCountries http.HandlerFunc
|
||||||
|
GetAppBootstrap http.HandlerFunc
|
||||||
|
ListH5Links http.HandlerFunc
|
||||||
|
ListAppBanners http.HandlerFunc
|
||||||
|
ListResources http.HandlerFunc
|
||||||
|
GetResourceGroup http.HandlerFunc
|
||||||
|
ListGifts http.HandlerFunc
|
||||||
|
IssueTencentIMUserSig http.HandlerFunc
|
||||||
|
IssueTencentRTCToken http.HandlerFunc
|
||||||
|
HandleTencentIMCallback http.HandlerFunc
|
||||||
|
HandleTencentRTCCallback http.HandlerFunc
|
||||||
|
UploadFile http.HandlerFunc
|
||||||
|
UploadAvatar http.HandlerFunc
|
||||||
|
HandlePushToken http.HandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserHandlers struct {
|
||||||
|
ResolveDisplayUserID http.HandlerFunc
|
||||||
|
BatchUserProfiles http.HandlerFunc
|
||||||
|
GetMyOverview http.HandlerFunc
|
||||||
|
GetMyIdentity http.HandlerFunc
|
||||||
|
GetMyHostIdentity http.HandlerFunc
|
||||||
|
GetMyRoleSummary http.HandlerFunc
|
||||||
|
CompleteMyOnboarding http.HandlerFunc
|
||||||
|
UpdateMyProfile http.HandlerFunc
|
||||||
|
ChangeMyCountry http.HandlerFunc
|
||||||
|
ChangeMyDisplayUserID http.HandlerFunc
|
||||||
|
ApplyMyPrettyDisplayUserID http.HandlerFunc
|
||||||
|
ListMyProfileVisitors http.HandlerFunc
|
||||||
|
ListMyFollowing http.HandlerFunc
|
||||||
|
ListMyFriends http.HandlerFunc
|
||||||
|
ListMyFriendApplications http.HandlerFunc
|
||||||
|
GetMyProfile http.HandlerFunc
|
||||||
|
ListMyResources http.HandlerFunc
|
||||||
|
EquipMyResource http.HandlerFunc
|
||||||
|
UserSocialAction http.HandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type ManagerHandlers struct {
|
||||||
|
ListManagerGrantResources http.HandlerFunc
|
||||||
|
GrantManagerResource http.HandlerFunc
|
||||||
|
LookupBusinessUsers http.HandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoomHandlers struct {
|
||||||
|
GetMyRoom http.HandlerFunc
|
||||||
|
ListRoomFeeds http.HandlerFunc
|
||||||
|
ListRooms http.HandlerFunc
|
||||||
|
GetCurrentRoom http.HandlerFunc
|
||||||
|
GetRoomSnapshot http.HandlerFunc
|
||||||
|
CreateRoom http.HandlerFunc
|
||||||
|
UpdateRoomProfile http.HandlerFunc
|
||||||
|
JoinRoom http.HandlerFunc
|
||||||
|
RoomHeartbeat http.HandlerFunc
|
||||||
|
LeaveRoom http.HandlerFunc
|
||||||
|
MicUp http.HandlerFunc
|
||||||
|
MicDown http.HandlerFunc
|
||||||
|
ChangeMicSeat http.HandlerFunc
|
||||||
|
ConfirmMicPublishing http.HandlerFunc
|
||||||
|
SetMicSeatLock http.HandlerFunc
|
||||||
|
SetChatEnabled http.HandlerFunc
|
||||||
|
SetRoomAdmin http.HandlerFunc
|
||||||
|
TransferRoomHost http.HandlerFunc
|
||||||
|
MuteUser http.HandlerFunc
|
||||||
|
KickUser http.HandlerFunc
|
||||||
|
UnbanUser http.HandlerFunc
|
||||||
|
SendGift http.HandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageHandlers struct {
|
||||||
|
ListMessageTabs http.HandlerFunc
|
||||||
|
MarkInboxSectionRead http.HandlerFunc
|
||||||
|
MarkInboxMessageReadPath http.HandlerFunc
|
||||||
|
DeleteInboxMessagePath http.HandlerFunc
|
||||||
|
ListInboxMessages http.HandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskHandlers struct {
|
||||||
|
ListTaskTabs http.HandlerFunc
|
||||||
|
ClaimTaskReward http.HandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type WalletHandlers struct {
|
||||||
|
GetWalletOverview http.HandlerFunc
|
||||||
|
GetMyBalances http.HandlerFunc
|
||||||
|
ListRechargeProducts http.HandlerFunc
|
||||||
|
GetDiamondExchangeConfig http.HandlerFunc
|
||||||
|
ApplyWithdrawal http.HandlerFunc
|
||||||
|
ListWalletTransactions http.HandlerFunc
|
||||||
|
TransferCoinFromSeller http.HandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type VIPHandlers struct {
|
||||||
|
GetMyVIP http.HandlerFunc
|
||||||
|
ListVIPPackages http.HandlerFunc
|
||||||
|
PurchaseVIP http.HandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type routes struct {
|
||||||
|
mux *http.ServeMux
|
||||||
|
config Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// New 只维护 gateway 对外 HTTP 路径到 transport handler 的绑定。
|
||||||
|
// 业务状态和命令执行仍然全部下沉到 owner service,路由层不承载业务规则。
|
||||||
|
func New(config Config) http.Handler {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
routes := routes{mux: mux, config: config}
|
||||||
|
|
||||||
|
routes.registerAuthRoutes()
|
||||||
|
routes.registerAppRoutes()
|
||||||
|
routes.registerUserRoutes()
|
||||||
|
routes.registerManagerRoutes()
|
||||||
|
routes.registerRoomRoutes()
|
||||||
|
routes.registerMessageRoutes()
|
||||||
|
routes.registerTaskRoutes()
|
||||||
|
routes.registerWalletRoutes()
|
||||||
|
routes.registerVIPRoutes()
|
||||||
|
|
||||||
|
return mux
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r routes) public(path string, method string, next http.HandlerFunc) {
|
||||||
|
r.register(path, method, r.config.PublicWrap, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r routes) auth(path string, method string, next http.HandlerFunc) {
|
||||||
|
r.register(path, method, r.config.AuthWrap, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r routes) profile(path string, method string, next http.HandlerFunc) {
|
||||||
|
r.register(path, method, r.config.ProfileWrap, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r routes) register(path string, method string, wrap Wrapper, next http.HandlerFunc) {
|
||||||
|
if method != "" {
|
||||||
|
// method 继续走 gateway envelope,而不是使用 ServeMux 自动 405。
|
||||||
|
next = httpkit.RequireMethod(method, next)
|
||||||
|
}
|
||||||
|
r.mux.Handle(APIV1Prefix+path, wrap(next))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r routes) registerAuthRoutes() {
|
||||||
|
h := r.config.Auth
|
||||||
|
r.public("/auth/account/login", "", h.LoginPassword)
|
||||||
|
r.auth("/auth/password/set", "", h.SetPassword)
|
||||||
|
r.public("/auth/third-party/login", "", h.LoginThirdParty)
|
||||||
|
r.public("/auth/token/refresh", "", h.RefreshToken)
|
||||||
|
r.public("/auth/logout", "", h.Logout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r routes) registerAppRoutes() {
|
||||||
|
h := r.config.App
|
||||||
|
r.public("/countries", "", h.ListRegistrationCountries)
|
||||||
|
r.public("/app/bootstrap", "", h.GetAppBootstrap)
|
||||||
|
r.public("/app/h5-links", "", h.ListH5Links)
|
||||||
|
r.public("/app/banners", "", h.ListAppBanners)
|
||||||
|
r.public("/resources", "", h.ListResources)
|
||||||
|
r.public("/resource-groups/{group_id}", "", h.GetResourceGroup)
|
||||||
|
r.public("/gifts", "", h.ListGifts)
|
||||||
|
r.profile("/im/usersig", http.MethodGet, h.IssueTencentIMUserSig)
|
||||||
|
r.profile("/rtc/token", http.MethodPost, h.IssueTencentRTCToken)
|
||||||
|
r.public("/tencent-im/callback", "", h.HandleTencentIMCallback)
|
||||||
|
r.public("/tencent-rtc/callback", "", h.HandleTencentRTCCallback)
|
||||||
|
r.auth("/files/upload", "", h.UploadFile)
|
||||||
|
r.auth("/files/avatar/upload", "", h.UploadAvatar)
|
||||||
|
r.profile("/devices/push-token", "", h.HandlePushToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r routes) registerUserRoutes() {
|
||||||
|
h := r.config.User
|
||||||
|
r.public("/users/by-display-user-id/{display_user_id}", "", h.ResolveDisplayUserID)
|
||||||
|
r.profile("/users/profiles:batch", "", h.BatchUserProfiles)
|
||||||
|
r.profile("/users/me/overview", "", h.GetMyOverview)
|
||||||
|
r.auth("/users/me/identity", "", h.GetMyIdentity)
|
||||||
|
r.profile("/users/me/host-identity", "", h.GetMyHostIdentity)
|
||||||
|
r.profile("/users/me/role-summary", "", h.GetMyRoleSummary)
|
||||||
|
r.auth("/users/me/onboarding/complete", "", h.CompleteMyOnboarding)
|
||||||
|
r.profile("/users/me/profile/update", "", h.UpdateMyProfile)
|
||||||
|
r.profile("/users/me/country/change", "", h.ChangeMyCountry)
|
||||||
|
r.auth("/users/me/display-id/change", "", h.ChangeMyDisplayUserID)
|
||||||
|
r.auth("/users/me/display-id/pretty/apply", "", h.ApplyMyPrettyDisplayUserID)
|
||||||
|
r.profile("/users/me/visitors", http.MethodGet, h.ListMyProfileVisitors)
|
||||||
|
r.profile("/users/me/following", http.MethodGet, h.ListMyFollowing)
|
||||||
|
r.profile("/users/me/friends", http.MethodGet, h.ListMyFriends)
|
||||||
|
r.profile("/users/me/friend-requests", http.MethodGet, h.ListMyFriendApplications)
|
||||||
|
r.profile("/users/me", "", h.GetMyProfile)
|
||||||
|
r.profile("/users/me/resources", "", h.ListMyResources)
|
||||||
|
r.profile("/users/me/resources/{resource_id}/equip", "", h.EquipMyResource)
|
||||||
|
r.profile("/users/{user_id}/{action...}", "", h.UserSocialAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r routes) registerManagerRoutes() {
|
||||||
|
h := r.config.Manager
|
||||||
|
r.profile("/manager-center/resource-grants/resources", http.MethodGet, h.ListManagerGrantResources)
|
||||||
|
r.profile("/manager-center/resource-grants", http.MethodPost, h.GrantManagerResource)
|
||||||
|
r.profile("/business/users/lookup", http.MethodGet, h.LookupBusinessUsers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r routes) registerRoomRoutes() {
|
||||||
|
h := r.config.Room
|
||||||
|
r.profile("/rooms/me", http.MethodGet, h.GetMyRoom)
|
||||||
|
r.profile("/rooms/feeds", http.MethodGet, h.ListRoomFeeds)
|
||||||
|
r.profile("/rooms", http.MethodGet, h.ListRooms)
|
||||||
|
r.profile("/rooms/current", http.MethodGet, h.GetCurrentRoom)
|
||||||
|
r.profile("/rooms/snapshot", http.MethodGet, h.GetRoomSnapshot)
|
||||||
|
r.profile("/rooms/create", http.MethodPost, h.CreateRoom)
|
||||||
|
r.profile("/rooms/profile/update", http.MethodPost, h.UpdateRoomProfile)
|
||||||
|
r.profile("/rooms/join", http.MethodPost, h.JoinRoom)
|
||||||
|
r.profile("/rooms/heartbeat", http.MethodPost, h.RoomHeartbeat)
|
||||||
|
r.profile("/rooms/leave", http.MethodPost, h.LeaveRoom)
|
||||||
|
r.profile("/rooms/mic/up", http.MethodPost, h.MicUp)
|
||||||
|
r.profile("/rooms/mic/down", http.MethodPost, h.MicDown)
|
||||||
|
r.profile("/rooms/mic/change", http.MethodPost, h.ChangeMicSeat)
|
||||||
|
r.profile("/rooms/mic/publishing/confirm", http.MethodPost, h.ConfirmMicPublishing)
|
||||||
|
r.profile("/rooms/mic/lock", http.MethodPost, h.SetMicSeatLock)
|
||||||
|
r.profile("/rooms/chat/enabled", http.MethodPost, h.SetChatEnabled)
|
||||||
|
r.profile("/rooms/admin/set", http.MethodPost, h.SetRoomAdmin)
|
||||||
|
r.profile("/rooms/host/transfer", http.MethodPost, h.TransferRoomHost)
|
||||||
|
r.profile("/rooms/user/mute", http.MethodPost, h.MuteUser)
|
||||||
|
r.profile("/rooms/user/kick", http.MethodPost, h.KickUser)
|
||||||
|
r.profile("/rooms/user/unban", http.MethodPost, h.UnbanUser)
|
||||||
|
r.profile("/rooms/gift/send", http.MethodPost, h.SendGift)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r routes) registerMessageRoutes() {
|
||||||
|
h := r.config.Message
|
||||||
|
r.profile("/messages/tabs", "", h.ListMessageTabs)
|
||||||
|
r.profile("/messages/read-all", "", h.MarkInboxSectionRead)
|
||||||
|
r.profile("/messages/{message_id}/read", "", h.MarkInboxMessageReadPath)
|
||||||
|
r.profile("/messages/{message_id}", "", h.DeleteInboxMessagePath)
|
||||||
|
r.profile("/messages", "", h.ListInboxMessages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r routes) registerTaskRoutes() {
|
||||||
|
h := r.config.Task
|
||||||
|
r.profile("/tasks/tabs", http.MethodGet, h.ListTaskTabs)
|
||||||
|
r.profile("/tasks/claim", http.MethodPost, h.ClaimTaskReward)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r routes) registerWalletRoutes() {
|
||||||
|
h := r.config.Wallet
|
||||||
|
r.profile("/wallet/me/overview", http.MethodGet, h.GetWalletOverview)
|
||||||
|
r.profile("/wallet/me/balances", "", h.GetMyBalances)
|
||||||
|
r.profile("/wallet/recharge/products", http.MethodGet, h.ListRechargeProducts)
|
||||||
|
r.profile("/wallet/diamond-exchange/config", http.MethodGet, h.GetDiamondExchangeConfig)
|
||||||
|
r.profile("/wallet/withdrawals/apply", http.MethodPost, h.ApplyWithdrawal)
|
||||||
|
r.profile("/wallet/transactions", http.MethodGet, h.ListWalletTransactions)
|
||||||
|
r.profile("/wallet/coin-seller/transfer", "", h.TransferCoinFromSeller)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r routes) registerVIPRoutes() {
|
||||||
|
h := r.config.VIP
|
||||||
|
r.profile("/vip/me", http.MethodGet, h.GetMyVIP)
|
||||||
|
r.profile("/vip/packages", http.MethodGet, h.ListVIPPackages)
|
||||||
|
r.profile("/vip/purchase", http.MethodPost, h.PurchaseVIP)
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
@ -190,7 +191,7 @@ func (h *Handler) allowLoginRisk(writer http.ResponseWriter, request *http.Reque
|
|||||||
}
|
}
|
||||||
if reason := config.FastGuard.blockReason(input.Language, input.Timezone); reason != "" {
|
if reason := config.FastGuard.blockReason(input.Language, input.Timezone); reason != "" {
|
||||||
h.recordLoginBlocked(request, input, reason)
|
h.recordLoginBlocked(request, input, reason)
|
||||||
writeError(writer, request, http.StatusForbidden, codeAuthLoginBlocked, "login blocked")
|
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodeAuthLoginBlocked, "login blocked")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if h.loginRiskCache == nil {
|
if h.loginRiskCache == nil {
|
||||||
@ -205,7 +206,7 @@ func (h *Handler) allowLoginRisk(writer http.ResponseWriter, request *http.Reque
|
|||||||
}
|
}
|
||||||
if ok && strings.EqualFold(decision.Decision, "blocked") {
|
if ok && strings.EqualFold(decision.Decision, "blocked") {
|
||||||
h.recordLoginBlocked(request, input, loginRiskBlockIPCache)
|
h.recordLoginBlocked(request, input, loginRiskBlockIPCache)
|
||||||
writeError(writer, request, http.StatusForbidden, codeAuthLoginBlocked, "login blocked")
|
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodeAuthLoginBlocked, "login blocked")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package http
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -51,7 +52,7 @@ type managerResourceGrantData struct {
|
|||||||
|
|
||||||
func (h *Handler) listManagerGrantResources(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) listManagerGrantResources(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.walletClient == nil || h.userHostClient == nil {
|
if h.walletClient == nil || h.userHostClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !h.requireManagerResourceGrantCapability(writer, request) {
|
if !h.requireManagerResourceGrantCapability(writer, request) {
|
||||||
@ -59,11 +60,11 @@ func (h *Handler) listManagerGrantResources(writer http.ResponseWriter, request
|
|||||||
}
|
}
|
||||||
page, pageSize, ok := managerPage(request)
|
page, pageSize, ok := managerPage(request)
|
||||||
if !ok {
|
if !ok {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.walletClient.ListResources(request.Context(), &walletv1.ListResourcesRequest{
|
resp, err := h.walletClient.ListResources(request.Context(), &walletv1.ListResourcesRequest{
|
||||||
RequestId: requestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
ResourceType: strings.TrimSpace(request.URL.Query().Get("resource_type")),
|
ResourceType: strings.TrimSpace(request.URL.Query().Get("resource_type")),
|
||||||
Page: page,
|
Page: page,
|
||||||
@ -72,24 +73,24 @@ func (h *Handler) listManagerGrantResources(writer http.ResponseWriter, request
|
|||||||
ManagerGrantOnly: true,
|
ManagerGrantOnly: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
items := make([]managerGrantResourceData, 0, len(resp.GetResources()))
|
items := make([]managerGrantResourceData, 0, len(resp.GetResources()))
|
||||||
for _, resource := range resp.GetResources() {
|
for _, resource := range resp.GetResources() {
|
||||||
items = append(items, managerGrantResourceFromProto(resource))
|
items = append(items, managerGrantResourceFromProto(resource))
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"items": items, "total": resp.GetTotal(), "page": page, "page_size": pageSize})
|
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": resp.GetTotal(), "page": page, "page_size": pageSize})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) lookupBusinessUsers(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) lookupBusinessUsers(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.userProfileClient == nil {
|
if h.userProfileClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pageSize, ok := parsePositiveInt32Query(request, "page_size", 20)
|
pageSize, ok := parsePositiveInt32Query(request, "page_size", 20)
|
||||||
if !ok {
|
if !ok {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if pageSize > 20 {
|
if pageSize > 20 {
|
||||||
@ -103,19 +104,19 @@ func (h *Handler) lookupBusinessUsers(writer http.ResponseWriter, request *http.
|
|||||||
PageSize: pageSize,
|
PageSize: pageSize,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
items := make([]businessUserLookupData, 0, len(resp.GetUsers()))
|
items := make([]businessUserLookupData, 0, len(resp.GetUsers()))
|
||||||
for _, user := range resp.GetUsers() {
|
for _, user := range resp.GetUsers() {
|
||||||
items = append(items, businessUserLookupFromProto(user))
|
items = append(items, businessUserLookupFromProto(user))
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"items": items, "total": len(items), "page_size": pageSize})
|
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items), "page_size": pageSize})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) grantManagerResource(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) grantManagerResource(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.walletClient == nil || h.userHostClient == nil || h.userProfileClient == nil {
|
if h.walletClient == nil || h.userHostClient == nil || h.userProfileClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var body struct {
|
var body struct {
|
||||||
@ -134,7 +135,7 @@ func (h *Handler) grantManagerResource(writer http.ResponseWriter, request *http
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if body.Quantity != nil || body.DurationMS != nil || body.DurationMSAlt != nil {
|
if body.Quantity != nil || body.DurationMS != nil || body.DurationMSAlt != nil {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !h.requireManagerResourceGrantCapability(writer, request) {
|
if !h.requireManagerResourceGrantCapability(writer, request) {
|
||||||
@ -147,7 +148,7 @@ func (h *Handler) grantManagerResource(writer http.ResponseWriter, request *http
|
|||||||
targetUserID, targetOK := rawInt64(body.TargetUserID, body.TargetUserIDAlt)
|
targetUserID, targetOK := rawInt64(body.TargetUserID, body.TargetUserIDAlt)
|
||||||
resourceID, resourceOK := rawInt64(body.ResourceID, body.ResourceIDAlt)
|
resourceID, resourceOK := rawInt64(body.ResourceID, body.ResourceIDAlt)
|
||||||
if commandID == "" || !targetOK || targetUserID <= 0 || !resourceOK || resourceID <= 0 {
|
if commandID == "" || !targetOK || targetUserID <= 0 || !resourceOK || resourceID <= 0 {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
targetResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
targetResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||||
@ -155,11 +156,11 @@ func (h *Handler) grantManagerResource(writer http.ResponseWriter, request *http
|
|||||||
UserId: targetUserID,
|
UserId: targetUserID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if target := targetResp.GetUser(); target == nil || target.GetStatus() != userv1.UserStatus_USER_STATUS_ACTIVE {
|
if target := targetResp.GetUser(); target == nil || target.GetStatus() != userv1.UserStatus_USER_STATUS_ACTIVE {
|
||||||
writeError(writer, request, http.StatusNotFound, codeNotFound, "not found")
|
httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
reason := strings.TrimSpace(body.Reason)
|
reason := strings.TrimSpace(body.Reason)
|
||||||
@ -185,7 +186,7 @@ func (h *Handler) grantManagerResource(writer http.ResponseWriter, request *http
|
|||||||
slog.Int64("resource_id", resourceID),
|
slog.Int64("resource_id", resourceID),
|
||||||
slog.String("command_id", commandID),
|
slog.String("command_id", commandID),
|
||||||
)
|
)
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
grant := managerResourceGrantFromProto(resp.GetGrant())
|
grant := managerResourceGrantFromProto(resp.GetGrant())
|
||||||
@ -195,7 +196,7 @@ func (h *Handler) grantManagerResource(writer http.ResponseWriter, request *http
|
|||||||
slog.Int64("resource_id", resourceID),
|
slog.Int64("resource_id", resourceID),
|
||||||
slog.String("grant_id", grant.GrantID),
|
slog.String("grant_id", grant.GrantID),
|
||||||
)
|
)
|
||||||
writeOK(writer, request, grant)
|
httpkit.WriteOK(writer, request, grant)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) requireManagerResourceGrantCapability(writer http.ResponseWriter, request *http.Request) bool {
|
func (h *Handler) requireManagerResourceGrantCapability(writer http.ResponseWriter, request *http.Request) bool {
|
||||||
@ -205,11 +206,11 @@ func (h *Handler) requireManagerResourceGrantCapability(writer http.ResponseWrit
|
|||||||
Capability: managerResourceGrantCapability,
|
Capability: managerResourceGrantCapability,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if !resp.GetAllowed() {
|
if !resp.GetAllowed() {
|
||||||
writeError(writer, request, http.StatusForbidden, codePermissionDenied, "permission denied")
|
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package http
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
@ -72,7 +73,7 @@ func TestListManagerGrantResourcesChecksCapabilityAndFiltersWalletRequest(t *tes
|
|||||||
if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
|
||||||
t.Fatalf("decode response failed: %v", err)
|
t.Fatalf("decode response failed: %v", err)
|
||||||
}
|
}
|
||||||
if envelope.Code != codeOK || envelope.RequestID != "req-manager-resources" || envelope.Data.Total != 1 || len(envelope.Data.Items) != 1 {
|
if envelope.Code != httpkit.CodeOK || envelope.RequestID != "req-manager-resources" || envelope.Data.Total != 1 || len(envelope.Data.Items) != 1 {
|
||||||
t.Fatalf("response envelope mismatch: %+v", envelope)
|
t.Fatalf("response envelope mismatch: %+v", envelope)
|
||||||
}
|
}
|
||||||
if envelope.Data.Items[0].ResourceID != "101" || envelope.Data.Items[0].Name != "VIP Badge" {
|
if envelope.Data.Items[0].ResourceID != "101" || envelope.Data.Items[0].Name != "VIP Badge" {
|
||||||
@ -91,7 +92,7 @@ func TestListManagerGrantResourcesRejectsUnauthorizedManager(t *testing.T) {
|
|||||||
|
|
||||||
router.ServeHTTP(recorder, request)
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
assertEnvelope(t, recorder, http.StatusForbidden, codePermissionDenied, "req-manager-denied")
|
assertEnvelope(t, recorder, http.StatusForbidden, httpkit.CodePermissionDenied, "req-manager-denied")
|
||||||
if walletClient.lastListResources != nil {
|
if walletClient.lastListResources != nil {
|
||||||
t.Fatalf("unauthorized manager must not reach wallet-service: %+v", walletClient.lastListResources)
|
t.Fatalf("unauthorized manager must not reach wallet-service: %+v", walletClient.lastListResources)
|
||||||
}
|
}
|
||||||
@ -129,7 +130,7 @@ func TestBusinessUserLookupForwardsSceneAndActor(t *testing.T) {
|
|||||||
if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
|
||||||
t.Fatalf("decode response failed: %v", err)
|
t.Fatalf("decode response failed: %v", err)
|
||||||
}
|
}
|
||||||
if envelope.Code != codeOK || envelope.Data.Total != 1 || envelope.Data.Items[0].UserID != "900001" || envelope.Data.Items[0].Status != "active" {
|
if envelope.Code != httpkit.CodeOK || envelope.Data.Total != 1 || envelope.Data.Items[0].UserID != "900001" || envelope.Data.Items[0].Status != "active" {
|
||||||
t.Fatalf("business lookup response mismatch: %+v", envelope)
|
t.Fatalf("business lookup response mismatch: %+v", envelope)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -172,7 +173,7 @@ func TestGrantManagerResourceRejectsClientComputedQuantityOrDuration(t *testing.
|
|||||||
|
|
||||||
router.ServeHTTP(recorder, request)
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
assertEnvelope(t, recorder, http.StatusBadRequest, codeInvalidArgument, "req-manager-grant-shape")
|
assertEnvelope(t, recorder, http.StatusBadRequest, httpkit.CodeInvalidArgument, "req-manager-grant-shape")
|
||||||
if walletClient.lastGrantResource != nil {
|
if walletClient.lastGrantResource != nil {
|
||||||
t.Fatalf("client computed grant shape must not reach wallet-service: %+v", walletClient.lastGrantResource)
|
t.Fatalf("client computed grant shape must not reach wallet-service: %+v", walletClient.lastGrantResource)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -46,7 +47,7 @@ type userProfileBatchData struct {
|
|||||||
// listMessageTabs returns backend unread summary and declares user conversations as Tencent IM SDK owned.
|
// listMessageTabs returns backend unread summary and declares user conversations as Tencent IM SDK owned.
|
||||||
func (h *Handler) listMessageTabs(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) listMessageTabs(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.messageClient == nil {
|
if h.messageClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.messageClient.ListMessageTabs(request.Context(), &activityv1.ListMessageTabsRequest{
|
resp, err := h.messageClient.ListMessageTabs(request.Context(), &activityv1.ListMessageTabsRequest{
|
||||||
@ -54,7 +55,7 @@ func (h *Handler) listMessageTabs(writer http.ResponseWriter, request *http.Requ
|
|||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sections := make([]messageTabSectionData, 0, len(resp.GetSections()))
|
sections := make([]messageTabSectionData, 0, len(resp.GetSections()))
|
||||||
@ -66,18 +67,18 @@ func (h *Handler) listMessageTabs(writer http.ResponseWriter, request *http.Requ
|
|||||||
Source: section.GetSource(),
|
Source: section.GetSource(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"sections": sections})
|
httpkit.WriteOK(writer, request, map[string]any{"sections": sections})
|
||||||
}
|
}
|
||||||
|
|
||||||
// listInboxMessages reads system/activity inbox list; private user conversations stay in Tencent IM SDK.
|
// listInboxMessages reads system/activity inbox list; private user conversations stay in Tencent IM SDK.
|
||||||
func (h *Handler) listInboxMessages(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) listInboxMessages(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.messageClient == nil {
|
if h.messageClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pageSize, ok := parseMessagePageSize(request.URL.Query().Get("page_size"))
|
pageSize, ok := parseMessagePageSize(request.URL.Query().Get("page_size"))
|
||||||
if !ok {
|
if !ok {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
section := strings.TrimSpace(request.URL.Query().Get("section"))
|
section := strings.TrimSpace(request.URL.Query().Get("section"))
|
||||||
@ -89,7 +90,7 @@ func (h *Handler) listInboxMessages(writer http.ResponseWriter, request *http.Re
|
|||||||
PageToken: strings.TrimSpace(request.URL.Query().Get("page_token")),
|
PageToken: strings.TrimSpace(request.URL.Query().Get("page_token")),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
items := make([]inboxMessageData, 0, len(resp.GetItems()))
|
items := make([]inboxMessageData, 0, len(resp.GetItems()))
|
||||||
@ -107,13 +108,13 @@ func (h *Handler) listInboxMessages(writer http.ResponseWriter, request *http.Re
|
|||||||
SentAtMS: item.GetSentAtMs(),
|
SentAtMS: item.GetSentAtMs(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"section": resp.GetSection(), "items": items, "next_page_token": resp.GetNextPageToken()})
|
httpkit.WriteOK(writer, request, map[string]any{"section": resp.GetSection(), "items": items, "next_page_token": resp.GetNextPageToken()})
|
||||||
}
|
}
|
||||||
|
|
||||||
// markInboxSectionRead marks all current visible unread messages in one backend-owned section.
|
// markInboxSectionRead marks all current visible unread messages in one backend-owned section.
|
||||||
func (h *Handler) markInboxSectionRead(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) markInboxSectionRead(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.messageClient == nil {
|
if h.messageClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var body struct {
|
var body struct {
|
||||||
@ -128,37 +129,47 @@ func (h *Handler) markInboxSectionRead(writer http.ResponseWriter, request *http
|
|||||||
Section: body.Section,
|
Section: body.Section,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"section": resp.GetSection(), "read_count": resp.GetReadCount()})
|
httpkit.WriteOK(writer, request, map[string]any{"section": resp.GetSection(), "read_count": resp.GetReadCount()})
|
||||||
}
|
}
|
||||||
|
|
||||||
// inboxMessageByID handles per-message read and delete routes under /api/v1/messages/{message_id}.
|
// markInboxMessageReadByPath handles /api/v1/messages/{message_id}/read.
|
||||||
func (h *Handler) inboxMessageByID(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) markInboxMessageReadByPath(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.messageClient == nil {
|
if h.messageClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
messageID, readRoute := parseInboxMessagePath(request.URL.Path)
|
messageID := strings.TrimSpace(request.PathValue("message_id"))
|
||||||
if messageID == "" {
|
if messageID == "" {
|
||||||
writeError(writer, request, http.StatusNotFound, codeNotFound, "not found")
|
httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if readRoute {
|
h.markInboxMessageRead(writer, request, messageID)
|
||||||
h.markInboxMessageRead(writer, request, messageID)
|
}
|
||||||
|
|
||||||
|
// deleteInboxMessageByPath handles /api/v1/messages/{message_id}.
|
||||||
|
func (h *Handler) deleteInboxMessageByPath(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
if h.messageClient == nil {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
messageID := strings.TrimSpace(request.PathValue("message_id"))
|
||||||
|
if messageID == "" {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if request.Method == http.MethodDelete {
|
if request.Method == http.MethodDelete {
|
||||||
h.deleteInboxMessage(writer, request, messageID)
|
h.deleteInboxMessage(writer, request, messageID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeError(writer, request, http.StatusNotFound, codeNotFound, "not found")
|
httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) markInboxMessageRead(writer http.ResponseWriter, request *http.Request, messageID string) {
|
func (h *Handler) markInboxMessageRead(writer http.ResponseWriter, request *http.Request, messageID string) {
|
||||||
if request.Method != http.MethodPost {
|
if request.Method != http.MethodPost {
|
||||||
writeError(writer, request, http.StatusNotFound, codeNotFound, "not found")
|
httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.messageClient.MarkInboxMessageRead(request.Context(), &activityv1.MarkInboxMessageReadRequest{
|
resp, err := h.messageClient.MarkInboxMessageRead(request.Context(), &activityv1.MarkInboxMessageReadRequest{
|
||||||
@ -167,10 +178,10 @@ func (h *Handler) markInboxMessageRead(writer http.ResponseWriter, request *http
|
|||||||
MessageId: messageID,
|
MessageId: messageID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"message_id": resp.GetMessageId(), "read": resp.GetRead(), "read_at_ms": resp.GetReadAtMs()})
|
httpkit.WriteOK(writer, request, map[string]any{"message_id": resp.GetMessageId(), "read": resp.GetRead(), "read_at_ms": resp.GetReadAtMs()})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) deleteInboxMessage(writer http.ResponseWriter, request *http.Request, messageID string) {
|
func (h *Handler) deleteInboxMessage(writer http.ResponseWriter, request *http.Request, messageID string) {
|
||||||
@ -180,21 +191,21 @@ func (h *Handler) deleteInboxMessage(writer http.ResponseWriter, request *http.R
|
|||||||
MessageId: messageID,
|
MessageId: messageID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"message_id": resp.GetMessageId(), "deleted": resp.GetDeleted()})
|
httpkit.WriteOK(writer, request, map[string]any{"message_id": resp.GetMessageId(), "deleted": resp.GetDeleted()})
|
||||||
}
|
}
|
||||||
|
|
||||||
// batchUserProfiles returns only safe profile fields needed to render Tencent IM conversation rows.
|
// batchUserProfiles returns only safe profile fields needed to render Tencent IM conversation rows.
|
||||||
func (h *Handler) batchUserProfiles(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) batchUserProfiles(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.userProfileClient == nil {
|
if h.userProfileClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
userIDs, ok := parseBatchUserIDs(request)
|
userIDs, ok := parseBatchUserIDs(request)
|
||||||
if !ok {
|
if !ok {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.userProfileClient.BatchGetUsers(request.Context(), &userv1.BatchGetUsersRequest{
|
resp, err := h.userProfileClient.BatchGetUsers(request.Context(), &userv1.BatchGetUsersRequest{
|
||||||
@ -202,7 +213,7 @@ func (h *Handler) batchUserProfiles(writer http.ResponseWriter, request *http.Re
|
|||||||
UserIds: userIDs,
|
UserIds: userIDs,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
items := make([]userProfileBatchData, 0, len(resp.GetUsers()))
|
items := make([]userProfileBatchData, 0, len(resp.GetUsers()))
|
||||||
@ -220,12 +231,12 @@ func (h *Handler) batchUserProfiles(writer http.ResponseWriter, request *http.Re
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"profiles": items})
|
httpkit.WriteOK(writer, request, map[string]any{"profiles": items})
|
||||||
}
|
}
|
||||||
|
|
||||||
func activityMeta(request *http.Request) *activityv1.RequestMeta {
|
func activityMeta(request *http.Request) *activityv1.RequestMeta {
|
||||||
return &activityv1.RequestMeta{
|
return &activityv1.RequestMeta{
|
||||||
RequestId: requestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
Caller: "gateway-service",
|
Caller: "gateway-service",
|
||||||
GatewayNodeId: "gateway-local",
|
GatewayNodeId: "gateway-local",
|
||||||
SentAtMs: time.Now().UnixMilli(),
|
SentAtMs: time.Now().UnixMilli(),
|
||||||
@ -245,29 +256,13 @@ func parseMessagePageSize(raw string) (int32, bool) {
|
|||||||
return int32(value), true
|
return int32(value), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseInboxMessagePath(path string) (string, bool) {
|
|
||||||
tail := strings.TrimPrefix(path, apiV1Prefix+"/messages/")
|
|
||||||
tail = strings.Trim(tail, "/")
|
|
||||||
if tail == "" || tail == "tabs" || tail == "read-all" {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
if strings.HasSuffix(tail, "/read") {
|
|
||||||
messageID := strings.TrimSuffix(tail, "/read")
|
|
||||||
return strings.Trim(messageID, "/"), true
|
|
||||||
}
|
|
||||||
if strings.Contains(tail, "/") {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
return tail, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseBatchUserIDs(request *http.Request) ([]int64, bool) {
|
func parseBatchUserIDs(request *http.Request) ([]int64, bool) {
|
||||||
rawValues := append([]string(nil), request.URL.Query()["user_ids"]...)
|
rawValues := append([]string(nil), request.URL.Query()["user_ids"]...)
|
||||||
rawValues = append(rawValues, request.URL.Query()["user_id"]...)
|
rawValues = append(rawValues, request.URL.Query()["user_id"]...)
|
||||||
seen := make(map[int64]bool)
|
seen := make(map[int64]bool)
|
||||||
result := make([]int64, 0, len(rawValues))
|
result := make([]int64, 0, len(rawValues))
|
||||||
for _, raw := range rawValues {
|
for _, raw := range rawValues {
|
||||||
for _, piece := range strings.Split(raw, ",") {
|
for piece := range strings.SplitSeq(raw, ",") {
|
||||||
piece = strings.TrimSpace(piece)
|
piece = strings.TrimSpace(piece)
|
||||||
if piece == "" {
|
if piece == "" {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package http
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@ -16,23 +17,23 @@ import (
|
|||||||
|
|
||||||
// apiHandler 包装需要 app 解析和 access token 的业务 API。
|
// apiHandler 包装需要 app 解析和 access token 的业务 API。
|
||||||
func (h *Handler) apiHandler(jwtVerifier *auth.Verifier, next http.HandlerFunc) http.Handler {
|
func (h *Handler) apiHandler(jwtVerifier *auth.Verifier, next http.HandlerFunc) http.Handler {
|
||||||
return withRequestID(withAccessLog(h.withResolvedApp(h.withAuth(jwtVerifier, next))))
|
return httpkit.WithRequestID(withAccessLog(h.withResolvedApp(h.withAuth(jwtVerifier, next))))
|
||||||
}
|
}
|
||||||
|
|
||||||
// profileAPIHandler 包装需要 app 解析、access token 和完整资料门禁的业务 API。
|
// profileAPIHandler 包装需要 app 解析、access token 和完整资料门禁的业务 API。
|
||||||
func (h *Handler) profileAPIHandler(jwtVerifier *auth.Verifier, next http.HandlerFunc) http.Handler {
|
func (h *Handler) profileAPIHandler(jwtVerifier *auth.Verifier, next http.HandlerFunc) http.Handler {
|
||||||
return withRequestID(withAccessLog(h.withResolvedApp(h.withAuth(jwtVerifier, requireCompletedProfile(next)))))
|
return httpkit.WithRequestID(withAccessLog(h.withResolvedApp(h.withAuth(jwtVerifier, httpkit.RequireCompletedProfile(next)))))
|
||||||
}
|
}
|
||||||
|
|
||||||
// publicAPIHandler 包装只需要 app 解析的公开 API;登录/注册会用解析结果创建对应 App 的用户。
|
// publicAPIHandler 包装只需要 app 解析的公开 API;登录/注册会用解析结果创建对应 App 的用户。
|
||||||
func (h *Handler) publicAPIHandler(next http.HandlerFunc) http.Handler {
|
func (h *Handler) publicAPIHandler(next http.HandlerFunc) http.Handler {
|
||||||
return withRequestID(withAccessLog(h.withResolvedApp(next)))
|
return httpkit.WithRequestID(withAccessLog(h.withResolvedApp(next)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// withAccessLog 统一记录 gateway 业务 API 的请求体、响应体、状态码和耗时。
|
// withAccessLog 统一记录 gateway 业务 API 的请求体、响应体、状态码和耗时。
|
||||||
// healthcheck 不经过该 middleware,避免探活日志淹没业务调用。
|
// healthcheck 不经过该 middleware,避免探活日志淹没业务调用。
|
||||||
func withAccessLog(next http.Handler) http.Handler {
|
func withAccessLog(next http.Handler) http.Handler {
|
||||||
return logx.HTTPMiddleware("gateway-service", requestIDFromContext, next)
|
return logx.HTTPMiddleware("gateway-service", httpkit.RequestIDFromContext, next)
|
||||||
}
|
}
|
||||||
|
|
||||||
// withAuth 在 transport 层完成鉴权失败响应写入。
|
// withAuth 在 transport 层完成鉴权失败响应写入。
|
||||||
@ -41,11 +42,11 @@ func (h *Handler) withAuth(jwtVerifier *auth.Verifier, next http.HandlerFunc) ht
|
|||||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||||
claims, err := jwtVerifier.Verify(request.Header.Get("Authorization"))
|
claims, err := jwtVerifier.Verify(request.Header.Get("Authorization"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(writer, request, http.StatusUnauthorized, codeUnauthorized, "unauthorized")
|
httpkit.WriteError(writer, request, http.StatusUnauthorized, httpkit.CodeUnauthorized, "unauthorized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(claims.SessionID) == "" {
|
if strings.TrimSpace(claims.SessionID) == "" {
|
||||||
writeError(writer, request, http.StatusUnauthorized, string(xerr.SessionRevoked), "unauthorized")
|
httpkit.WriteError(writer, request, http.StatusUnauthorized, string(xerr.SessionRevoked), "unauthorized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,7 +56,7 @@ func (h *Handler) withAuth(jwtVerifier *auth.Verifier, next http.HandlerFunc) ht
|
|||||||
logx.Warn(ctx, "session_denylist_read_failed", slog.String("component", "gateway_auth"), slog.String("session_id", claims.SessionID), slog.String("error", err.Error()))
|
logx.Warn(ctx, "session_denylist_read_failed", slog.String("component", "gateway_auth"), slog.String("session_id", claims.SessionID), slog.String("error", err.Error()))
|
||||||
}
|
}
|
||||||
if revoked {
|
if revoked {
|
||||||
writeError(writer, request.WithContext(ctx), http.StatusUnauthorized, string(xerr.SessionRevoked), "unauthorized")
|
httpkit.WriteError(writer, request.WithContext(ctx), http.StatusUnauthorized, string(xerr.SessionRevoked), "unauthorized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
next.ServeHTTP(writer, request.WithContext(ctx))
|
next.ServeHTTP(writer, request.WithContext(ctx))
|
||||||
@ -75,7 +76,7 @@ func (h *Handler) withResolvedApp(next http.Handler) http.Handler {
|
|||||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||||
appCode, err := h.resolveRequestAppCode(request.Context(), request)
|
appCode, err := h.resolveRequestAppCode(request.Context(), request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,7 +92,7 @@ func (h *Handler) resolveRequestAppCode(ctx context.Context, request *http.Reque
|
|||||||
if h.appRegistryClient != nil && (explicitAppCode != "" || packageName != "") {
|
if h.appRegistryClient != nil && (explicitAppCode != "" || packageName != "") {
|
||||||
resp, err := h.appRegistryClient.ResolveApp(ctx, &userv1.ResolveAppRequest{
|
resp, err := h.appRegistryClient.ResolveApp(ctx, &userv1.ResolveAppRequest{
|
||||||
Meta: &userv1.RequestMeta{
|
Meta: &userv1.RequestMeta{
|
||||||
RequestId: requestIDFromContext(ctx),
|
RequestId: httpkit.RequestIDFromContext(ctx),
|
||||||
Caller: "gateway-service",
|
Caller: "gateway-service",
|
||||||
SentAtMs: time.Now().UnixMilli(),
|
SentAtMs: time.Now().UnixMilli(),
|
||||||
AppCode: explicitAppCode,
|
AppCode: explicitAppCode,
|
||||||
@ -126,28 +127,3 @@ func firstHeader(request *http.Request, names ...string) string {
|
|||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// requireMethod 在业务 handler 之前固定 HTTP method,避免 path-only mux 让错误 method 进入命令链路。
|
|
||||||
func requireMethod(method string, next http.HandlerFunc) http.HandlerFunc {
|
|
||||||
return func(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
if request.Method != method {
|
|
||||||
writeError(writer, request, http.StatusMethodNotAllowed, codeInvalidArgument, "invalid argument")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
next(writer, request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// requireCompletedProfile 在 gateway 层执行外部准入门禁。
|
|
||||||
// 这里故意不访问 user-service,依赖 access token 快照,避免所有高频房间入口同步打用户服务。
|
|
||||||
func requireCompletedProfile(next http.HandlerFunc) http.HandlerFunc {
|
|
||||||
return func(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
if !auth.ProfileCompletedFromContext(request.Context()) {
|
|
||||||
writeError(writer, request, http.StatusForbidden, codeProfileRequired, "profile required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
next.ServeHTTP(writer, request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package http
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@ -76,7 +77,7 @@ type overviewVIPData struct {
|
|||||||
// getMyOverview 聚合 App 我的页首屏摘要;gateway 只做 RPC 编排和入口显隐规则,不查数据库。
|
// getMyOverview 聚合 App 我的页首屏摘要;gateway 只做 RPC 编排和入口显隐规则,不查数据库。
|
||||||
func (h *Handler) getMyOverview(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) getMyOverview(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.userProfileClient == nil {
|
if h.userProfileClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,73 +89,63 @@ func (h *Handler) getMyOverview(writer http.ResponseWriter, request *http.Reques
|
|||||||
|
|
||||||
var profileResp *userv1.GetUserResponse
|
var profileResp *userv1.GetUserResponse
|
||||||
var profileErr error
|
var profileErr error
|
||||||
wg.Add(1)
|
wg.Go(func() {
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
profileResp, profileErr = h.userProfileClient.GetUser(ctx, &userv1.GetUserRequest{
|
profileResp, profileErr = h.userProfileClient.GetUser(ctx, &userv1.GetUserRequest{
|
||||||
Meta: authRequestMeta(request.WithContext(ctx), ""),
|
Meta: authRequestMeta(request.WithContext(ctx), ""),
|
||||||
UserId: userID,
|
UserId: userID,
|
||||||
})
|
})
|
||||||
}()
|
})
|
||||||
|
|
||||||
var walletResp *walletv1.GetWalletValueSummaryResponse
|
var walletResp *walletv1.GetWalletValueSummaryResponse
|
||||||
var walletErr error
|
var walletErr error
|
||||||
wg.Add(1)
|
wg.Go(func() {
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
if h.walletClient == nil {
|
if h.walletClient == nil {
|
||||||
walletErr = xerr.New(xerr.Unavailable, "wallet service is not configured")
|
walletErr = xerr.New(xerr.Unavailable, "wallet service is not configured")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
walletResp, walletErr = h.walletClient.GetWalletValueSummary(ctx, &walletv1.GetWalletValueSummaryRequest{
|
walletResp, walletErr = h.walletClient.GetWalletValueSummary(ctx, &walletv1.GetWalletValueSummaryRequest{
|
||||||
RequestId: requestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
UserId: userID,
|
UserId: userID,
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
})
|
})
|
||||||
}()
|
})
|
||||||
|
|
||||||
var vipResp *walletv1.GetMyVipResponse
|
var vipResp *walletv1.GetMyVipResponse
|
||||||
var vipErr error
|
var vipErr error
|
||||||
wg.Add(1)
|
wg.Go(func() {
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
if h.walletClient == nil {
|
if h.walletClient == nil {
|
||||||
vipErr = xerr.New(xerr.Unavailable, "wallet service is not configured")
|
vipErr = xerr.New(xerr.Unavailable, "wallet service is not configured")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
vipResp, vipErr = h.walletClient.GetMyVip(ctx, &walletv1.GetMyVipRequest{
|
vipResp, vipErr = h.walletClient.GetMyVip(ctx, &walletv1.GetMyVipRequest{
|
||||||
RequestId: requestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
UserId: userID,
|
UserId: userID,
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
})
|
})
|
||||||
}()
|
})
|
||||||
|
|
||||||
var roles overviewRolesData
|
var roles overviewRolesData
|
||||||
var rolesErr error
|
var rolesErr error
|
||||||
wg.Add(1)
|
wg.Go(func() {
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
roles, rolesErr = h.getOverviewRoles(ctx, request, userID)
|
roles, rolesErr = h.getOverviewRoles(ctx, request, userID)
|
||||||
}()
|
})
|
||||||
|
|
||||||
var statsResp *userv1.GetMyProfileStatsResponse
|
var statsResp *userv1.GetMyProfileStatsResponse
|
||||||
var statsErr error
|
var statsErr error
|
||||||
wg.Add(1)
|
wg.Go(func() {
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
statsResp, statsErr = h.userProfileClient.GetMyProfileStats(ctx, &userv1.GetMyProfileStatsRequest{
|
statsResp, statsErr = h.userProfileClient.GetMyProfileStats(ctx, &userv1.GetMyProfileStatsRequest{
|
||||||
Meta: authRequestMeta(request.WithContext(ctx), ""),
|
Meta: authRequestMeta(request.WithContext(ctx), ""),
|
||||||
UserId: userID,
|
UserId: userID,
|
||||||
})
|
})
|
||||||
}()
|
})
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
if profileErr != nil {
|
if profileErr != nil {
|
||||||
writeRPCError(writer, request, profileErr)
|
httpkit.WriteRPCError(writer, request, profileErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if profileResp.GetUser() == nil {
|
if profileResp.GetUser() == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,7 +156,7 @@ func (h *Handler) getMyOverview(writer http.ResponseWriter, request *http.Reques
|
|||||||
vip := overviewVIP(vipResp, vipErr)
|
vip := overviewVIP(vipResp, vipErr)
|
||||||
profileStats := overviewStats(statsResp, statsErr)
|
profileStats := overviewStats(statsResp, statsErr)
|
||||||
|
|
||||||
writeOK(writer, request, myOverviewData{
|
httpkit.WriteOK(writer, request, myOverviewData{
|
||||||
Profile: profileData(profileResp.GetUser(), 0),
|
Profile: profileData(profileResp.GetUser(), 0),
|
||||||
ProfileStats: profileStats,
|
ProfileStats: profileStats,
|
||||||
Wallet: wallet,
|
Wallet: wallet,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package http
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
@ -201,12 +202,12 @@ func performOverviewRequest(t *testing.T, handler *Handler, requestID string) ma
|
|||||||
if recorder.Code != http.StatusOK {
|
if recorder.Code != http.StatusOK {
|
||||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||||
}
|
}
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode overview response failed: %v", err)
|
t.Fatalf("decode overview response failed: %v", err)
|
||||||
}
|
}
|
||||||
data, ok := response.Data.(map[string]any)
|
data, ok := response.Data.(map[string]any)
|
||||||
if response.Code != codeOK || !ok || response.RequestID != requestID {
|
if response.Code != httpkit.CodeOK || !ok || response.RequestID != requestID {
|
||||||
t.Fatalf("overview envelope mismatch: %+v", response)
|
t.Fatalf("overview envelope mismatch: %+v", response)
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package http
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -94,16 +95,16 @@ type userResourceData struct {
|
|||||||
|
|
||||||
func (h *Handler) listResources(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) listResources(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.walletClient == nil {
|
if h.walletClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
page, pageSize, ok := resourcePage(request)
|
page, pageSize, ok := resourcePage(request)
|
||||||
if !ok {
|
if !ok {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.walletClient.ListResources(request.Context(), &walletv1.ListResourcesRequest{
|
resp, err := h.walletClient.ListResources(request.Context(), &walletv1.ListResourcesRequest{
|
||||||
RequestId: requestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
ResourceType: strings.TrimSpace(request.URL.Query().Get("resource_type")),
|
ResourceType: strings.TrimSpace(request.URL.Query().Get("resource_type")),
|
||||||
Keyword: strings.TrimSpace(request.URL.Query().Get("keyword")),
|
Keyword: strings.TrimSpace(request.URL.Query().Get("keyword")),
|
||||||
@ -112,60 +113,60 @@ func (h *Handler) listResources(writer http.ResponseWriter, request *http.Reques
|
|||||||
ActiveOnly: true,
|
ActiveOnly: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
items := make([]resourceData, 0, len(resp.GetResources()))
|
items := make([]resourceData, 0, len(resp.GetResources()))
|
||||||
for _, item := range resp.GetResources() {
|
for _, item := range resp.GetResources() {
|
||||||
items = append(items, resourceFromProto(item))
|
items = append(items, resourceFromProto(item))
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"items": items, "total": resp.GetTotal(), "page": page, "page_size": pageSize})
|
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": resp.GetTotal(), "page": page, "page_size": pageSize})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) getResourceGroup(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) getResourceGroup(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.walletClient == nil {
|
if h.walletClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
groupID, ok := resourceIDFromPath(request.URL.Path, apiV1Prefix+"/resource-groups/")
|
groupID, ok := httpkit.PositiveInt64PathValue(request, "group_id")
|
||||||
if !ok {
|
if !ok {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.walletClient.GetResourceGroup(request.Context(), &walletv1.GetResourceGroupRequest{
|
resp, err := h.walletClient.GetResourceGroup(request.Context(), &walletv1.GetResourceGroupRequest{
|
||||||
RequestId: requestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
GroupId: groupID,
|
GroupId: groupID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
group := resourceGroupFromProto(resp.GetGroup())
|
group := resourceGroupFromProto(resp.GetGroup())
|
||||||
if group.Status != "active" {
|
if group.Status != "active" {
|
||||||
writeError(writer, request, http.StatusNotFound, codeNotFound, "not found")
|
httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(writer, request, group)
|
httpkit.WriteOK(writer, request, group)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) listGifts(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) listGifts(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.walletClient == nil {
|
if h.walletClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
page, pageSize, ok := resourcePage(request)
|
page, pageSize, ok := resourcePage(request)
|
||||||
if !ok {
|
if !ok {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
regionID, _, ok := optionalNonNegativeInt64Query(request, "region_id")
|
regionID, _, ok := optionalNonNegativeInt64Query(request, "region_id")
|
||||||
if !ok {
|
if !ok {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.walletClient.ListGiftConfigs(request.Context(), &walletv1.ListGiftConfigsRequest{
|
resp, err := h.walletClient.ListGiftConfigs(request.Context(), &walletv1.ListGiftConfigsRequest{
|
||||||
RequestId: requestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
Keyword: strings.TrimSpace(request.URL.Query().Get("keyword")),
|
Keyword: strings.TrimSpace(request.URL.Query().Get("keyword")),
|
||||||
Page: page,
|
Page: page,
|
||||||
@ -175,74 +176,74 @@ func (h *Handler) listGifts(writer http.ResponseWriter, request *http.Request) {
|
|||||||
FilterRegion: true,
|
FilterRegion: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
items := make([]giftConfigData, 0, len(resp.GetGifts()))
|
items := make([]giftConfigData, 0, len(resp.GetGifts()))
|
||||||
for _, item := range resp.GetGifts() {
|
for _, item := range resp.GetGifts() {
|
||||||
items = append(items, giftFromProto(item))
|
items = append(items, giftFromProto(item))
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"items": items, "total": resp.GetTotal(), "page": page, "page_size": pageSize})
|
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": resp.GetTotal(), "page": page, "page_size": pageSize})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) listMyResources(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) listMyResources(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.walletClient == nil {
|
if h.walletClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.walletClient.ListUserResources(request.Context(), &walletv1.ListUserResourcesRequest{
|
resp, err := h.walletClient.ListUserResources(request.Context(), &walletv1.ListUserResourcesRequest{
|
||||||
RequestId: requestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
ResourceType: strings.TrimSpace(request.URL.Query().Get("resource_type")),
|
ResourceType: strings.TrimSpace(request.URL.Query().Get("resource_type")),
|
||||||
ActiveOnly: true,
|
ActiveOnly: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
items := make([]userResourceData, 0, len(resp.GetResources()))
|
items := make([]userResourceData, 0, len(resp.GetResources()))
|
||||||
for _, item := range resp.GetResources() {
|
for _, item := range resp.GetResources() {
|
||||||
items = append(items, userResourceFromProto(item))
|
items = append(items, userResourceFromProto(item))
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"items": items, "total": len(items)})
|
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items)})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) equipMyResource(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) equipMyResource(writer http.ResponseWriter, request *http.Request) {
|
||||||
if request.Method != http.MethodPost {
|
if request.Method != http.MethodPost {
|
||||||
writeError(writer, request, http.StatusMethodNotAllowed, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusMethodNotAllowed, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if h.walletClient == nil {
|
if h.walletClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resourceID, ok := resourceIDFromActionPath(request.URL.Path, apiV1Prefix+"/users/me/resources/", "equip")
|
resourceID, ok := httpkit.PositiveInt64PathValue(request, "resource_id")
|
||||||
if !ok {
|
if !ok {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var body struct {
|
var body struct {
|
||||||
EntitlementID string `json:"entitlement_id"`
|
EntitlementID string `json:"entitlement_id"`
|
||||||
}
|
}
|
||||||
if request.Body != nil && request.Body != http.NoBody {
|
if request.Body != nil && request.Body != http.NoBody {
|
||||||
if err := decodeJSON(request.Body, &body); err != nil && !errors.Is(err, io.EOF) {
|
if err := httpkit.DecodeJSON(request.Body, &body); err != nil && !errors.Is(err, io.EOF) {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidJSON, "invalid request body")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidJSON, "invalid request body")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resp, err := h.walletClient.EquipUserResource(request.Context(), &walletv1.EquipUserResourceRequest{
|
resp, err := h.walletClient.EquipUserResource(request.Context(), &walletv1.EquipUserResourceRequest{
|
||||||
RequestId: requestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
ResourceId: resourceID,
|
ResourceId: resourceID,
|
||||||
EntitlementId: strings.TrimSpace(body.EntitlementID),
|
EntitlementId: strings.TrimSpace(body.EntitlementID),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(writer, request, userResourceFromProto(resp.GetResource()))
|
httpkit.WriteOK(writer, request, userResourceFromProto(resp.GetResource()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func resourceFromProto(item *walletv1.Resource) resourceData {
|
func resourceFromProto(item *walletv1.Resource) resourceData {
|
||||||
@ -397,22 +398,3 @@ func optionalNonNegativeInt64Query(request *http.Request, name string) (int64, b
|
|||||||
}
|
}
|
||||||
return value, true, true
|
return value, true, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func resourceIDFromPath(path string, prefix string) (int64, bool) {
|
|
||||||
raw := strings.Trim(strings.TrimPrefix(path, prefix), "/")
|
|
||||||
value, err := strconv.ParseInt(raw, 10, 64)
|
|
||||||
return value, err == nil && value > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func resourceIDFromActionPath(path string, prefix string, action string) (int64, bool) {
|
|
||||||
if !strings.HasPrefix(path, prefix) {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
raw := strings.Trim(strings.TrimPrefix(path, prefix), "/")
|
|
||||||
parts := strings.Split(raw, "/")
|
|
||||||
if len(parts) != 2 || parts[1] != action {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
value, err := strconv.ParseInt(parts[0], 10, 64)
|
|
||||||
return value, err == nil && value > 0
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,124 +0,0 @@
|
|||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"hyapp/pkg/idgen"
|
|
||||||
"hyapp/pkg/xerr"
|
|
||||||
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
)
|
|
||||||
|
|
||||||
type requestIDContextKey struct{}
|
|
||||||
|
|
||||||
const (
|
|
||||||
codeOK = "OK"
|
|
||||||
codeInvalidJSON = "INVALID_JSON"
|
|
||||||
codeInvalidArgument = "INVALID_ARGUMENT"
|
|
||||||
codeUnauthorized = "UNAUTHORIZED"
|
|
||||||
codePermissionDenied = "PERMISSION_DENIED"
|
|
||||||
codeProfileRequired = "PROFILE_REQUIRED"
|
|
||||||
codeAuthLoginBlocked = "AUTH_LOGIN_BLOCKED"
|
|
||||||
codeNotFound = "NOT_FOUND"
|
|
||||||
codeRateLimited = "RATE_LIMITED"
|
|
||||||
codeUpstreamError = "UPSTREAM_ERROR"
|
|
||||||
codeInternalError = "INTERNAL_ERROR"
|
|
||||||
)
|
|
||||||
|
|
||||||
// responseEnvelope 是 gateway 暴露给客户端的稳定 JSON 外壳。
|
|
||||||
// HTTP status 表示传输层结果,code 表示客户端可依赖的错误语义。
|
|
||||||
type responseEnvelope struct {
|
|
||||||
Code string `json:"code"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
RequestID string `json:"request_id"`
|
|
||||||
Data any `json:"data,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// withRequestID 为每个业务 API 请求建立追踪 ID。
|
|
||||||
// 客户端传入 X-Request-ID 时沿用,未传时由 gateway 生成,后续 gRPC RequestMeta 复用同一个值。
|
|
||||||
func withRequestID(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
requestID := strings.TrimSpace(request.Header.Get("X-Request-ID"))
|
|
||||||
if requestID == "" {
|
|
||||||
requestID = idgen.New("req")
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.Header().Set("X-Request-ID", requestID)
|
|
||||||
ctx := context.WithValue(request.Context(), requestIDContextKey{}, requestID)
|
|
||||||
next.ServeHTTP(writer, request.WithContext(ctx))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// requestIDFromContext 读取当前 HTTP 请求的追踪 ID。
|
|
||||||
// 理论上业务路由都会先经过 withRequestID;兜底生成值只用于防御错误调用。
|
|
||||||
func requestIDFromContext(ctx context.Context) string {
|
|
||||||
requestID, _ := ctx.Value(requestIDContextKey{}).(string)
|
|
||||||
if requestID != "" {
|
|
||||||
return requestID
|
|
||||||
}
|
|
||||||
|
|
||||||
return idgen.New("req")
|
|
||||||
}
|
|
||||||
|
|
||||||
// decodeJSON 隔离 JSON 解码细节,避免 handler 直接依赖 envelope 写法。
|
|
||||||
func decodeJSON(reader io.Reader, out any) error {
|
|
||||||
return json.NewDecoder(reader).Decode(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeOK 写成功 envelope。
|
|
||||||
func writeOK(writer http.ResponseWriter, request *http.Request, data any) {
|
|
||||||
writeEnvelope(writer, http.StatusOK, responseEnvelope{
|
|
||||||
Code: codeOK,
|
|
||||||
Message: "ok",
|
|
||||||
RequestID: responseRequestID(request),
|
|
||||||
Data: data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeError 写失败 envelope。
|
|
||||||
// 错误 message 保持稳定、短小,排障依赖 request_id 追日志,而不是把内部错误直接暴露给客户端。
|
|
||||||
func writeError(writer http.ResponseWriter, request *http.Request, statusCode int, code string, message string) {
|
|
||||||
writeEnvelope(writer, statusCode, responseEnvelope{
|
|
||||||
Code: code,
|
|
||||||
Message: message,
|
|
||||||
RequestID: responseRequestID(request),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeRPCError 把内部 gRPC status + ErrorInfo.reason 转成外部稳定 envelope。
|
|
||||||
func writeRPCError(writer http.ResponseWriter, request *http.Request, err error) {
|
|
||||||
if err == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, ok := status.FromError(err); !ok {
|
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
reason := xerr.ReasonFromGRPC(err)
|
|
||||||
statusCode, code, message := mapReasonToHTTP(reason)
|
|
||||||
writeError(writer, request, statusCode, code, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func mapReasonToHTTP(reason xerr.Code) (int, string, string) {
|
|
||||||
spec := xerr.SpecOf(reason)
|
|
||||||
return spec.HTTPStatus, spec.PublicCode, spec.PublicMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeEnvelope 是 gateway 业务 API 的唯一 JSON 响应出口。
|
|
||||||
func writeEnvelope(writer http.ResponseWriter, statusCode int, response responseEnvelope) {
|
|
||||||
writer.Header().Set("Content-Type", "application/json")
|
|
||||||
writer.WriteHeader(statusCode)
|
|
||||||
if err := json.NewEncoder(writer).Encode(response); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// responseRequestID 统一确定响应中的 request_id。
|
|
||||||
func responseRequestID(request *http.Request) string {
|
|
||||||
return requestIDFromContext(request.Context())
|
|
||||||
}
|
|
||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -1087,11 +1088,11 @@ func TestRoutesWriteUnifiedEnvelopeAndReuseRequestID(t *testing.T) {
|
|||||||
t.Fatalf("response request header mismatch: got %q", recorder.Header().Get("X-Request-ID"))
|
t.Fatalf("response request header mismatch: got %q", recorder.Header().Get("X-Request-ID"))
|
||||||
}
|
}
|
||||||
|
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode response failed: %v", err)
|
t.Fatalf("decode response failed: %v", err)
|
||||||
}
|
}
|
||||||
if response.Code != codeOK || response.Message != "ok" || response.RequestID != "req-test" {
|
if response.Code != httpkit.CodeOK || response.Message != "ok" || response.RequestID != "req-test" {
|
||||||
t.Fatalf("unexpected envelope: %+v", response)
|
t.Fatalf("unexpected envelope: %+v", response)
|
||||||
}
|
}
|
||||||
if client.lastCreate == nil || client.lastCreate.GetMeta().GetRequestId() != "req-test" {
|
if client.lastCreate == nil || client.lastCreate.GetMeta().GetRequestId() != "req-test" {
|
||||||
@ -1348,7 +1349,7 @@ func TestJoinRoomReturnsInitialDataWithIMGroupRTCTokenAndProfiles(t *testing.T)
|
|||||||
if recorder.Code != http.StatusOK {
|
if recorder.Code != http.StatusOK {
|
||||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||||
}
|
}
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode join response failed: %v", err)
|
t.Fatalf("decode join response failed: %v", err)
|
||||||
}
|
}
|
||||||
@ -1412,7 +1413,7 @@ func TestListRoomsUsesUserRegionFromUserService(t *testing.T) {
|
|||||||
if queryClient.lastList.GetTab() != "hot" || queryClient.lastList.GetLimit() != 2 || queryClient.lastList.GetCursor() != "cursor-1" || queryClient.lastList.GetQuery() != "room" {
|
if queryClient.lastList.GetTab() != "hot" || queryClient.lastList.GetLimit() != 2 || queryClient.lastList.GetCursor() != "cursor-1" || queryClient.lastList.GetQuery() != "room" {
|
||||||
t.Fatalf("ListRooms query params were not propagated: %+v", queryClient.lastList)
|
t.Fatalf("ListRooms query params were not propagated: %+v", queryClient.lastList)
|
||||||
}
|
}
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode room list response failed: %v", err)
|
t.Fatalf("decode room list response failed: %v", err)
|
||||||
}
|
}
|
||||||
@ -1556,7 +1557,7 @@ func TestGetMyRoomUsesAuthenticatedOwnerAndReturnsIMGroup(t *testing.T) {
|
|||||||
if queryClient.lastMyRoom == nil || queryClient.lastMyRoom.GetOwnerUserId() != 42 {
|
if queryClient.lastMyRoom == nil || queryClient.lastMyRoom.GetOwnerUserId() != 42 {
|
||||||
t.Fatalf("my room must use authenticated owner: %+v", queryClient.lastMyRoom)
|
t.Fatalf("my room must use authenticated owner: %+v", queryClient.lastMyRoom)
|
||||||
}
|
}
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode my room response failed: %v", err)
|
t.Fatalf("decode my room response failed: %v", err)
|
||||||
}
|
}
|
||||||
@ -1582,7 +1583,7 @@ func TestListRoomsRejectsInvalidLimitBeforeGRPC(t *testing.T) {
|
|||||||
|
|
||||||
router.ServeHTTP(recorder, request)
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
assertEnvelope(t, recorder, http.StatusBadRequest, codeInvalidArgument, "req-room-list-limit")
|
assertEnvelope(t, recorder, http.StatusBadRequest, httpkit.CodeInvalidArgument, "req-room-list-limit")
|
||||||
if queryClient.lastList != nil {
|
if queryClient.lastList != nil {
|
||||||
t.Fatalf("invalid limit must not reach room-service: %+v", queryClient.lastList)
|
t.Fatalf("invalid limit must not reach room-service: %+v", queryClient.lastList)
|
||||||
}
|
}
|
||||||
@ -1610,7 +1611,7 @@ func TestBindPushTokenUsesAuthenticatedUserAndDeviceMeta(t *testing.T) {
|
|||||||
if deviceClient.lastBind.GetPushToken() != "push-1" || deviceClient.lastBind.GetPlatform() != "android" || deviceClient.lastBind.GetProvider() != "fcm" {
|
if deviceClient.lastBind.GetPushToken() != "push-1" || deviceClient.lastBind.GetPlatform() != "android" || deviceClient.lastBind.GetProvider() != "fcm" {
|
||||||
t.Fatalf("push token bind fields mismatch: %+v", deviceClient.lastBind)
|
t.Fatalf("push token bind fields mismatch: %+v", deviceClient.lastBind)
|
||||||
}
|
}
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode bind response failed: %v", err)
|
t.Fatalf("decode bind response failed: %v", err)
|
||||||
}
|
}
|
||||||
@ -1639,7 +1640,7 @@ func TestDeletePushTokenUsesAuthenticatedUser(t *testing.T) {
|
|||||||
if deviceClient.lastDelete == nil || deviceClient.lastDelete.GetUserId() != 42 || deviceClient.lastDelete.GetDeviceId() != "dev-1" || deviceClient.lastDelete.GetPushToken() != "push-1" {
|
if deviceClient.lastDelete == nil || deviceClient.lastDelete.GetUserId() != 42 || deviceClient.lastDelete.GetDeviceId() != "dev-1" || deviceClient.lastDelete.GetPushToken() != "push-1" {
|
||||||
t.Fatalf("push token delete fields mismatch: %+v", deviceClient.lastDelete)
|
t.Fatalf("push token delete fields mismatch: %+v", deviceClient.lastDelete)
|
||||||
}
|
}
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode delete response failed: %v", err)
|
t.Fatalf("decode delete response failed: %v", err)
|
||||||
}
|
}
|
||||||
@ -1677,7 +1678,7 @@ func TestGetCurrentRoomUsesAuthenticatedUser(t *testing.T) {
|
|||||||
if queryClient.lastCurrent.GetUserId() != 42 || queryClient.lastCurrent.GetMeta().GetRequestId() != "req-current-room" {
|
if queryClient.lastCurrent.GetUserId() != 42 || queryClient.lastCurrent.GetMeta().GetRequestId() != "req-current-room" {
|
||||||
t.Fatalf("current room must use authenticated user and request meta: %+v", queryClient.lastCurrent)
|
t.Fatalf("current room must use authenticated user and request meta: %+v", queryClient.lastCurrent)
|
||||||
}
|
}
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode current room response failed: %v", err)
|
t.Fatalf("decode current room response failed: %v", err)
|
||||||
}
|
}
|
||||||
@ -1721,7 +1722,7 @@ func TestGetRoomSnapshotUsesAuthenticatedUser(t *testing.T) {
|
|||||||
if queryClient.lastSnapshot.GetViewerUserId() != 42 || queryClient.lastSnapshot.GetRoomId() != "room-snapshot" || queryClient.lastSnapshot.GetMeta().GetRequestId() != "req-room-snapshot" {
|
if queryClient.lastSnapshot.GetViewerUserId() != 42 || queryClient.lastSnapshot.GetRoomId() != "room-snapshot" || queryClient.lastSnapshot.GetMeta().GetRequestId() != "req-room-snapshot" {
|
||||||
t.Fatalf("snapshot query must use authenticated user and request meta: %+v", queryClient.lastSnapshot)
|
t.Fatalf("snapshot query must use authenticated user and request meta: %+v", queryClient.lastSnapshot)
|
||||||
}
|
}
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode snapshot response failed: %v", err)
|
t.Fatalf("decode snapshot response failed: %v", err)
|
||||||
}
|
}
|
||||||
@ -1751,7 +1752,7 @@ func TestGetRoomSnapshotRejectsInvalidRoomIDBeforeGRPC(t *testing.T) {
|
|||||||
|
|
||||||
router.ServeHTTP(recorder, request)
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
assertEnvelope(t, recorder, http.StatusBadRequest, codeInvalidArgument, "req-room-snapshot-invalid")
|
assertEnvelope(t, recorder, http.StatusBadRequest, httpkit.CodeInvalidArgument, "req-room-snapshot-invalid")
|
||||||
if queryClient.lastSnapshot != nil {
|
if queryClient.lastSnapshot != nil {
|
||||||
t.Fatalf("invalid room_id must not reach room-service: %+v", queryClient.lastSnapshot)
|
t.Fatalf("invalid room_id must not reach room-service: %+v", queryClient.lastSnapshot)
|
||||||
}
|
}
|
||||||
@ -1789,12 +1790,12 @@ func TestListRegistrationCountriesIsPublicAndReturnsCountryMetadata(t *testing.T
|
|||||||
if countryClient.last == nil || countryClient.last.GetMeta().GetRequestId() != "req-countries" {
|
if countryClient.last == nil || countryClient.last.GetMeta().GetRequestId() != "req-countries" {
|
||||||
t.Fatalf("country query request metadata mismatch: %+v", countryClient.last)
|
t.Fatalf("country query request metadata mismatch: %+v", countryClient.last)
|
||||||
}
|
}
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode response failed: %v", err)
|
t.Fatalf("decode response failed: %v", err)
|
||||||
}
|
}
|
||||||
data, ok := response.Data.(map[string]any)
|
data, ok := response.Data.(map[string]any)
|
||||||
if response.Code != codeOK || !ok {
|
if response.Code != httpkit.CodeOK || !ok {
|
||||||
t.Fatalf("unexpected envelope: %+v", response)
|
t.Fatalf("unexpected envelope: %+v", response)
|
||||||
}
|
}
|
||||||
countries, ok := data["countries"].([]any)
|
countries, ok := data["countries"].([]any)
|
||||||
@ -1825,12 +1826,12 @@ func TestListH5LinksReturnsAdminAppConfig(t *testing.T) {
|
|||||||
if recorder.Code != http.StatusOK {
|
if recorder.Code != http.StatusOK {
|
||||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||||
}
|
}
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode response failed: %v", err)
|
t.Fatalf("decode response failed: %v", err)
|
||||||
}
|
}
|
||||||
data, ok := response.Data.(map[string]any)
|
data, ok := response.Data.(map[string]any)
|
||||||
if response.Code != codeOK || !ok {
|
if response.Code != httpkit.CodeOK || !ok {
|
||||||
t.Fatalf("unexpected envelope: %+v", response)
|
t.Fatalf("unexpected envelope: %+v", response)
|
||||||
}
|
}
|
||||||
if data["total"].(float64) != 2 {
|
if data["total"].(float64) != 2 {
|
||||||
@ -1855,7 +1856,7 @@ func TestListH5LinksRequiresConfigReader(t *testing.T) {
|
|||||||
|
|
||||||
router.ServeHTTP(recorder, request)
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
assertEnvelope(t, recorder, http.StatusBadGateway, codeUpstreamError, "req-h5-links-missing")
|
assertEnvelope(t, recorder, http.StatusBadGateway, httpkit.CodeUpstreamError, "req-h5-links-missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAppBootstrapIsPublicAndLightweight(t *testing.T) {
|
func TestAppBootstrapIsPublicAndLightweight(t *testing.T) {
|
||||||
@ -1870,12 +1871,12 @@ func TestAppBootstrapIsPublicAndLightweight(t *testing.T) {
|
|||||||
if recorder.Code != http.StatusOK {
|
if recorder.Code != http.StatusOK {
|
||||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||||
}
|
}
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode response failed: %v", err)
|
t.Fatalf("decode response failed: %v", err)
|
||||||
}
|
}
|
||||||
data, ok := response.Data.(map[string]any)
|
data, ok := response.Data.(map[string]any)
|
||||||
if response.Code != codeOK || !ok {
|
if response.Code != httpkit.CodeOK || !ok {
|
||||||
t.Fatalf("unexpected envelope: %+v", response)
|
t.Fatalf("unexpected envelope: %+v", response)
|
||||||
}
|
}
|
||||||
if data["app_code"] != "lalu" || data["server_time_ms"].(float64) <= 0 || data["force_upgrade"] != false || data["maintenance"] != false {
|
if data["app_code"] != "lalu" || data["server_time_ms"].(float64) <= 0 || data["force_upgrade"] != false || data["maintenance"] != false {
|
||||||
@ -1913,12 +1914,12 @@ func TestListAppBannersReturnsAdminAppConfig(t *testing.T) {
|
|||||||
if recorder.Code != http.StatusOK {
|
if recorder.Code != http.StatusOK {
|
||||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||||
}
|
}
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode response failed: %v", err)
|
t.Fatalf("decode response failed: %v", err)
|
||||||
}
|
}
|
||||||
data, ok := response.Data.(map[string]any)
|
data, ok := response.Data.(map[string]any)
|
||||||
if response.Code != codeOK || !ok {
|
if response.Code != httpkit.CodeOK || !ok {
|
||||||
t.Fatalf("unexpected envelope: %+v", response)
|
t.Fatalf("unexpected envelope: %+v", response)
|
||||||
}
|
}
|
||||||
if data["total"].(float64) != 1 {
|
if data["total"].(float64) != 1 {
|
||||||
@ -1977,12 +1978,12 @@ func TestGetMyHostIdentityCombinesActiveHostAndBDProfiles(t *testing.T) {
|
|||||||
if hostClient.last == nil || hostClient.last.GetUserId() != 42 {
|
if hostClient.last == nil || hostClient.last.GetUserId() != 42 {
|
||||||
t.Fatalf("host identity coin seller request mismatch: %+v", hostClient.last)
|
t.Fatalf("host identity coin seller request mismatch: %+v", hostClient.last)
|
||||||
}
|
}
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode response failed: %v", err)
|
t.Fatalf("decode response failed: %v", err)
|
||||||
}
|
}
|
||||||
data, ok := response.Data.(map[string]any)
|
data, ok := response.Data.(map[string]any)
|
||||||
if response.Code != codeOK || !ok ||
|
if response.Code != httpkit.CodeOK || !ok ||
|
||||||
data["is_host"] != true ||
|
data["is_host"] != true ||
|
||||||
data["is_agent"] != true ||
|
data["is_agent"] != true ||
|
||||||
data["is_bd"] != true ||
|
data["is_bd"] != true ||
|
||||||
@ -2011,12 +2012,12 @@ func TestGetMyHostIdentityTreatsMissingProfilesAsFalse(t *testing.T) {
|
|||||||
if recorder.Code != http.StatusOK {
|
if recorder.Code != http.StatusOK {
|
||||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||||
}
|
}
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode response failed: %v", err)
|
t.Fatalf("decode response failed: %v", err)
|
||||||
}
|
}
|
||||||
data, ok := response.Data.(map[string]any)
|
data, ok := response.Data.(map[string]any)
|
||||||
if response.Code != codeOK || !ok ||
|
if response.Code != httpkit.CodeOK || !ok ||
|
||||||
data["is_host"] != false ||
|
data["is_host"] != false ||
|
||||||
data["is_agent"] != false ||
|
data["is_agent"] != false ||
|
||||||
data["is_bd"] != false ||
|
data["is_bd"] != false ||
|
||||||
@ -2037,7 +2038,7 @@ func TestConfirmMicPublishingForwardsSessionVersionAndEventTime(t *testing.T) {
|
|||||||
|
|
||||||
router.ServeHTTP(recorder, request)
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
assertEnvelope(t, recorder, http.StatusOK, codeOK, "req-confirm-mic")
|
assertEnvelope(t, recorder, http.StatusOK, httpkit.CodeOK, "req-confirm-mic")
|
||||||
if client.lastConfirmMic == nil {
|
if client.lastConfirmMic == nil {
|
||||||
t.Fatal("ConfirmMicPublishing request was not sent")
|
t.Fatal("ConfirmMicPublishing request was not sent")
|
||||||
}
|
}
|
||||||
@ -2067,11 +2068,11 @@ func TestThirdPartyLoginUsesPublicEnvelopeAndPropagatesRequestID(t *testing.T) {
|
|||||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode response failed: %v", err)
|
t.Fatalf("decode response failed: %v", err)
|
||||||
}
|
}
|
||||||
if response.Code != codeOK || response.RequestID != "req-auth-third" {
|
if response.Code != httpkit.CodeOK || response.RequestID != "req-auth-third" {
|
||||||
t.Fatalf("unexpected envelope: %+v", response)
|
t.Fatalf("unexpected envelope: %+v", response)
|
||||||
}
|
}
|
||||||
req := userClient.lastLoginThirdParty
|
req := userClient.lastLoginThirdParty
|
||||||
@ -2104,7 +2105,7 @@ func TestThirdPartyLoginRateLimitByProviderAndIP(t *testing.T) {
|
|||||||
router := handler.Routes(auth.NewVerifier("secret"))
|
router := handler.Routes(auth.NewVerifier("secret"))
|
||||||
body := []byte(`{"provider":"firebase","credential":"firebase-id-token-1","device_id":"dev-1","platform":"ios"}`)
|
body := []byte(`{"provider":"firebase","credential":"firebase-id-token-1","device_id":"dev-1","platform":"ios"}`)
|
||||||
|
|
||||||
for i := 0; i < 2; i++ {
|
for i := range 2 {
|
||||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/third-party/login", bytes.NewReader(body))
|
request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/third-party/login", bytes.NewReader(body))
|
||||||
request.Header.Set("X-Request-ID", "req-third-rate")
|
request.Header.Set("X-Request-ID", "req-third-rate")
|
||||||
request.Header.Set("X-Forwarded-For", "203.0.113.10")
|
request.Header.Set("X-Forwarded-For", "203.0.113.10")
|
||||||
@ -2116,7 +2117,7 @@ func TestThirdPartyLoginRateLimitByProviderAndIP(t *testing.T) {
|
|||||||
t.Fatalf("first request should pass: status=%d body=%s", recorder.Code, recorder.Body.String())
|
t.Fatalf("first request should pass: status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||||
}
|
}
|
||||||
if i == 1 {
|
if i == 1 {
|
||||||
assertEnvelope(t, recorder, http.StatusTooManyRequests, codeRateLimited, "req-third-rate")
|
assertEnvelope(t, recorder, http.StatusTooManyRequests, httpkit.CodeRateLimited, "req-third-rate")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if userClient.loginThirdCount != 1 {
|
if userClient.loginThirdCount != 1 {
|
||||||
@ -2133,7 +2134,7 @@ func TestAccountLoginRateLimitByAccount(t *testing.T) {
|
|||||||
router := handler.Routes(auth.NewVerifier("secret"))
|
router := handler.Routes(auth.NewVerifier("secret"))
|
||||||
body := []byte(`{"account":"100001","password":"bad","device_id":"dev-1"}`)
|
body := []byte(`{"account":"100001","password":"bad","device_id":"dev-1"}`)
|
||||||
|
|
||||||
for i := 0; i < 2; i++ {
|
for i := range 2 {
|
||||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/account/login", bytes.NewReader(body))
|
request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/account/login", bytes.NewReader(body))
|
||||||
request.Header.Set("X-Request-ID", "req-password-rate")
|
request.Header.Set("X-Request-ID", "req-password-rate")
|
||||||
request.Header.Set("X-Forwarded-For", "203.0.113.20")
|
request.Header.Set("X-Forwarded-For", "203.0.113.20")
|
||||||
@ -2145,7 +2146,7 @@ func TestAccountLoginRateLimitByAccount(t *testing.T) {
|
|||||||
t.Fatalf("first request should pass: status=%d body=%s", recorder.Code, recorder.Body.String())
|
t.Fatalf("first request should pass: status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||||
}
|
}
|
||||||
if i == 1 {
|
if i == 1 {
|
||||||
assertEnvelope(t, recorder, http.StatusTooManyRequests, codeRateLimited, "req-password-rate")
|
assertEnvelope(t, recorder, http.StatusTooManyRequests, httpkit.CodeRateLimited, "req-password-rate")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if userClient.loginPasswordCount != 1 {
|
if userClient.loginPasswordCount != 1 {
|
||||||
@ -2163,7 +2164,7 @@ func TestAccountLoginAcceptsAccountField(t *testing.T) {
|
|||||||
|
|
||||||
router.ServeHTTP(recorder, request)
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
assertEnvelope(t, recorder, http.StatusOK, codeOK, "req-account-login")
|
assertEnvelope(t, recorder, http.StatusOK, httpkit.CodeOK, "req-account-login")
|
||||||
if userClient.lastLoginPassword == nil || userClient.lastLoginPassword.GetDisplayUserId() != "123456" {
|
if userClient.lastLoginPassword == nil || userClient.lastLoginPassword.GetDisplayUserId() != "123456" {
|
||||||
t.Fatalf("account was not propagated as display_user_id: %+v", userClient.lastLoginPassword)
|
t.Fatalf("account was not propagated as display_user_id: %+v", userClient.lastLoginPassword)
|
||||||
}
|
}
|
||||||
@ -2193,7 +2194,7 @@ func TestLoginRiskFastGuardBlocksLanguage(t *testing.T) {
|
|||||||
|
|
||||||
router.ServeHTTP(recorder, request)
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
assertEnvelope(t, recorder, http.StatusForbidden, codeAuthLoginBlocked, "req-login-risk-language")
|
assertEnvelope(t, recorder, http.StatusForbidden, httpkit.CodeAuthLoginBlocked, "req-login-risk-language")
|
||||||
if userClient.loginPasswordCount != 0 {
|
if userClient.loginPasswordCount != 0 {
|
||||||
t.Fatalf("blocked login must not reach LoginPassword, count=%d", userClient.loginPasswordCount)
|
t.Fatalf("blocked login must not reach LoginPassword, count=%d", userClient.loginPasswordCount)
|
||||||
}
|
}
|
||||||
@ -2217,7 +2218,7 @@ func TestLoginRiskIPCacheBlocksLogin(t *testing.T) {
|
|||||||
|
|
||||||
router.ServeHTTP(recorder, request)
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
assertEnvelope(t, recorder, http.StatusForbidden, codeAuthLoginBlocked, "req-login-risk-cache")
|
assertEnvelope(t, recorder, http.StatusForbidden, httpkit.CodeAuthLoginBlocked, "req-login-risk-cache")
|
||||||
if userClient.loginThirdCount != 0 {
|
if userClient.loginThirdCount != 0 {
|
||||||
t.Fatalf("blocked login must not reach LoginThirdParty, count=%d", userClient.loginThirdCount)
|
t.Fatalf("blocked login must not reach LoginThirdParty, count=%d", userClient.loginThirdCount)
|
||||||
}
|
}
|
||||||
@ -2251,7 +2252,7 @@ func TestRefreshRateLimitByDeviceID(t *testing.T) {
|
|||||||
router := handler.Routes(auth.NewVerifier("secret"))
|
router := handler.Routes(auth.NewVerifier("secret"))
|
||||||
body := []byte(`{"refresh_token":"refresh-1","device_id":"dev-1"}`)
|
body := []byte(`{"refresh_token":"refresh-1","device_id":"dev-1"}`)
|
||||||
|
|
||||||
for i := 0; i < 2; i++ {
|
for i := range 2 {
|
||||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/token/refresh", bytes.NewReader(body))
|
request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/token/refresh", bytes.NewReader(body))
|
||||||
request.Header.Set("X-Request-ID", "req-refresh-rate")
|
request.Header.Set("X-Request-ID", "req-refresh-rate")
|
||||||
request.Header.Set("X-Forwarded-For", "203.0.113.30")
|
request.Header.Set("X-Forwarded-For", "203.0.113.30")
|
||||||
@ -2263,7 +2264,7 @@ func TestRefreshRateLimitByDeviceID(t *testing.T) {
|
|||||||
t.Fatalf("first request should pass: status=%d body=%s", recorder.Code, recorder.Body.String())
|
t.Fatalf("first request should pass: status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||||
}
|
}
|
||||||
if i == 1 {
|
if i == 1 {
|
||||||
assertEnvelope(t, recorder, http.StatusTooManyRequests, codeRateLimited, "req-refresh-rate")
|
assertEnvelope(t, recorder, http.StatusTooManyRequests, httpkit.CodeRateLimited, "req-refresh-rate")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if userClient.refreshCount != 1 {
|
if userClient.refreshCount != 1 {
|
||||||
@ -2280,7 +2281,7 @@ func TestLogoutRateLimitBySessionIDAndIP(t *testing.T) {
|
|||||||
router := handler.Routes(auth.NewVerifier("secret"))
|
router := handler.Routes(auth.NewVerifier("secret"))
|
||||||
body := []byte(`{"session_id":"sess-1","refresh_token":"refresh-1"}`)
|
body := []byte(`{"session_id":"sess-1","refresh_token":"refresh-1"}`)
|
||||||
|
|
||||||
for i := 0; i < 2; i++ {
|
for i := range 2 {
|
||||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/logout", bytes.NewReader(body))
|
request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/logout", bytes.NewReader(body))
|
||||||
request.Header.Set("X-Request-ID", "req-logout-rate")
|
request.Header.Set("X-Request-ID", "req-logout-rate")
|
||||||
request.Header.Set("X-Forwarded-For", "203.0.113.40")
|
request.Header.Set("X-Forwarded-For", "203.0.113.40")
|
||||||
@ -2292,7 +2293,7 @@ func TestLogoutRateLimitBySessionIDAndIP(t *testing.T) {
|
|||||||
t.Fatalf("first request should pass: status=%d body=%s", recorder.Code, recorder.Body.String())
|
t.Fatalf("first request should pass: status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||||
}
|
}
|
||||||
if i == 1 {
|
if i == 1 {
|
||||||
assertEnvelope(t, recorder, http.StatusTooManyRequests, codeRateLimited, "req-logout-rate")
|
assertEnvelope(t, recorder, http.StatusTooManyRequests, httpkit.CodeRateLimited, "req-logout-rate")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if userClient.logoutCount != 1 {
|
if userClient.logoutCount != 1 {
|
||||||
@ -2360,12 +2361,12 @@ func TestGetMyProfileUsesAuthenticatedUserID(t *testing.T) {
|
|||||||
if profileClient.lastGet == nil || profileClient.lastGet.GetUserId() != 42 {
|
if profileClient.lastGet == nil || profileClient.lastGet.GetUserId() != 42 {
|
||||||
t.Fatalf("authenticated user_id was not propagated: %+v", profileClient.lastGet)
|
t.Fatalf("authenticated user_id was not propagated: %+v", profileClient.lastGet)
|
||||||
}
|
}
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode response failed: %v", err)
|
t.Fatalf("decode response failed: %v", err)
|
||||||
}
|
}
|
||||||
data, ok := response.Data.(map[string]any)
|
data, ok := response.Data.(map[string]any)
|
||||||
if response.Code != codeOK || !ok || data["gender"] != "female" {
|
if response.Code != httpkit.CodeOK || !ok || data["gender"] != "female" {
|
||||||
t.Fatalf("profile response mismatch: %+v", response)
|
t.Fatalf("profile response mismatch: %+v", response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2414,12 +2415,12 @@ func TestMessageTabsAndInboxRoutesUseAuthenticatedUser(t *testing.T) {
|
|||||||
if messageClient.lastList == nil || messageClient.lastList.GetUserId() != 42 || messageClient.lastList.GetSection() != "system" || messageClient.lastList.GetPageSize() != 20 || messageClient.lastList.GetPageToken() != "cursor-1" {
|
if messageClient.lastList == nil || messageClient.lastList.GetUserId() != 42 || messageClient.lastList.GetSection() != "system" || messageClient.lastList.GetPageSize() != 20 || messageClient.lastList.GetPageToken() != "cursor-1" {
|
||||||
t.Fatalf("message list request mismatch: %+v", messageClient.lastList)
|
t.Fatalf("message list request mismatch: %+v", messageClient.lastList)
|
||||||
}
|
}
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode list response failed: %v", err)
|
t.Fatalf("decode list response failed: %v", err)
|
||||||
}
|
}
|
||||||
data, ok := response.Data.(map[string]any)
|
data, ok := response.Data.(map[string]any)
|
||||||
if response.Code != codeOK || !ok || data["section"] != "system" || data["next_page_token"] != "cursor-2" {
|
if response.Code != httpkit.CodeOK || !ok || data["section"] != "system" || data["next_page_token"] != "cursor-2" {
|
||||||
t.Fatalf("message list envelope mismatch: %+v", response)
|
t.Fatalf("message list envelope mismatch: %+v", response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2489,12 +2490,12 @@ func TestTaskTabsAndClaimRoutesUseAuthenticatedUser(t *testing.T) {
|
|||||||
if taskClient.lastClaim == nil || taskClient.lastClaim.GetUserId() != 42 || taskClient.lastClaim.GetTaskId() != "daily-send-message" || taskClient.lastClaim.GetTaskType() != "daily" || taskClient.lastClaim.GetTaskDay() != "2026-05-09" || taskClient.lastClaim.GetCommandId() != "cmd-task-claim" {
|
if taskClient.lastClaim == nil || taskClient.lastClaim.GetUserId() != 42 || taskClient.lastClaim.GetTaskId() != "daily-send-message" || taskClient.lastClaim.GetTaskType() != "daily" || taskClient.lastClaim.GetTaskDay() != "2026-05-09" || taskClient.lastClaim.GetCommandId() != "cmd-task-claim" {
|
||||||
t.Fatalf("task claim request mismatch: %+v", taskClient.lastClaim)
|
t.Fatalf("task claim request mismatch: %+v", taskClient.lastClaim)
|
||||||
}
|
}
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode task claim response failed: %v", err)
|
t.Fatalf("decode task claim response failed: %v", err)
|
||||||
}
|
}
|
||||||
data, ok := response.Data.(map[string]any)
|
data, ok := response.Data.(map[string]any)
|
||||||
if response.Code != codeOK || !ok || data["task_id"] != "daily-send-message" || data["status"] != "granted" || data["wallet_transaction_id"] != "wtx-1" {
|
if response.Code != httpkit.CodeOK || !ok || data["task_id"] != "daily-send-message" || data["status"] != "granted" || data["wallet_transaction_id"] != "wtx-1" {
|
||||||
t.Fatalf("task claim envelope mismatch: %+v", response)
|
t.Fatalf("task claim envelope mismatch: %+v", response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2549,13 +2550,13 @@ func TestBatchUserProfilesReturnsOnlySafeDisplayFields(t *testing.T) {
|
|||||||
if profileClient.lastBatch == nil || profileClient.lastBatch.GetMeta().GetRequestId() != "req-profile-batch" || len(profileClient.lastBatch.GetUserIds()) != 2 {
|
if profileClient.lastBatch == nil || profileClient.lastBatch.GetMeta().GetRequestId() != "req-profile-batch" || len(profileClient.lastBatch.GetUserIds()) != 2 {
|
||||||
t.Fatalf("batch user request mismatch: %+v", profileClient.lastBatch)
|
t.Fatalf("batch user request mismatch: %+v", profileClient.lastBatch)
|
||||||
}
|
}
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode profile batch failed: %v", err)
|
t.Fatalf("decode profile batch failed: %v", err)
|
||||||
}
|
}
|
||||||
data, ok := response.Data.(map[string]any)
|
data, ok := response.Data.(map[string]any)
|
||||||
profiles, profilesOK := data["profiles"].([]any)
|
profiles, profilesOK := data["profiles"].([]any)
|
||||||
if response.Code != codeOK || !ok || !profilesOK || len(profiles) != 2 {
|
if response.Code != httpkit.CodeOK || !ok || !profilesOK || len(profiles) != 2 {
|
||||||
t.Fatalf("profile batch response mismatch: %+v", response)
|
t.Fatalf("profile batch response mismatch: %+v", response)
|
||||||
}
|
}
|
||||||
first, ok := profiles[0].(map[string]any)
|
first, ok := profiles[0].(map[string]any)
|
||||||
@ -2590,12 +2591,12 @@ func TestGetMyBalancesUsesAuthenticatedUserIDAndDefaultsCoin(t *testing.T) {
|
|||||||
if len(walletClient.last.GetAssetTypes()) != 1 || walletClient.last.GetAssetTypes()[0] != "COIN" {
|
if len(walletClient.last.GetAssetTypes()) != 1 || walletClient.last.GetAssetTypes()[0] != "COIN" {
|
||||||
t.Fatalf("wallet default asset types mismatch: %+v", walletClient.last)
|
t.Fatalf("wallet default asset types mismatch: %+v", walletClient.last)
|
||||||
}
|
}
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode response failed: %v", err)
|
t.Fatalf("decode response failed: %v", err)
|
||||||
}
|
}
|
||||||
data, ok := response.Data.(map[string]any)
|
data, ok := response.Data.(map[string]any)
|
||||||
if response.Code != codeOK || !ok {
|
if response.Code != httpkit.CodeOK || !ok {
|
||||||
t.Fatalf("unexpected envelope: %+v", response)
|
t.Fatalf("unexpected envelope: %+v", response)
|
||||||
}
|
}
|
||||||
balances, ok := data["balances"].([]any)
|
balances, ok := data["balances"].([]any)
|
||||||
@ -2661,12 +2662,12 @@ func TestCoinSellerTransferChecksIdentityAndPropagatesWalletCommand(t *testing.T
|
|||||||
if len(profileClient.getRequests) != 2 || profileClient.getRequests[0].GetUserId() != 42 || profileClient.getRequests[1].GetUserId() != 900002 {
|
if len(profileClient.getRequests) != 2 || profileClient.getRequests[0].GetUserId() != 42 || profileClient.getRequests[1].GetUserId() != 900002 {
|
||||||
t.Fatalf("coin seller region lookup mismatch: %+v", profileClient.getRequests)
|
t.Fatalf("coin seller region lookup mismatch: %+v", profileClient.getRequests)
|
||||||
}
|
}
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode response failed: %v", err)
|
t.Fatalf("decode response failed: %v", err)
|
||||||
}
|
}
|
||||||
data, ok := response.Data.(map[string]any)
|
data, ok := response.Data.(map[string]any)
|
||||||
if response.Code != codeOK || !ok || data["transaction_id"] != "wtx-coin-seller" || data["seller_balance_after"] != float64(80000) || data["recharge_usd_minor"] != float64(100) {
|
if response.Code != httpkit.CodeOK || !ok || data["transaction_id"] != "wtx-coin-seller" || data["seller_balance_after"] != float64(80000) || data["recharge_usd_minor"] != float64(100) {
|
||||||
t.Fatalf("coin seller transfer response mismatch: %+v", response)
|
t.Fatalf("coin seller transfer response mismatch: %+v", response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2779,12 +2780,12 @@ func TestCompleteOnboardingAllowsIncompleteProfileAndUsesAuthenticatedUserID(t *
|
|||||||
t.Fatalf("complete onboarding must propagate current session_id: %+v", profileClient.lastComplete.GetMeta())
|
t.Fatalf("complete onboarding must propagate current session_id: %+v", profileClient.lastComplete.GetMeta())
|
||||||
}
|
}
|
||||||
|
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode response failed: %v", err)
|
t.Fatalf("decode response failed: %v", err)
|
||||||
}
|
}
|
||||||
data, ok := response.Data.(map[string]any)
|
data, ok := response.Data.(map[string]any)
|
||||||
if response.Code != codeOK || !ok || data["profile_completed"] != true || data["onboarding_status"] != "completed" {
|
if response.Code != httpkit.CodeOK || !ok || data["profile_completed"] != true || data["onboarding_status"] != "completed" {
|
||||||
t.Fatalf("unexpected onboarding response: %+v", response)
|
t.Fatalf("unexpected onboarding response: %+v", response)
|
||||||
}
|
}
|
||||||
token, ok := data["token"].(map[string]any)
|
token, ok := data["token"].(map[string]any)
|
||||||
@ -2818,7 +2819,7 @@ func TestProfileMutationEndpointsRequireCompletedProfileToken(t *testing.T) {
|
|||||||
|
|
||||||
router.ServeHTTP(recorder, request)
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
assertEnvelope(t, recorder, http.StatusForbidden, codeProfileRequired, "req-profile-mutation")
|
assertEnvelope(t, recorder, http.StatusForbidden, httpkit.CodeProfileRequired, "req-profile-mutation")
|
||||||
if profileClient.lastUpdate != nil || profileClient.lastCountry != nil {
|
if profileClient.lastUpdate != nil || profileClient.lastCountry != nil {
|
||||||
t.Fatalf("profile gate must stop mutation before user-service: update=%+v country=%+v", profileClient.lastUpdate, profileClient.lastCountry)
|
t.Fatalf("profile gate must stop mutation before user-service: update=%+v country=%+v", profileClient.lastUpdate, profileClient.lastCountry)
|
||||||
}
|
}
|
||||||
@ -2866,7 +2867,7 @@ func TestProfileGateRejectsIncompleteUsersForRoomIMRTCPaidCapabilities(t *testin
|
|||||||
|
|
||||||
router.ServeHTTP(recorder, request)
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
assertEnvelope(t, recorder, http.StatusForbidden, codeProfileRequired, "req-profile-required")
|
assertEnvelope(t, recorder, http.StatusForbidden, httpkit.CodeProfileRequired, "req-profile-required")
|
||||||
if roomClient.lastCreate != nil || roomClient.lastJoin != nil || roomClient.lastHeartbeat != nil || roomClient.lastGift != nil {
|
if roomClient.lastCreate != nil || roomClient.lastJoin != nil || roomClient.lastHeartbeat != nil || roomClient.lastGift != nil {
|
||||||
t.Fatalf("profile gate must stop protected request before room-service: %+v", roomClient)
|
t.Fatalf("profile gate must stop protected request before room-service: %+v", roomClient)
|
||||||
}
|
}
|
||||||
@ -2910,7 +2911,7 @@ func TestVoiceRoomRoutesRejectWrongHTTPMethod(t *testing.T) {
|
|||||||
|
|
||||||
router.ServeHTTP(recorder, request)
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
assertEnvelope(t, recorder, http.StatusMethodNotAllowed, codeInvalidArgument, "req-method-guard")
|
assertEnvelope(t, recorder, http.StatusMethodNotAllowed, httpkit.CodeInvalidArgument, "req-method-guard")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2937,12 +2938,12 @@ func TestUploadAvatarAllowsIncompleteProfileAndWritesObject(t *testing.T) {
|
|||||||
if recorder.Code != http.StatusOK {
|
if recorder.Code != http.StatusOK {
|
||||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||||
}
|
}
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode upload response failed: %v", err)
|
t.Fatalf("decode upload response failed: %v", err)
|
||||||
}
|
}
|
||||||
data, ok := response.Data.(map[string]any)
|
data, ok := response.Data.(map[string]any)
|
||||||
if response.Code != codeOK || !ok {
|
if response.Code != httpkit.CodeOK || !ok {
|
||||||
t.Fatalf("unexpected upload envelope: %+v", response)
|
t.Fatalf("unexpected upload envelope: %+v", response)
|
||||||
}
|
}
|
||||||
if !strings.HasPrefix(uploader.lastKey, "app/avatars/42/") || !strings.HasSuffix(uploader.lastKey, ".png") {
|
if !strings.HasPrefix(uploader.lastKey, "app/avatars/42/") || !strings.HasSuffix(uploader.lastKey, ".png") {
|
||||||
@ -2970,7 +2971,7 @@ func TestUploadAvatarRejectsSpoofedImageContentType(t *testing.T) {
|
|||||||
|
|
||||||
router.ServeHTTP(recorder, request)
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
assertEnvelope(t, recorder, http.StatusBadRequest, codeInvalidArgument, "req-upload-avatar-invalid")
|
assertEnvelope(t, recorder, http.StatusBadRequest, httpkit.CodeInvalidArgument, "req-upload-avatar-invalid")
|
||||||
if uploader.lastKey != "" {
|
if uploader.lastKey != "" {
|
||||||
t.Fatalf("invalid avatar must not be uploaded: %+v", uploader)
|
t.Fatalf("invalid avatar must not be uploaded: %+v", uploader)
|
||||||
}
|
}
|
||||||
@ -2987,7 +2988,7 @@ func TestUploadFileRequiresObjectUploader(t *testing.T) {
|
|||||||
|
|
||||||
router.ServeHTTP(recorder, request)
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
assertEnvelope(t, recorder, http.StatusBadGateway, codeUpstreamError, "req-upload-disabled")
|
assertEnvelope(t, recorder, http.StatusBadGateway, httpkit.CodeUpstreamError, "req-upload-disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthLoginMapsGRPCReason(t *testing.T) {
|
func TestAuthLoginMapsGRPCReason(t *testing.T) {
|
||||||
@ -3005,7 +3006,7 @@ func TestAuthLoginMapsGRPCReason(t *testing.T) {
|
|||||||
|
|
||||||
func TestMapReasonToHTTPMapsGenericConflict(t *testing.T) {
|
func TestMapReasonToHTTPMapsGenericConflict(t *testing.T) {
|
||||||
// room-service 的麦位占用、幂等 payload 冲突等通用业务冲突必须稳定透出 409。
|
// room-service 的麦位占用、幂等 payload 冲突等通用业务冲突必须稳定透出 409。
|
||||||
statusCode, code, message := mapReasonToHTTP(xerr.Conflict)
|
statusCode, code, message := httpkit.MapReasonToHTTP(xerr.Conflict)
|
||||||
if statusCode != http.StatusConflict || code != string(xerr.Conflict) || message != "conflict" {
|
if statusCode != http.StatusConflict || code != string(xerr.Conflict) || message != "conflict" {
|
||||||
t.Fatalf("generic conflict mapping mismatch: status=%d code=%s message=%s", statusCode, code, message)
|
t.Fatalf("generic conflict mapping mismatch: status=%d code=%s message=%s", statusCode, code, message)
|
||||||
}
|
}
|
||||||
@ -3019,7 +3020,7 @@ func TestRoutesWriteUnauthorizedEnvelope(t *testing.T) {
|
|||||||
|
|
||||||
router.ServeHTTP(recorder, request)
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
assertEnvelope(t, recorder, http.StatusUnauthorized, codeUnauthorized, "req-auth")
|
assertEnvelope(t, recorder, http.StatusUnauthorized, httpkit.CodeUnauthorized, "req-auth")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRoutesWriteInvalidJSONEnvelope(t *testing.T) {
|
func TestRoutesWriteInvalidJSONEnvelope(t *testing.T) {
|
||||||
@ -3031,7 +3032,7 @@ func TestRoutesWriteInvalidJSONEnvelope(t *testing.T) {
|
|||||||
|
|
||||||
router.ServeHTTP(recorder, request)
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
assertEnvelope(t, recorder, http.StatusBadRequest, codeInvalidJSON, "req-json")
|
assertEnvelope(t, recorder, http.StatusBadRequest, httpkit.CodeInvalidJSON, "req-json")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateRoomIgnoresClientRoomIDAndGeneratesScopedRoomID(t *testing.T) {
|
func TestCreateRoomIgnoresClientRoomIDAndGeneratesScopedRoomID(t *testing.T) {
|
||||||
@ -3098,7 +3099,7 @@ func TestUpdateRoomProfileForwardsOptionalFields(t *testing.T) {
|
|||||||
t.Fatalf("UpdateRoomProfile optional fields mismatch: %+v", req)
|
t.Fatalf("UpdateRoomProfile optional fields mismatch: %+v", req)
|
||||||
}
|
}
|
||||||
|
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode response failed: %v", err)
|
t.Fatalf("decode response failed: %v", err)
|
||||||
}
|
}
|
||||||
@ -3143,7 +3144,7 @@ func TestCreateRoomRejectsMissingRequiredFieldsBeforeGRPC(t *testing.T) {
|
|||||||
|
|
||||||
router.ServeHTTP(recorder, request)
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
assertEnvelope(t, recorder, http.StatusBadRequest, codeInvalidArgument, "req-create-required-"+test.name)
|
assertEnvelope(t, recorder, http.StatusBadRequest, httpkit.CodeInvalidArgument, "req-create-required-"+test.name)
|
||||||
if client.lastCreate != nil {
|
if client.lastCreate != nil {
|
||||||
t.Fatalf("missing required create field must not be forwarded: %+v", client.lastCreate)
|
t.Fatalf("missing required create field must not be forwarded: %+v", client.lastCreate)
|
||||||
}
|
}
|
||||||
@ -3161,7 +3162,7 @@ func TestRoutesWriteUpstreamErrorEnvelope(t *testing.T) {
|
|||||||
|
|
||||||
router.ServeHTTP(recorder, request)
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
assertEnvelope(t, recorder, http.StatusBadGateway, codeUpstreamError, "req-upstream")
|
assertEnvelope(t, recorder, http.StatusBadGateway, httpkit.CodeUpstreamError, "req-upstream")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateRoomOwnerConflictWritesConflictEnvelope(t *testing.T) {
|
func TestCreateRoomOwnerConflictWritesConflictEnvelope(t *testing.T) {
|
||||||
@ -3213,12 +3214,12 @@ func TestTencentIMUserSigUsesAuthenticatedUserIDAndFailsClosed(t *testing.T) {
|
|||||||
if recorder.Code != http.StatusOK {
|
if recorder.Code != http.StatusOK {
|
||||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||||
}
|
}
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode response failed: %v", err)
|
t.Fatalf("decode response failed: %v", err)
|
||||||
}
|
}
|
||||||
data, ok := response.Data.(map[string]any)
|
data, ok := response.Data.(map[string]any)
|
||||||
if response.Code != codeOK || !ok || data["user_id"] != "42" || data["user_sig"] == "" {
|
if response.Code != httpkit.CodeOK || !ok || data["user_id"] != "42" || data["user_sig"] == "" {
|
||||||
t.Fatalf("unexpected usersig response: %+v", response)
|
t.Fatalf("unexpected usersig response: %+v", response)
|
||||||
}
|
}
|
||||||
groups, ok := data["join_groups"].([]any)
|
groups, ok := data["join_groups"].([]any)
|
||||||
@ -3240,7 +3241,7 @@ func TestTencentIMUserSigUsesAuthenticatedUserIDAndFailsClosed(t *testing.T) {
|
|||||||
missingConfigRecorder := httptest.NewRecorder()
|
missingConfigRecorder := httptest.NewRecorder()
|
||||||
missingConfigRouter.ServeHTTP(missingConfigRecorder, missingConfigRequest)
|
missingConfigRouter.ServeHTTP(missingConfigRecorder, missingConfigRequest)
|
||||||
|
|
||||||
assertEnvelope(t, missingConfigRecorder, http.StatusInternalServerError, codeInternalError, "req-usersig-missing")
|
assertEnvelope(t, missingConfigRecorder, http.StatusInternalServerError, httpkit.CodeInternalError, "req-usersig-missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTencentRTCTokenUsesPresenceGuardAndInternalUserID(t *testing.T) {
|
func TestTencentRTCTokenUsesPresenceGuardAndInternalUserID(t *testing.T) {
|
||||||
@ -3269,12 +3270,12 @@ func TestTencentRTCTokenUsesPresenceGuardAndInternalUserID(t *testing.T) {
|
|||||||
if recorder.Code != http.StatusOK {
|
if recorder.Code != http.StatusOK {
|
||||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||||
}
|
}
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode response failed: %v", err)
|
t.Fatalf("decode response failed: %v", err)
|
||||||
}
|
}
|
||||||
data, ok := response.Data.(map[string]any)
|
data, ok := response.Data.(map[string]any)
|
||||||
if response.Code != codeOK || !ok {
|
if response.Code != httpkit.CodeOK || !ok {
|
||||||
t.Fatalf("unexpected envelope: %+v", response)
|
t.Fatalf("unexpected envelope: %+v", response)
|
||||||
}
|
}
|
||||||
if data["user_id"] != "42" || data["rtc_user_id"] != "42" || data["room_id"] != "room_1001" || data["str_room_id"] != "room_1001" {
|
if data["user_id"] != "42" || data["rtc_user_id"] != "42" || data["room_id"] != "room_1001" || data["str_room_id"] != "room_1001" {
|
||||||
@ -3310,7 +3311,7 @@ func TestTencentRTCTokenRejectsInvalidRoomIDBeforeGuard(t *testing.T) {
|
|||||||
|
|
||||||
router.ServeHTTP(recorder, request)
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
assertEnvelope(t, recorder, http.StatusBadRequest, codeInvalidArgument, "req-rtc-invalid")
|
assertEnvelope(t, recorder, http.StatusBadRequest, httpkit.CodeInvalidArgument, "req-rtc-invalid")
|
||||||
if guard.lastPresence != nil {
|
if guard.lastPresence != nil {
|
||||||
t.Fatalf("invalid room_id must not call presence guard: %+v", guard.lastPresence)
|
t.Fatalf("invalid room_id must not call presence guard: %+v", guard.lastPresence)
|
||||||
}
|
}
|
||||||
@ -3323,9 +3324,9 @@ func TestTencentRTCTokenMapsPresenceDenialReasons(t *testing.T) {
|
|||||||
statusCode int
|
statusCode int
|
||||||
code string
|
code string
|
||||||
}{
|
}{
|
||||||
{name: "not_in_room", reason: "not_in_room", statusCode: http.StatusForbidden, code: codePermissionDenied},
|
{name: "not_in_room", reason: "not_in_room", statusCode: http.StatusForbidden, code: httpkit.CodePermissionDenied},
|
||||||
{name: "user_banned", reason: "user_banned", statusCode: http.StatusForbidden, code: codePermissionDenied},
|
{name: "user_banned", reason: "user_banned", statusCode: http.StatusForbidden, code: httpkit.CodePermissionDenied},
|
||||||
{name: "room_not_found", reason: "room_not_found", statusCode: http.StatusNotFound, code: codeNotFound},
|
{name: "room_not_found", reason: "room_not_found", statusCode: http.StatusNotFound, code: httpkit.CodeNotFound},
|
||||||
{name: "room_closed", reason: "room_closed", statusCode: http.StatusConflict, code: string(xerr.RoomClosed)},
|
{name: "room_closed", reason: "room_closed", statusCode: http.StatusConflict, code: string(xerr.RoomClosed)},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3372,7 +3373,7 @@ func TestTencentRTCTokenFailsClosedForConfigAndUpstreamErrors(t *testing.T) {
|
|||||||
|
|
||||||
missingConfigRouter.ServeHTTP(missingConfigRecorder, missingConfigRequest)
|
missingConfigRouter.ServeHTTP(missingConfigRecorder, missingConfigRequest)
|
||||||
|
|
||||||
assertEnvelope(t, missingConfigRecorder, http.StatusInternalServerError, codeInternalError, "req-rtc-config")
|
assertEnvelope(t, missingConfigRecorder, http.StatusInternalServerError, httpkit.CodeInternalError, "req-rtc-config")
|
||||||
if missingConfigGuard.lastPresence != nil {
|
if missingConfigGuard.lastPresence != nil {
|
||||||
t.Fatalf("missing rtc config must fail before room-service guard: %+v", missingConfigGuard.lastPresence)
|
t.Fatalf("missing rtc config must fail before room-service guard: %+v", missingConfigGuard.lastPresence)
|
||||||
}
|
}
|
||||||
@ -3395,7 +3396,7 @@ func TestTencentRTCTokenFailsClosedForConfigAndUpstreamErrors(t *testing.T) {
|
|||||||
|
|
||||||
upstreamRouter.ServeHTTP(upstreamRecorder, upstreamRequest)
|
upstreamRouter.ServeHTTP(upstreamRecorder, upstreamRequest)
|
||||||
|
|
||||||
assertEnvelope(t, upstreamRecorder, http.StatusBadGateway, codeUpstreamError, "req-rtc-upstream")
|
assertEnvelope(t, upstreamRecorder, http.StatusBadGateway, httpkit.CodeUpstreamError, "req-rtc-upstream")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTencentRTCCallbackVerifiesSignatureAndAppliesEvent(t *testing.T) {
|
func TestTencentRTCCallbackVerifiesSignatureAndAppliesEvent(t *testing.T) {
|
||||||
@ -3575,7 +3576,7 @@ func assertEnvelope(t *testing.T, recorder *httptest.ResponseRecorder, statusCod
|
|||||||
t.Fatalf("status mismatch: got %d want %d body=%s", recorder.Code, statusCode, recorder.Body.String())
|
t.Fatalf("status mismatch: got %d want %d body=%s", recorder.Code, statusCode, recorder.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode response failed: %v", err)
|
t.Fatalf("decode response failed: %v", err)
|
||||||
}
|
}
|
||||||
@ -3591,7 +3592,7 @@ func assertEnvelopeMessage(t *testing.T, recorder *httptest.ResponseRecorder, st
|
|||||||
t.Fatalf("status mismatch: got %d want %d body=%s", recorder.Code, statusCode, recorder.Body.String())
|
t.Fatalf("status mismatch: got %d want %d body=%s", recorder.Code, statusCode, recorder.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
var response responseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode response failed: %v", err)
|
t.Fatalf("decode response failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package http
|
|||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -32,18 +33,18 @@ type roomFeedRelationCursor struct {
|
|||||||
func (h *Handler) listRooms(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) listRooms(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.userProfileClient == nil || h.roomQueryClient == nil {
|
if h.userProfileClient == nil || h.roomQueryClient == nil {
|
||||||
// 列表入口必须同时依赖 user-service 区域归属和 room-service 读模型。
|
// 列表入口必须同时依赖 user-service 区域归属和 room-service 读模型。
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
limit, ok := parseRoomListLimit(request.URL.Query().Get("limit"))
|
limit, ok := parseRoomListLimit(request.URL.Query().Get("limit"))
|
||||||
if !ok {
|
if !ok {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tab, ok := parsePublicRoomListTab(request.URL.Query().Get("tab"))
|
tab, ok := parsePublicRoomListTab(request.URL.Query().Get("tab"))
|
||||||
if !ok {
|
if !ok {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,7 +55,7 @@ func (h *Handler) listRooms(writer http.ResponseWriter, request *http.Request) {
|
|||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// user-service 是 region_id 的唯一 owner,查询失败不能用 IP 国家兜底。
|
// user-service 是 region_id 的唯一 owner,查询失败不能用 IP 国家兜底。
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,28 +69,28 @@ func (h *Handler) listRooms(writer http.ResponseWriter, request *http.Request) {
|
|||||||
Query: roomListQuery(request),
|
Query: roomListQuery(request),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(writer, request, roomListDataFromProto(resp))
|
httpkit.WriteOK(writer, request, roomListDataFromProto(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
// listRoomFeeds 查询 Mine 页 visited/friend/following 用户房间流。
|
// listRoomFeeds 查询 Mine 页 visited/friend/following 用户房间流。
|
||||||
func (h *Handler) listRoomFeeds(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) listRoomFeeds(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.userProfileClient == nil || h.roomQueryClient == nil {
|
if h.userProfileClient == nil || h.roomQueryClient == nil {
|
||||||
// feed 同样需要 user-service 区域归属,避免跨区域露出用户关系流房间。
|
// feed 同样需要 user-service 区域归属,避免跨区域露出用户关系流房间。
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
limit, ok := parseRoomListLimit(request.URL.Query().Get("limit"))
|
limit, ok := parseRoomListLimit(request.URL.Query().Get("limit"))
|
||||||
if !ok {
|
if !ok {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tab, ok := parseRoomFeedTab(request.URL.Query().Get("tab"))
|
tab, ok := parseRoomFeedTab(request.URL.Query().Get("tab"))
|
||||||
if !ok {
|
if !ok {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,17 +100,17 @@ func (h *Handler) listRoomFeeds(writer http.ResponseWriter, request *http.Reques
|
|||||||
UserId: viewerUserID,
|
UserId: viewerUserID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
relatedUsers, ok, err := h.roomFeedRelatedUsers(request, tab, roomListQuery(request), request.URL.Query().Get("cursor"))
|
relatedUsers, ok, err := h.roomFeedRelatedUsers(request, tab, roomListQuery(request), request.URL.Query().Get("cursor"))
|
||||||
if !ok {
|
if !ok {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,10 +125,10 @@ func (h *Handler) listRoomFeeds(writer http.ResponseWriter, request *http.Reques
|
|||||||
RelatedUsers: relatedUsers,
|
RelatedUsers: relatedUsers,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(writer, request, roomListDataFromProto(resp))
|
httpkit.WriteOK(writer, request, roomListDataFromProto(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) roomFeedRelatedUsers(request *http.Request, tab string, query string, rawCursor string) ([]*roomv1.RoomFeedRelatedUser, bool, error) {
|
func (h *Handler) roomFeedRelatedUsers(request *http.Request, tab string, query string, rawCursor string) ([]*roomv1.RoomFeedRelatedUser, bool, error) {
|
||||||
@ -281,7 +282,7 @@ func parseRoomListLimit(raw string) (int32, bool) {
|
|||||||
// 这个入口直接查 room-service 的 owner 权威数据,不依赖发现页 room_list_entries 投影是否命中。
|
// 这个入口直接查 room-service 的 owner 权威数据,不依赖发现页 room_list_entries 投影是否命中。
|
||||||
func (h *Handler) getMyRoom(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) getMyRoom(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.roomQueryClient == nil {
|
if h.roomQueryClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -290,17 +291,17 @@ func (h *Handler) getMyRoom(writer http.ResponseWriter, request *http.Request) {
|
|||||||
OwnerUserId: auth.UserIDFromContext(request.Context()),
|
OwnerUserId: auth.UserIDFromContext(request.Context()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(writer, request, myRoomDataFromProto(resp))
|
httpkit.WriteOK(writer, request, myRoomDataFromProto(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
// getCurrentRoom 查询当前登录用户是否仍有可恢复房间。
|
// getCurrentRoom 查询当前登录用户是否仍有可恢复房间。
|
||||||
// gateway 只传鉴权 user_id;presence 和麦位状态由 room-service 用读模型和 Room Cell 快照判断。
|
// gateway 只传鉴权 user_id;presence 和麦位状态由 room-service 用读模型和 Room Cell 快照判断。
|
||||||
func (h *Handler) getCurrentRoom(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) getCurrentRoom(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.roomQueryClient == nil {
|
if h.roomQueryClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,10 +310,10 @@ func (h *Handler) getCurrentRoom(writer http.ResponseWriter, request *http.Reque
|
|||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(writer, request, currentRoomDataFromProto(resp))
|
httpkit.WriteOK(writer, request, currentRoomDataFromProto(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
// currentRoomData 是 gateway 的外部 JSON 契约,显式保留 false/0 字段。
|
// currentRoomData 是 gateway 的外部 JSON 契约,显式保留 false/0 字段。
|
||||||
@ -351,12 +352,12 @@ func currentRoomDataFromProto(resp *roomv1.GetCurrentRoomResponse) currentRoomDa
|
|||||||
// 该接口只读 Room Cell/snapshot,不刷新 heartbeat,也不隐式 JoinRoom。
|
// 该接口只读 Room Cell/snapshot,不刷新 heartbeat,也不隐式 JoinRoom。
|
||||||
func (h *Handler) getRoomSnapshot(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) getRoomSnapshot(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.roomQueryClient == nil {
|
if h.roomQueryClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
roomID := strings.TrimSpace(request.URL.Query().Get("room_id"))
|
roomID := strings.TrimSpace(request.URL.Query().Get("room_id"))
|
||||||
if !roomid.ValidStringID(roomID) {
|
if !roomid.ValidStringID(roomID) {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -387,12 +388,12 @@ func (h *Handler) createRoom(writer http.ResponseWriter, request *http.Request)
|
|||||||
body.RoomDescription = strings.TrimSpace(body.RoomDescription)
|
body.RoomDescription = strings.TrimSpace(body.RoomDescription)
|
||||||
if body.SeatCount < 0 || body.Mode == "" || body.RoomName == "" {
|
if body.SeatCount < 0 || body.Mode == "" || body.RoomName == "" {
|
||||||
// seat_count 可以省略走后台默认值;mode 和 room_name 仍是创建页的最小必填信息。
|
// seat_count 可以省略走后台默认值;mode 和 room_name 仍是创建页的最小必填信息。
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if h.userProfileClient == nil {
|
if h.userProfileClient == nil {
|
||||||
// CreateRoom 的 visible_region_id 必须来自 user-service,不能让客户端传入或 gateway 猜测。
|
// CreateRoom 的 visible_region_id 必须来自 user-service,不能让客户端传入或 gateway 猜测。
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||||
@ -400,7 +401,7 @@ func (h *Handler) createRoom(writer http.ResponseWriter, request *http.Request)
|
|||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -447,10 +448,10 @@ func (h *Handler) updateRoomProfile(writer http.ResponseWriter, request *http.Re
|
|||||||
SeatCount: body.SeatCount,
|
SeatCount: body.SeatCount,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(writer, request, updateRoomProfileDataFromProto(resp))
|
httpkit.WriteOK(writer, request, updateRoomProfileDataFromProto(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
func trimOptionalString(value *string) {
|
func trimOptionalString(value *string) {
|
||||||
@ -477,10 +478,10 @@ func (h *Handler) joinRoom(writer http.ResponseWriter, request *http.Request) {
|
|||||||
Role: body.Role,
|
Role: body.Role,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(writer, request, h.joinRoomInitialData(request, body.RoomID, resp))
|
httpkit.WriteOK(writer, request, h.joinRoomInitialData(request, body.RoomID, resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) joinRoomInitialData(request *http.Request, requestedRoomID string, resp *roomv1.JoinRoomResponse) joinRoomData {
|
func (h *Handler) joinRoomInitialData(request *http.Request, requestedRoomID string, resp *roomv1.JoinRoomResponse) joinRoomData {
|
||||||
|
|||||||
@ -4,97 +4,117 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"hyapp/services/gateway-service/internal/auth"
|
"hyapp/services/gateway-service/internal/auth"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httproutes"
|
||||||
)
|
)
|
||||||
|
|
||||||
const apiV1Prefix = "/api/v1"
|
// Routes 把 Handler 的私有方法适配成独立路由包需要的显式 handler 表。
|
||||||
|
// 路径注册集中在 httproutes,gateway 依赖装配和鉴权 wrapper 仍留在 transport 入口层。
|
||||||
// Routes 只维护 gateway 对外 HTTP 路径到 transport handler 的绑定。
|
|
||||||
// 业务状态和命令执行仍然全部下沉到 room-service,gateway 不在这里承载房间逻辑。
|
|
||||||
func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
|
func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
|
||||||
mux := http.NewServeMux()
|
return httproutes.New(httproutes.Config{
|
||||||
|
PublicWrap: h.publicAPIHandler,
|
||||||
mux.Handle(apiV1Prefix+"/auth/account/login", h.publicAPIHandler(h.loginPassword))
|
AuthWrap: func(handler http.HandlerFunc) http.Handler {
|
||||||
mux.Handle(apiV1Prefix+"/auth/password/set", h.apiHandler(jwtVerifier, h.setPassword))
|
return h.apiHandler(jwtVerifier, handler)
|
||||||
mux.Handle(apiV1Prefix+"/auth/third-party/login", h.publicAPIHandler(h.loginThirdParty))
|
},
|
||||||
mux.Handle(apiV1Prefix+"/auth/token/refresh", h.publicAPIHandler(h.refreshToken))
|
ProfileWrap: func(handler http.HandlerFunc) http.Handler {
|
||||||
mux.Handle(apiV1Prefix+"/auth/logout", h.publicAPIHandler(h.logout))
|
return h.profileAPIHandler(jwtVerifier, handler)
|
||||||
|
},
|
||||||
mux.Handle(apiV1Prefix+"/countries", h.publicAPIHandler(h.listRegistrationCountries))
|
Auth: httproutes.AuthHandlers{
|
||||||
mux.Handle(apiV1Prefix+"/app/bootstrap", h.publicAPIHandler(h.getAppBootstrap))
|
LoginPassword: h.loginPassword,
|
||||||
mux.Handle(apiV1Prefix+"/app/h5-links", h.publicAPIHandler(h.listH5Links))
|
SetPassword: h.setPassword,
|
||||||
mux.Handle(apiV1Prefix+"/app/banners", h.publicAPIHandler(h.listAppBanners))
|
LoginThirdParty: h.loginThirdParty,
|
||||||
mux.Handle(apiV1Prefix+"/resources", h.publicAPIHandler(h.listResources))
|
RefreshToken: h.refreshToken,
|
||||||
mux.Handle(apiV1Prefix+"/resource-groups/", h.publicAPIHandler(h.getResourceGroup))
|
Logout: h.logout,
|
||||||
mux.Handle(apiV1Prefix+"/gifts", h.publicAPIHandler(h.listGifts))
|
},
|
||||||
mux.Handle(apiV1Prefix+"/manager-center/resource-grants/resources", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.listManagerGrantResources)))
|
App: httproutes.AppHandlers{
|
||||||
mux.Handle(apiV1Prefix+"/manager-center/resource-grants", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.grantManagerResource)))
|
ListRegistrationCountries: h.listRegistrationCountries,
|
||||||
mux.Handle(apiV1Prefix+"/business/users/lookup", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.lookupBusinessUsers)))
|
GetAppBootstrap: h.getAppBootstrap,
|
||||||
mux.Handle(apiV1Prefix+"/im/usersig", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.issueTencentIMUserSig)))
|
ListH5Links: h.listH5Links,
|
||||||
mux.Handle(apiV1Prefix+"/rtc/token", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.issueTencentRTCToken)))
|
ListAppBanners: h.listAppBanners,
|
||||||
mux.Handle(apiV1Prefix+"/tencent-im/callback", h.publicAPIHandler(h.handleTencentIMCallback))
|
ListResources: h.listResources,
|
||||||
mux.Handle(apiV1Prefix+"/files/upload", h.apiHandler(jwtVerifier, h.uploadFile))
|
GetResourceGroup: h.getResourceGroup,
|
||||||
mux.Handle(apiV1Prefix+"/files/avatar/upload", h.apiHandler(jwtVerifier, h.uploadAvatar))
|
ListGifts: h.listGifts,
|
||||||
mux.Handle(apiV1Prefix+"/devices/push-token", h.profileAPIHandler(jwtVerifier, h.handlePushToken))
|
IssueTencentIMUserSig: h.issueTencentIMUserSig,
|
||||||
|
IssueTencentRTCToken: h.issueTencentRTCToken,
|
||||||
mux.Handle(apiV1Prefix+"/users/by-display-user-id/", h.publicAPIHandler(h.resolveDisplayUserID))
|
HandleTencentIMCallback: h.handleTencentIMCallback,
|
||||||
mux.Handle(apiV1Prefix+"/users/profiles:batch", h.profileAPIHandler(jwtVerifier, h.batchUserProfiles))
|
HandleTencentRTCCallback: h.handleTencentRTCCallback,
|
||||||
mux.Handle(apiV1Prefix+"/users/me/overview", h.profileAPIHandler(jwtVerifier, h.getMyOverview))
|
UploadFile: h.uploadFile,
|
||||||
mux.Handle(apiV1Prefix+"/users/me/identity", h.apiHandler(jwtVerifier, h.getMyIdentity))
|
UploadAvatar: h.uploadAvatar,
|
||||||
mux.Handle(apiV1Prefix+"/users/me/host-identity", h.profileAPIHandler(jwtVerifier, h.getMyHostIdentity))
|
HandlePushToken: h.handlePushToken,
|
||||||
mux.Handle(apiV1Prefix+"/users/me/role-summary", h.profileAPIHandler(jwtVerifier, h.getMyRoleSummary))
|
},
|
||||||
mux.Handle(apiV1Prefix+"/users/me/onboarding/complete", h.apiHandler(jwtVerifier, h.completeMyOnboarding))
|
User: httproutes.UserHandlers{
|
||||||
mux.Handle(apiV1Prefix+"/users/me/profile/update", h.profileAPIHandler(jwtVerifier, h.updateMyProfile))
|
ResolveDisplayUserID: h.resolveDisplayUserID,
|
||||||
mux.Handle(apiV1Prefix+"/users/me/country/change", h.profileAPIHandler(jwtVerifier, h.changeMyCountry))
|
BatchUserProfiles: h.batchUserProfiles,
|
||||||
mux.Handle(apiV1Prefix+"/users/me/display-id/change", h.apiHandler(jwtVerifier, h.changeMyDisplayUserID))
|
GetMyOverview: h.getMyOverview,
|
||||||
mux.Handle(apiV1Prefix+"/users/me/display-id/pretty/apply", h.apiHandler(jwtVerifier, h.applyMyPrettyDisplayUserID))
|
GetMyIdentity: h.getMyIdentity,
|
||||||
mux.Handle(apiV1Prefix+"/users/me/visitors", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.listMyProfileVisitors)))
|
GetMyHostIdentity: h.getMyHostIdentity,
|
||||||
mux.Handle(apiV1Prefix+"/users/me/following", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.listMyFollowing)))
|
GetMyRoleSummary: h.getMyRoleSummary,
|
||||||
mux.Handle(apiV1Prefix+"/users/me/friends", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.listMyFriends)))
|
CompleteMyOnboarding: h.completeMyOnboarding,
|
||||||
mux.Handle(apiV1Prefix+"/users/me/friend-requests", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.listMyFriendApplications)))
|
UpdateMyProfile: h.updateMyProfile,
|
||||||
mux.Handle(apiV1Prefix+"/users/me", h.profileAPIHandler(jwtVerifier, h.getMyProfile))
|
ChangeMyCountry: h.changeMyCountry,
|
||||||
mux.Handle(apiV1Prefix+"/users/me/resources", h.profileAPIHandler(jwtVerifier, h.listMyResources))
|
ChangeMyDisplayUserID: h.changeMyDisplayUserID,
|
||||||
mux.Handle(apiV1Prefix+"/users/me/resources/", h.profileAPIHandler(jwtVerifier, h.equipMyResource))
|
ApplyMyPrettyDisplayUserID: h.applyMyPrettyDisplayUserID,
|
||||||
mux.Handle(apiV1Prefix+"/users/", h.profileAPIHandler(jwtVerifier, h.userSocialAction))
|
ListMyProfileVisitors: h.listMyProfileVisitors,
|
||||||
|
ListMyFollowing: h.listMyFollowing,
|
||||||
mux.Handle(apiV1Prefix+"/rooms/me", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.getMyRoom)))
|
ListMyFriends: h.listMyFriends,
|
||||||
mux.Handle(apiV1Prefix+"/rooms/feeds", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.listRoomFeeds)))
|
ListMyFriendApplications: h.listMyFriendApplications,
|
||||||
mux.Handle(apiV1Prefix+"/rooms", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.listRooms)))
|
GetMyProfile: h.getMyProfile,
|
||||||
mux.Handle(apiV1Prefix+"/rooms/current", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.getCurrentRoom)))
|
ListMyResources: h.listMyResources,
|
||||||
mux.Handle(apiV1Prefix+"/rooms/snapshot", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.getRoomSnapshot)))
|
EquipMyResource: h.equipMyResource,
|
||||||
mux.Handle(apiV1Prefix+"/rooms/create", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.createRoom)))
|
UserSocialAction: h.userSocialAction,
|
||||||
mux.Handle(apiV1Prefix+"/rooms/profile/update", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.updateRoomProfile)))
|
},
|
||||||
mux.Handle(apiV1Prefix+"/rooms/join", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.joinRoom)))
|
Manager: httproutes.ManagerHandlers{
|
||||||
mux.Handle(apiV1Prefix+"/rooms/heartbeat", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.roomHeartbeat)))
|
ListManagerGrantResources: h.listManagerGrantResources,
|
||||||
mux.Handle(apiV1Prefix+"/rooms/leave", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.leaveRoom)))
|
GrantManagerResource: h.grantManagerResource,
|
||||||
mux.Handle(apiV1Prefix+"/rooms/mic/up", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.micUp)))
|
LookupBusinessUsers: h.lookupBusinessUsers,
|
||||||
mux.Handle(apiV1Prefix+"/rooms/mic/down", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.micDown)))
|
},
|
||||||
mux.Handle(apiV1Prefix+"/rooms/mic/change", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.changeMicSeat)))
|
Room: httproutes.RoomHandlers{
|
||||||
mux.Handle(apiV1Prefix+"/rooms/mic/publishing/confirm", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.confirmMicPublishing)))
|
GetMyRoom: h.getMyRoom,
|
||||||
mux.Handle(apiV1Prefix+"/rooms/mic/lock", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.setMicSeatLock)))
|
ListRoomFeeds: h.listRoomFeeds,
|
||||||
mux.Handle(apiV1Prefix+"/rooms/chat/enabled", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.setChatEnabled)))
|
ListRooms: h.listRooms,
|
||||||
mux.Handle(apiV1Prefix+"/rooms/admin/set", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.setRoomAdmin)))
|
GetCurrentRoom: h.getCurrentRoom,
|
||||||
mux.Handle(apiV1Prefix+"/rooms/host/transfer", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.transferRoomHost)))
|
GetRoomSnapshot: h.getRoomSnapshot,
|
||||||
mux.Handle(apiV1Prefix+"/rooms/user/mute", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.muteUser)))
|
CreateRoom: h.createRoom,
|
||||||
mux.Handle(apiV1Prefix+"/rooms/user/kick", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.kickUser)))
|
UpdateRoomProfile: h.updateRoomProfile,
|
||||||
mux.Handle(apiV1Prefix+"/rooms/user/unban", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.unbanUser)))
|
JoinRoom: h.joinRoom,
|
||||||
mux.Handle(apiV1Prefix+"/rooms/gift/send", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.sendGift)))
|
RoomHeartbeat: h.roomHeartbeat,
|
||||||
mux.Handle(apiV1Prefix+"/tencent-rtc/callback", h.publicAPIHandler(h.handleTencentRTCCallback))
|
LeaveRoom: h.leaveRoom,
|
||||||
mux.Handle(apiV1Prefix+"/messages/tabs", h.profileAPIHandler(jwtVerifier, h.listMessageTabs))
|
MicUp: h.micUp,
|
||||||
mux.Handle(apiV1Prefix+"/messages/read-all", h.profileAPIHandler(jwtVerifier, h.markInboxSectionRead))
|
MicDown: h.micDown,
|
||||||
mux.Handle(apiV1Prefix+"/messages/", h.profileAPIHandler(jwtVerifier, h.inboxMessageByID))
|
ChangeMicSeat: h.changeMicSeat,
|
||||||
mux.Handle(apiV1Prefix+"/messages", h.profileAPIHandler(jwtVerifier, h.listInboxMessages))
|
ConfirmMicPublishing: h.confirmMicPublishing,
|
||||||
mux.Handle(apiV1Prefix+"/tasks/tabs", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.listTaskTabs)))
|
SetMicSeatLock: h.setMicSeatLock,
|
||||||
mux.Handle(apiV1Prefix+"/tasks/claim", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.claimTaskReward)))
|
SetChatEnabled: h.setChatEnabled,
|
||||||
mux.Handle(apiV1Prefix+"/wallet/me/overview", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.getWalletOverview)))
|
SetRoomAdmin: h.setRoomAdmin,
|
||||||
mux.Handle(apiV1Prefix+"/wallet/me/balances", h.profileAPIHandler(jwtVerifier, h.getMyBalances))
|
TransferRoomHost: h.transferRoomHost,
|
||||||
mux.Handle(apiV1Prefix+"/wallet/recharge/products", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.listRechargeProducts)))
|
MuteUser: h.muteUser,
|
||||||
mux.Handle(apiV1Prefix+"/wallet/diamond-exchange/config", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.getDiamondExchangeConfig)))
|
KickUser: h.kickUser,
|
||||||
mux.Handle(apiV1Prefix+"/wallet/withdrawals/apply", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.applyWithdrawal)))
|
UnbanUser: h.unbanUser,
|
||||||
mux.Handle(apiV1Prefix+"/wallet/transactions", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.listWalletTransactions)))
|
SendGift: h.sendGift,
|
||||||
mux.Handle(apiV1Prefix+"/wallet/coin-seller/transfer", h.profileAPIHandler(jwtVerifier, h.transferCoinFromSeller))
|
},
|
||||||
mux.Handle(apiV1Prefix+"/vip/me", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.getMyVIP)))
|
Message: httproutes.MessageHandlers{
|
||||||
mux.Handle(apiV1Prefix+"/vip/packages", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.listVIPPackages)))
|
ListMessageTabs: h.listMessageTabs,
|
||||||
mux.Handle(apiV1Prefix+"/vip/purchase", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.purchaseVIP)))
|
MarkInboxSectionRead: h.markInboxSectionRead,
|
||||||
|
MarkInboxMessageReadPath: h.markInboxMessageReadByPath,
|
||||||
return mux
|
DeleteInboxMessagePath: h.deleteInboxMessageByPath,
|
||||||
|
ListInboxMessages: h.listInboxMessages,
|
||||||
|
},
|
||||||
|
Task: httproutes.TaskHandlers{
|
||||||
|
ListTaskTabs: h.listTaskTabs,
|
||||||
|
ClaimTaskReward: h.claimTaskReward,
|
||||||
|
},
|
||||||
|
Wallet: httproutes.WalletHandlers{
|
||||||
|
GetWalletOverview: h.getWalletOverview,
|
||||||
|
GetMyBalances: h.getMyBalances,
|
||||||
|
ListRechargeProducts: h.listRechargeProducts,
|
||||||
|
GetDiamondExchangeConfig: h.getDiamondExchangeConfig,
|
||||||
|
ApplyWithdrawal: h.applyWithdrawal,
|
||||||
|
ListWalletTransactions: h.listWalletTransactions,
|
||||||
|
TransferCoinFromSeller: h.transferCoinFromSeller,
|
||||||
|
},
|
||||||
|
VIP: httproutes.VIPHandlers{
|
||||||
|
GetMyVIP: h.getMyVIP,
|
||||||
|
ListVIPPackages: h.listVIPPackages,
|
||||||
|
PurchaseVIP: h.purchaseVIP,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,106 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"hyapp/pkg/logx"
|
||||||
|
"hyapp/services/gateway-service/internal/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
var benchmarkLogOnce sync.Once
|
||||||
|
|
||||||
|
type benchmarkResponseWriter struct {
|
||||||
|
header http.Header
|
||||||
|
status int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBenchmarkResponseWriter() *benchmarkResponseWriter {
|
||||||
|
return &benchmarkResponseWriter{header: make(http.Header)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *benchmarkResponseWriter) Header() http.Header {
|
||||||
|
return w.header
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *benchmarkResponseWriter) Write(payload []byte) (int, error) {
|
||||||
|
return len(payload), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *benchmarkResponseWriter) WriteHeader(statusCode int) {
|
||||||
|
w.status = statusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *benchmarkResponseWriter) reset() {
|
||||||
|
for key := range w.header {
|
||||||
|
delete(w.header, key)
|
||||||
|
}
|
||||||
|
w.status = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func benchmarkGatewayRouter() http.Handler {
|
||||||
|
benchmarkLogOnce.Do(func() {
|
||||||
|
_ = logx.Init(logx.Config{Service: "gateway-service", Env: "bench", Level: "error", Output: io.Discard})
|
||||||
|
})
|
||||||
|
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, &fakeUserIdentityClient{
|
||||||
|
resolveByDisplay: map[string]int64{"10001": 10001},
|
||||||
|
}, &fakeUserProfileClient{})
|
||||||
|
handler.SetMessageInboxClient(&fakeMessageInboxClient{})
|
||||||
|
handler.SetUserSocialClient(&fakeUserSocialClient{})
|
||||||
|
return handler.Routes(auth.NewVerifier("secret"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func benchmarkGatewayToken(b *testing.B, userID int64) string {
|
||||||
|
b.Helper()
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
|
"user_id": strconv.FormatInt(userID, 10),
|
||||||
|
"app_code": "lalu",
|
||||||
|
"sid": "bench-session",
|
||||||
|
"profile_completed": true,
|
||||||
|
"onboarding_status": "completed",
|
||||||
|
})
|
||||||
|
signed, err := token.SignedString([]byte("secret"))
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("sign benchmark token failed: %v", err)
|
||||||
|
}
|
||||||
|
return signed
|
||||||
|
}
|
||||||
|
|
||||||
|
func benchmarkGatewayRoute(b *testing.B, router http.Handler, method string, path string, token string) {
|
||||||
|
b.Helper()
|
||||||
|
request := httptest.NewRequest(method, path, nil)
|
||||||
|
request.Header.Set("X-App-Code", "lalu")
|
||||||
|
if token != "" {
|
||||||
|
request.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
}
|
||||||
|
writer := newBenchmarkResponseWriter()
|
||||||
|
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
writer.reset()
|
||||||
|
router.ServeHTTP(writer, request)
|
||||||
|
if writer.status != http.StatusOK {
|
||||||
|
b.Fatalf("status = %d, want 200", writer.status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkGatewayResolveDisplayUserIDRoute(b *testing.B) {
|
||||||
|
benchmarkGatewayRoute(b, benchmarkGatewayRouter(), http.MethodGet, "/api/v1/users/by-display-user-id/10001", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkGatewayMessageReadRoute(b *testing.B) {
|
||||||
|
token := benchmarkGatewayToken(b, 42)
|
||||||
|
benchmarkGatewayRoute(b, benchmarkGatewayRouter(), http.MethodPost, "/api/v1/messages/msg-10001/read", token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkGatewayUserSocialRoute(b *testing.B) {
|
||||||
|
token := benchmarkGatewayToken(b, 42)
|
||||||
|
benchmarkGatewayRoute(b, benchmarkGatewayRouter(), http.MethodPost, "/api/v1/users/10002/friend-requests/accept", token)
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
@ -25,39 +26,39 @@ func (h *Handler) issueTencentRTCToken(writer http.ResponseWriter, request *http
|
|||||||
}
|
}
|
||||||
if !roomid.ValidStringID(body.RoomID) {
|
if !roomid.ValidStringID(body.RoomID) {
|
||||||
// Token signing repeats room_id validation to fail closed on historical or dirty rooms.
|
// Token signing repeats room_id validation to fail closed on historical or dirty rooms.
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userID := auth.UserIDFromContext(request.Context())
|
userID := auth.UserIDFromContext(request.Context())
|
||||||
if userID <= 0 {
|
if userID <= 0 {
|
||||||
// Auth middleware normally blocks this; the handler keeps a defensive guard before signing.
|
// Auth middleware normally blocks this; the handler keeps a defensive guard before signing.
|
||||||
writeError(writer, request, http.StatusUnauthorized, codeUnauthorized, "unauthorized")
|
httpkit.WriteError(writer, request, http.StatusUnauthorized, httpkit.CodeUnauthorized, "unauthorized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logCtx := logx.With(request.Context(), slog.String("request_id", requestIDFromContext(request.Context())), slog.String("app_code", appcode.FromContext(request.Context())))
|
logCtx := logx.With(request.Context(), slog.String("request_id", httpkit.RequestIDFromContext(request.Context())), slog.String("app_code", appcode.FromContext(request.Context())))
|
||||||
|
|
||||||
rtcConfig := h.tencentRTCTokenConfig()
|
rtcConfig := h.tencentRTCTokenConfig()
|
||||||
if err := tencentrtc.ValidateConfig(rtcConfig); err != nil {
|
if err := tencentrtc.ValidateConfig(rtcConfig); err != nil {
|
||||||
logx.Error(logCtx, "gateway_rtc_token_config_error", err, slog.Int64("user_id", userID), slog.String("room_id", body.RoomID))
|
logx.Error(logCtx, "gateway_rtc_token_config_error", err, slog.Int64("user_id", userID), slog.String("room_id", body.RoomID))
|
||||||
writeError(writer, request, http.StatusInternalServerError, codeInternalError, "internal error")
|
httpkit.WriteError(writer, request, http.StatusInternalServerError, httpkit.CodeInternalError, "internal error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.roomGuardClient == nil {
|
if h.roomGuardClient == nil {
|
||||||
// Without room-service presence, gateway cannot safely decide RTC room entry.
|
// Without room-service presence, gateway cannot safely decide RTC room entry.
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guardResp, err := h.roomGuardClient.VerifyRoomPresence(request.Context(), &roomv1.VerifyRoomPresenceRequest{
|
guardResp, err := h.roomGuardClient.VerifyRoomPresence(request.Context(), &roomv1.VerifyRoomPresenceRequest{
|
||||||
RoomId: body.RoomID,
|
RoomId: body.RoomID,
|
||||||
UserId: userID,
|
UserId: userID,
|
||||||
RequestId: requestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if guardResp == nil || !guardResp.GetPresent() {
|
if guardResp == nil || !guardResp.GetPresent() {
|
||||||
@ -68,7 +69,7 @@ func (h *Handler) issueTencentRTCToken(writer http.ResponseWriter, request *http
|
|||||||
token, err := h.generateTencentRTCToken(userID, body.RoomID)
|
token, err := h.generateTencentRTCToken(userID, body.RoomID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logx.Error(logCtx, "gateway_rtc_token_sign_failed", err, slog.Int64("user_id", userID), slog.String("room_id", body.RoomID))
|
logx.Error(logCtx, "gateway_rtc_token_sign_failed", err, slog.Int64("user_id", userID), slog.String("room_id", body.RoomID))
|
||||||
writeError(writer, request, http.StatusInternalServerError, codeInternalError, "internal error")
|
httpkit.WriteError(writer, request, http.StatusInternalServerError, httpkit.CodeInternalError, "internal error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,7 +80,7 @@ func (h *Handler) issueTencentRTCToken(writer http.ResponseWriter, request *http
|
|||||||
slog.String("str_room_id", token.StrRoomID),
|
slog.String("str_room_id", token.StrRoomID),
|
||||||
slog.Int64("room_version", guardResp.GetRoomVersion()),
|
slog.Int64("room_version", guardResp.GetRoomVersion()),
|
||||||
)
|
)
|
||||||
writeOK(writer, request, token)
|
httpkit.WriteOK(writer, request, token)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) writeRTCPresenceDenied(writer http.ResponseWriter, request *http.Request, targetRoomID string, userID int64, guardResp *roomv1.VerifyRoomPresenceResponse) {
|
func (h *Handler) writeRTCPresenceDenied(writer http.ResponseWriter, request *http.Request, targetRoomID string, userID int64, guardResp *roomv1.VerifyRoomPresenceResponse) {
|
||||||
@ -90,18 +91,18 @@ func (h *Handler) writeRTCPresenceDenied(writer http.ResponseWriter, request *ht
|
|||||||
roomVersion = guardResp.GetRoomVersion()
|
roomVersion = guardResp.GetRoomVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
logCtx := logx.With(request.Context(), slog.String("request_id", requestIDFromContext(request.Context())), slog.String("app_code", appcode.FromContext(request.Context())))
|
logCtx := logx.With(request.Context(), slog.String("request_id", httpkit.RequestIDFromContext(request.Context())), slog.String("app_code", appcode.FromContext(request.Context())))
|
||||||
logx.Warn(logCtx, "gateway_rtc_token_denied", slog.Int64("user_id", userID), slog.String("room_id", targetRoomID), slog.Int64("room_version", roomVersion), slog.String("reason", reason))
|
logx.Warn(logCtx, "gateway_rtc_token_denied", slog.Int64("user_id", userID), slog.String("room_id", targetRoomID), slog.Int64("room_version", roomVersion), slog.String("reason", reason))
|
||||||
if reason == "room_not_found" {
|
if reason == "room_not_found" {
|
||||||
writeError(writer, request, http.StatusNotFound, codeNotFound, "not found")
|
httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if reason == "room_closed" {
|
if reason == "room_closed" {
|
||||||
writeError(writer, request, http.StatusConflict, string(xerr.RoomClosed), "room closed")
|
httpkit.WriteError(writer, request, http.StatusConflict, string(xerr.RoomClosed), "room closed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeError(writer, request, http.StatusForbidden, codePermissionDenied, "permission denied")
|
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
|
||||||
}
|
}
|
||||||
|
|
||||||
var timeNow = defaultTimeNow
|
var timeNow = defaultTimeNow
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
userv1 "hyapp.local/api/proto/user/v1"
|
userv1 "hyapp.local/api/proto/user/v1"
|
||||||
@ -39,12 +39,12 @@ type friendApplicationData struct {
|
|||||||
// listMyProfileVisitors 返回访问我的主页的人;这是关系 read model,不实时扫用户行为日志。
|
// listMyProfileVisitors 返回访问我的主页的人;这是关系 read model,不实时扫用户行为日志。
|
||||||
func (h *Handler) listMyProfileVisitors(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) listMyProfileVisitors(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.userSocialClient == nil {
|
if h.userSocialClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
page, pageSize, ok := socialPage(request)
|
page, pageSize, ok := socialPage(request)
|
||||||
if !ok {
|
if !ok {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.userSocialClient.ListProfileVisitors(request.Context(), &userv1.ListProfileVisitorsRequest{
|
resp, err := h.userSocialClient.ListProfileVisitors(request.Context(), &userv1.ListProfileVisitorsRequest{
|
||||||
@ -54,25 +54,25 @@ func (h *Handler) listMyProfileVisitors(writer http.ResponseWriter, request *htt
|
|||||||
PageSize: pageSize,
|
PageSize: pageSize,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
records := make([]profileVisitRecordData, 0, len(resp.GetRecords()))
|
records := make([]profileVisitRecordData, 0, len(resp.GetRecords()))
|
||||||
for _, record := range resp.GetRecords() {
|
for _, record := range resp.GetRecords() {
|
||||||
records = append(records, profileVisitData(record))
|
records = append(records, profileVisitData(record))
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"records": records, "total": resp.GetTotal()})
|
httpkit.WriteOK(writer, request, map[string]any{"records": records, "total": resp.GetTotal()})
|
||||||
}
|
}
|
||||||
|
|
||||||
// listMyFollowing 返回我关注的用户列表;关注我的列表后续单独建接口,避免语义混淆。
|
// listMyFollowing 返回我关注的用户列表;关注我的列表后续单独建接口,避免语义混淆。
|
||||||
func (h *Handler) listMyFollowing(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) listMyFollowing(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.userSocialClient == nil {
|
if h.userSocialClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
page, pageSize, ok := socialPage(request)
|
page, pageSize, ok := socialPage(request)
|
||||||
if !ok {
|
if !ok {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.userSocialClient.ListFollowing(request.Context(), &userv1.ListFollowingRequest{
|
resp, err := h.userSocialClient.ListFollowing(request.Context(), &userv1.ListFollowingRequest{
|
||||||
@ -82,25 +82,25 @@ func (h *Handler) listMyFollowing(writer http.ResponseWriter, request *http.Requ
|
|||||||
PageSize: pageSize,
|
PageSize: pageSize,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
records := make([]followRecordData, 0, len(resp.GetRecords()))
|
records := make([]followRecordData, 0, len(resp.GetRecords()))
|
||||||
for _, record := range resp.GetRecords() {
|
for _, record := range resp.GetRecords() {
|
||||||
records = append(records, followData(record))
|
records = append(records, followData(record))
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"records": records, "total": resp.GetTotal()})
|
httpkit.WriteOK(writer, request, map[string]any{"records": records, "total": resp.GetTotal()})
|
||||||
}
|
}
|
||||||
|
|
||||||
// listMyFriends 返回双向好友列表。
|
// listMyFriends 返回双向好友列表。
|
||||||
func (h *Handler) listMyFriends(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) listMyFriends(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.userSocialClient == nil {
|
if h.userSocialClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
page, pageSize, ok := socialPage(request)
|
page, pageSize, ok := socialPage(request)
|
||||||
if !ok {
|
if !ok {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.userSocialClient.ListFriends(request.Context(), &userv1.ListFriendsRequest{
|
resp, err := h.userSocialClient.ListFriends(request.Context(), &userv1.ListFriendsRequest{
|
||||||
@ -110,25 +110,25 @@ func (h *Handler) listMyFriends(writer http.ResponseWriter, request *http.Reques
|
|||||||
PageSize: pageSize,
|
PageSize: pageSize,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
records := make([]friendRecordData, 0, len(resp.GetRecords()))
|
records := make([]friendRecordData, 0, len(resp.GetRecords()))
|
||||||
for _, record := range resp.GetRecords() {
|
for _, record := range resp.GetRecords() {
|
||||||
records = append(records, friendData(record))
|
records = append(records, friendData(record))
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"records": records, "total": resp.GetTotal()})
|
httpkit.WriteOK(writer, request, map[string]any{"records": records, "total": resp.GetTotal()})
|
||||||
}
|
}
|
||||||
|
|
||||||
// listMyFriendApplications 返回待处理申请;direction=incoming/outgoing。
|
// listMyFriendApplications 返回待处理申请;direction=incoming/outgoing。
|
||||||
func (h *Handler) listMyFriendApplications(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) listMyFriendApplications(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.userSocialClient == nil {
|
if h.userSocialClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
page, pageSize, ok := socialPage(request)
|
page, pageSize, ok := socialPage(request)
|
||||||
if !ok {
|
if !ok {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.userSocialClient.ListFriendApplications(request.Context(), &userv1.ListFriendApplicationsRequest{
|
resp, err := h.userSocialClient.ListFriendApplications(request.Context(), &userv1.ListFriendApplicationsRequest{
|
||||||
@ -139,27 +139,28 @@ func (h *Handler) listMyFriendApplications(writer http.ResponseWriter, request *
|
|||||||
PageSize: pageSize,
|
PageSize: pageSize,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
applications := make([]friendApplicationData, 0, len(resp.GetApplications()))
|
applications := make([]friendApplicationData, 0, len(resp.GetApplications()))
|
||||||
for _, application := range resp.GetApplications() {
|
for _, application := range resp.GetApplications() {
|
||||||
applications = append(applications, friendApplicationDataFromProto(application))
|
applications = append(applications, friendApplicationDataFromProto(application))
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"applications": applications, "total": resp.GetTotal()})
|
httpkit.WriteOK(writer, request, map[string]any{"applications": applications, "total": resp.GetTotal()})
|
||||||
}
|
}
|
||||||
|
|
||||||
// userSocialAction 处理 /users/{user_id}/visit|follow|friend-requests|friend-requests/accept|friend。
|
// userSocialAction 处理 /users/{user_id}/visit|follow|friend-requests|friend-requests/accept|friend。
|
||||||
func (h *Handler) userSocialAction(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) userSocialAction(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.userSocialClient == nil {
|
if h.userSocialClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
targetUserID, action, ok := parseUserSocialActionPath(request.URL.Path)
|
targetUserID, ok := httpkit.PositiveInt64PathValue(request, "user_id")
|
||||||
if !ok {
|
if !ok {
|
||||||
writeError(writer, request, http.StatusNotFound, codeNotFound, "not found")
|
httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
action := strings.Trim(request.PathValue("action"), "/")
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case "visit":
|
case "visit":
|
||||||
@ -173,13 +174,13 @@ func (h *Handler) userSocialAction(writer http.ResponseWriter, request *http.Req
|
|||||||
case "friend":
|
case "friend":
|
||||||
h.deleteFriend(writer, request, targetUserID)
|
h.deleteFriend(writer, request, targetUserID)
|
||||||
default:
|
default:
|
||||||
writeError(writer, request, http.StatusNotFound, codeNotFound, "not found")
|
httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) recordProfileVisit(writer http.ResponseWriter, request *http.Request, targetUserID int64) {
|
func (h *Handler) recordProfileVisit(writer http.ResponseWriter, request *http.Request, targetUserID int64) {
|
||||||
if request.Method != http.MethodPost {
|
if request.Method != http.MethodPost {
|
||||||
writeError(writer, request, http.StatusNotFound, codeNotFound, "not found")
|
httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.userSocialClient.RecordProfileVisit(request.Context(), &userv1.RecordProfileVisitRequest{
|
resp, err := h.userSocialClient.RecordProfileVisit(request.Context(), &userv1.RecordProfileVisitRequest{
|
||||||
@ -188,10 +189,10 @@ func (h *Handler) recordProfileVisit(writer http.ResponseWriter, request *http.R
|
|||||||
TargetUserId: targetUserID,
|
TargetUserId: targetUserID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"recorded": resp.GetRecorded(), "target_stats": overviewStatsFromProto(resp.GetTargetStats())})
|
httpkit.WriteOK(writer, request, map[string]any{"recorded": resp.GetRecorded(), "target_stats": overviewStatsFromProto(resp.GetTargetStats())})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleFollowAction(writer http.ResponseWriter, request *http.Request, followeeUserID int64) {
|
func (h *Handler) handleFollowAction(writer http.ResponseWriter, request *http.Request, followeeUserID int64) {
|
||||||
@ -204,10 +205,10 @@ func (h *Handler) handleFollowAction(writer http.ResponseWriter, request *http.R
|
|||||||
FolloweeUserId: followeeUserID,
|
FolloweeUserId: followeeUserID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"following": resp.GetFollowing(), "follower_stats": overviewStatsFromProto(resp.GetFollowerStats())})
|
httpkit.WriteOK(writer, request, map[string]any{"following": resp.GetFollowing(), "follower_stats": overviewStatsFromProto(resp.GetFollowerStats())})
|
||||||
case http.MethodDelete:
|
case http.MethodDelete:
|
||||||
resp, err := h.userSocialClient.UnfollowUser(request.Context(), &userv1.UnfollowUserRequest{
|
resp, err := h.userSocialClient.UnfollowUser(request.Context(), &userv1.UnfollowUserRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: authRequestMeta(request, ""),
|
||||||
@ -215,18 +216,18 @@ func (h *Handler) handleFollowAction(writer http.ResponseWriter, request *http.R
|
|||||||
FolloweeUserId: followeeUserID,
|
FolloweeUserId: followeeUserID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"following": resp.GetFollowing(), "follower_stats": overviewStatsFromProto(resp.GetFollowerStats())})
|
httpkit.WriteOK(writer, request, map[string]any{"following": resp.GetFollowing(), "follower_stats": overviewStatsFromProto(resp.GetFollowerStats())})
|
||||||
default:
|
default:
|
||||||
writeError(writer, request, http.StatusNotFound, codeNotFound, "not found")
|
httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) applyFriend(writer http.ResponseWriter, request *http.Request, targetUserID int64) {
|
func (h *Handler) applyFriend(writer http.ResponseWriter, request *http.Request, targetUserID int64) {
|
||||||
if request.Method != http.MethodPost {
|
if request.Method != http.MethodPost {
|
||||||
writeError(writer, request, http.StatusNotFound, codeNotFound, "not found")
|
httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.userSocialClient.ApplyFriend(request.Context(), &userv1.ApplyFriendRequest{
|
resp, err := h.userSocialClient.ApplyFriend(request.Context(), &userv1.ApplyFriendRequest{
|
||||||
@ -235,15 +236,15 @@ func (h *Handler) applyFriend(writer http.ResponseWriter, request *http.Request,
|
|||||||
TargetUserId: targetUserID,
|
TargetUserId: targetUserID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"application": friendApplicationDataFromProto(resp.GetApplication()), "already_friends": resp.GetAlreadyFriends()})
|
httpkit.WriteOK(writer, request, map[string]any{"application": friendApplicationDataFromProto(resp.GetApplication()), "already_friends": resp.GetAlreadyFriends()})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) acceptFriendApplication(writer http.ResponseWriter, request *http.Request, requesterUserID int64) {
|
func (h *Handler) acceptFriendApplication(writer http.ResponseWriter, request *http.Request, requesterUserID int64) {
|
||||||
if request.Method != http.MethodPost {
|
if request.Method != http.MethodPost {
|
||||||
writeError(writer, request, http.StatusNotFound, codeNotFound, "not found")
|
httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.userSocialClient.AcceptFriendApplication(request.Context(), &userv1.AcceptFriendApplicationRequest{
|
resp, err := h.userSocialClient.AcceptFriendApplication(request.Context(), &userv1.AcceptFriendApplicationRequest{
|
||||||
@ -252,15 +253,15 @@ func (h *Handler) acceptFriendApplication(writer http.ResponseWriter, request *h
|
|||||||
RequesterUserId: requesterUserID,
|
RequesterUserId: requesterUserID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"friend": friendData(resp.GetFriend())})
|
httpkit.WriteOK(writer, request, map[string]any{"friend": friendData(resp.GetFriend())})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) deleteFriend(writer http.ResponseWriter, request *http.Request, friendUserID int64) {
|
func (h *Handler) deleteFriend(writer http.ResponseWriter, request *http.Request, friendUserID int64) {
|
||||||
if request.Method != http.MethodDelete {
|
if request.Method != http.MethodDelete {
|
||||||
writeError(writer, request, http.StatusNotFound, codeNotFound, "not found")
|
httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.userSocialClient.DeleteFriend(request.Context(), &userv1.DeleteFriendRequest{
|
resp, err := h.userSocialClient.DeleteFriend(request.Context(), &userv1.DeleteFriendRequest{
|
||||||
@ -269,10 +270,10 @@ func (h *Handler) deleteFriend(writer http.ResponseWriter, request *http.Request
|
|||||||
FriendUserId: friendUserID,
|
FriendUserId: friendUserID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"deleted": resp.GetDeleted()})
|
httpkit.WriteOK(writer, request, map[string]any{"deleted": resp.GetDeleted()})
|
||||||
}
|
}
|
||||||
|
|
||||||
func socialPage(request *http.Request) (int32, int32, bool) {
|
func socialPage(request *http.Request) (int32, int32, bool) {
|
||||||
@ -287,23 +288,6 @@ func socialPage(request *http.Request) (int32, int32, bool) {
|
|||||||
return page, pageSize, true
|
return page, pageSize, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseUserSocialActionPath(path string) (int64, string, bool) {
|
|
||||||
if !strings.HasPrefix(path, apiV1Prefix+"/users/") {
|
|
||||||
return 0, "", false
|
|
||||||
}
|
|
||||||
raw := strings.Trim(strings.TrimPrefix(path, apiV1Prefix+"/users/"), "/")
|
|
||||||
parts := strings.Split(raw, "/")
|
|
||||||
if len(parts) < 2 || len(parts) > 3 || parts[0] == "me" {
|
|
||||||
return 0, "", false
|
|
||||||
}
|
|
||||||
userID, err := strconv.ParseInt(parts[0], 10, 64)
|
|
||||||
if err != nil || userID <= 0 {
|
|
||||||
return 0, "", false
|
|
||||||
}
|
|
||||||
action := strings.Join(parts[1:], "/")
|
|
||||||
return userID, action, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func profileVisitData(record *userv1.ProfileVisitRecord) profileVisitRecordData {
|
func profileVisitData(record *userv1.ProfileVisitRecord) profileVisitRecordData {
|
||||||
if record == nil {
|
if record == nil {
|
||||||
return profileVisitRecordData{}
|
return profileVisitRecordData{}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package http
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
@ -119,8 +120,8 @@ func TestSocialListRoutesPassPagingAndDirection(t *testing.T) {
|
|||||||
if socialClient.lastFriendApps == nil || socialClient.lastFriendApps.GetUserId() != 10001 || socialClient.lastFriendApps.GetDirection() != "outgoing" || socialClient.lastFriendApps.GetPage() != 2 || socialClient.lastFriendApps.GetPageSize() != 30 {
|
if socialClient.lastFriendApps == nil || socialClient.lastFriendApps.GetUserId() != 10001 || socialClient.lastFriendApps.GetDirection() != "outgoing" || socialClient.lastFriendApps.GetPage() != 2 || socialClient.lastFriendApps.GetPageSize() != 30 {
|
||||||
t.Fatalf("friend application list request mismatch: %+v", socialClient.lastFriendApps)
|
t.Fatalf("friend application list request mismatch: %+v", socialClient.lastFriendApps)
|
||||||
}
|
}
|
||||||
var envelope responseEnvelope
|
var envelope httpkit.ResponseEnvelope
|
||||||
if err := json.Unmarshal(response.Body.Bytes(), &envelope); err != nil || envelope.Code != codeOK {
|
if err := json.Unmarshal(response.Body.Bytes(), &envelope); err != nil || envelope.Code != httpkit.CodeOK {
|
||||||
t.Fatalf("invalid envelope: %+v err=%v", envelope, err)
|
t.Fatalf("invalid envelope: %+v err=%v", envelope, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -89,7 +90,7 @@ func (b taskClaimRequestBody) normalizedCommandID() string {
|
|||||||
// listTaskTabs 返回当前用户任务页;进度由 activity-service 事实表决定。
|
// listTaskTabs 返回当前用户任务页;进度由 activity-service 事实表决定。
|
||||||
func (h *Handler) listTaskTabs(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) listTaskTabs(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.taskClient == nil {
|
if h.taskClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.taskClient.ListUserTasks(request.Context(), &activityv1.ListUserTasksRequest{
|
resp, err := h.taskClient.ListUserTasks(request.Context(), &activityv1.ListUserTasksRequest{
|
||||||
@ -97,7 +98,7 @@ func (h *Handler) listTaskTabs(writer http.ResponseWriter, request *http.Request
|
|||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sections := make([]taskSectionData, 0, len(resp.GetSections()))
|
sections := make([]taskSectionData, 0, len(resp.GetSections()))
|
||||||
@ -113,7 +114,7 @@ func (h *Handler) listTaskTabs(writer http.ResponseWriter, request *http.Request
|
|||||||
NextRefreshAtMS: section.GetNextRefreshAtMs(),
|
NextRefreshAtMS: section.GetNextRefreshAtMs(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{
|
httpkit.WriteOK(writer, request, map[string]any{
|
||||||
"sections": sections,
|
"sections": sections,
|
||||||
"server_time_ms": resp.GetServerTimeMs(),
|
"server_time_ms": resp.GetServerTimeMs(),
|
||||||
"next_refresh_at_ms": resp.GetNextRefreshAtMs(),
|
"next_refresh_at_ms": resp.GetNextRefreshAtMs(),
|
||||||
@ -123,7 +124,7 @@ func (h *Handler) listTaskTabs(writer http.ResponseWriter, request *http.Request
|
|||||||
// claimTaskReward 只透传当前用户身份和幂等命令,完成校验及发奖在 activity-service 内完成。
|
// claimTaskReward 只透传当前用户身份和幂等命令,完成校验及发奖在 activity-service 内完成。
|
||||||
func (h *Handler) claimTaskReward(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) claimTaskReward(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.taskClient == nil {
|
if h.taskClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var body taskClaimRequestBody
|
var body taskClaimRequestBody
|
||||||
@ -135,7 +136,7 @@ func (h *Handler) claimTaskReward(writer http.ResponseWriter, request *http.Requ
|
|||||||
taskDay := body.normalizedTaskDay()
|
taskDay := body.normalizedTaskDay()
|
||||||
commandID := commandIDOrNew(body.normalizedCommandID())
|
commandID := commandIDOrNew(body.normalizedCommandID())
|
||||||
if taskID == "" || taskType == "" || taskDay == "" {
|
if taskID == "" || taskType == "" || taskDay == "" {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.taskClient.ClaimTaskReward(request.Context(), &activityv1.ClaimTaskRewardRequest{
|
resp, err := h.taskClient.ClaimTaskReward(request.Context(), &activityv1.ClaimTaskRewardRequest{
|
||||||
@ -147,10 +148,10 @@ func (h *Handler) claimTaskReward(writer http.ResponseWriter, request *http.Requ
|
|||||||
CommandId: commandID,
|
CommandId: commandID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(writer, request, taskClaimData{
|
httpkit.WriteOK(writer, request, taskClaimData{
|
||||||
ClaimID: resp.GetClaimId(),
|
ClaimID: resp.GetClaimId(),
|
||||||
TaskID: resp.GetTaskId(),
|
TaskID: resp.GetTaskId(),
|
||||||
TaskType: resp.GetTaskType(),
|
TaskType: resp.GetTaskType(),
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package http
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -121,7 +122,7 @@ func (h *Handler) handleTencentIMRoomJoinCallback(writer http.ResponseWriter, re
|
|||||||
resp, err := h.roomGuardClient.VerifyRoomPresence(request.Context(), &roomv1.VerifyRoomPresenceRequest{
|
resp, err := h.roomGuardClient.VerifyRoomPresence(request.Context(), &roomv1.VerifyRoomPresenceRequest{
|
||||||
RoomId: roomID,
|
RoomId: roomID,
|
||||||
UserId: userID,
|
UserId: userID,
|
||||||
RequestId: requestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
})
|
})
|
||||||
if err != nil || !resp.GetPresent() {
|
if err != nil || !resp.GetPresent() {
|
||||||
@ -198,7 +199,7 @@ func (h *Handler) tencentIMCallbackUser(request *http.Request, callbackAppCode s
|
|||||||
// 回调路径不信任客户端 join 参数,每次都回查 user-service 当前用户事实。
|
// 回调路径不信任客户端 join 参数,每次都回查 user-service 当前用户事实。
|
||||||
resp, err := h.userProfileClient.GetUser(appcode.WithContext(request.Context(), callbackAppCode), &userv1.GetUserRequest{
|
resp, err := h.userProfileClient.GetUser(appcode.WithContext(request.Context(), callbackAppCode), &userv1.GetUserRequest{
|
||||||
Meta: &userv1.RequestMeta{
|
Meta: &userv1.RequestMeta{
|
||||||
RequestId: requestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
Caller: "gateway-service",
|
Caller: "gateway-service",
|
||||||
SentAtMs: time.Now().UnixMilli(),
|
SentAtMs: time.Now().UnixMilli(),
|
||||||
AppCode: callbackAppCode,
|
AppCode: callbackAppCode,
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -87,7 +88,7 @@ func (h *Handler) handleTencentRTCCallback(writer http.ResponseWriter, request *
|
|||||||
roomID, ok := parseTencentRTCRoomID(body.EventInfo.RoomID)
|
roomID, ok := parseTencentRTCRoomID(body.EventInfo.RoomID)
|
||||||
userID, userOK := parseTencentIMUserID(body.EventInfo.UserID)
|
userID, userOK := parseTencentIMUserID(body.EventInfo.UserID)
|
||||||
eventTimeMS := tencentRTCEventTimeMS(body)
|
eventTimeMS := tencentRTCEventTimeMS(body)
|
||||||
logCtx := logx.With(request.Context(), slog.String("request_id", requestIDFromContext(request.Context())), slog.String("app_code", tencentRTCCallbackAppCode(request)))
|
logCtx := logx.With(request.Context(), slog.String("request_id", httpkit.RequestIDFromContext(request.Context())), slog.String("app_code", tencentRTCCallbackAppCode(request)))
|
||||||
if !ok || !userOK || eventTimeMS <= 0 || h.roomClient == nil {
|
if !ok || !userOK || eventTimeMS <= 0 || h.roomClient == nil {
|
||||||
logx.Warn(logCtx, "gateway_rtc_callback_ignored",
|
logx.Warn(logCtx, "gateway_rtc_callback_ignored",
|
||||||
slog.String("provider", "tencent"),
|
slog.String("provider", "tencent"),
|
||||||
@ -103,7 +104,7 @@ func (h *Handler) handleTencentRTCCallback(writer http.ResponseWriter, request *
|
|||||||
|
|
||||||
resp, err := h.roomClient.ApplyRTCEvent(request.Context(), &roomv1.ApplyRTCEventRequest{
|
resp, err := h.roomClient.ApplyRTCEvent(request.Context(), &roomv1.ApplyRTCEventRequest{
|
||||||
Meta: &roomv1.RequestMeta{
|
Meta: &roomv1.RequestMeta{
|
||||||
RequestId: requestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
CommandId: tencentRTCCallbackCommandID(body, roomID, userID, eventTimeMS),
|
CommandId: tencentRTCCallbackCommandID(body, roomID, userID, eventTimeMS),
|
||||||
ActorUserId: userID,
|
ActorUserId: userID,
|
||||||
RoomId: roomID,
|
RoomId: roomID,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package http
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -67,12 +68,12 @@ func (h *Handler) uploadFile(writer http.ResponseWriter, request *http.Request)
|
|||||||
|
|
||||||
func (h *Handler) uploadObject(writer http.ResponseWriter, request *http.Request, policy uploadPolicy) {
|
func (h *Handler) uploadObject(writer http.ResponseWriter, request *http.Request, policy uploadPolicy) {
|
||||||
if request.Method != http.MethodPost {
|
if request.Method != http.MethodPost {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if h.objectUploader == nil {
|
if h.objectUploader == nil {
|
||||||
// 上传入口必须 fail-closed,避免客户端拿到伪造 URL 后继续写用户资料。
|
// 上传入口必须 fail-closed,避免客户端拿到伪造 URL 后继续写用户资料。
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +89,7 @@ func (h *Handler) uploadObject(writer http.ResponseWriter, request *http.Request
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
if header.Size <= 0 || header.Size > policy.MaxBytes {
|
if header.Size <= 0 || header.Size > policy.MaxBytes {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,18 +102,18 @@ func (h *Handler) uploadObject(writer http.ResponseWriter, request *http.Request
|
|||||||
contentType = detectedContentType
|
contentType = detectedContentType
|
||||||
}
|
}
|
||||||
if !validateUploadContent(policy, header.Filename, contentType) {
|
if !validateUploadContent(policy, header.Filename, contentType) {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
objectKey := buildUploadObjectKey(policy, auth.UserIDFromContext(request.Context()), header.Filename, contentType, timeNow())
|
objectKey := buildUploadObjectKey(policy, auth.UserIDFromContext(request.Context()), header.Filename, contentType, timeNow())
|
||||||
objectURL, err := h.objectUploader.PutObject(request.Context(), objectKey, io.MultiReader(bytes.NewReader(head), file), header.Size, contentType)
|
objectURL, err := h.objectUploader.PutObject(request.Context(), objectKey, io.MultiReader(bytes.NewReader(head), file), header.Size, contentType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeOK(writer, request, uploadResponse{
|
httpkit.WriteOK(writer, request, uploadResponse{
|
||||||
URL: objectURL,
|
URL: objectURL,
|
||||||
ObjectKey: objectKey,
|
ObjectKey: objectKey,
|
||||||
ContentType: contentType,
|
ContentType: contentType,
|
||||||
@ -123,7 +124,7 @@ func (h *Handler) uploadObject(writer http.ResponseWriter, request *http.Request
|
|||||||
func parseUploadFile(writer http.ResponseWriter, request *http.Request, maxBytes int64) (multipart.File, *multipart.FileHeader, bool) {
|
func parseUploadFile(writer http.ResponseWriter, request *http.Request, maxBytes int64) (multipart.File, *multipart.FileHeader, bool) {
|
||||||
request.Body = http.MaxBytesReader(writer, request.Body, maxBytes+uploadMultipartOverheadLimit)
|
request.Body = http.MaxBytesReader(writer, request.Body, maxBytes+uploadMultipartOverheadLimit)
|
||||||
if err := request.ParseMultipartForm(1 << 20); err != nil {
|
if err := request.ParseMultipartForm(1 << 20); err != nil {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return nil, nil, false
|
return nil, nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,7 +135,7 @@ func parseUploadFile(writer http.ResponseWriter, request *http.Request, maxBytes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return nil, nil, false
|
return nil, nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,7 +143,7 @@ func sniffUploadContent(writer http.ResponseWriter, request *http.Request, file
|
|||||||
head := make([]byte, uploadSniffBytes)
|
head := make([]byte, uploadSniffBytes)
|
||||||
n, err := io.ReadFull(file, head)
|
n, err := io.ReadFull(file, head)
|
||||||
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
|
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return nil, "", "", false
|
return nil, "", "", false
|
||||||
}
|
}
|
||||||
head = head[:n]
|
head = head[:n]
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -87,7 +88,7 @@ type countryData struct {
|
|||||||
// 该接口公开读,不要求 access token;user-service 负责只返回 active 且 enabled 的国家。
|
// 该接口公开读,不要求 access token;user-service 负责只返回 active 且 enabled 的国家。
|
||||||
func (h *Handler) listRegistrationCountries(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) listRegistrationCountries(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.userCountryClient == nil {
|
if h.userCountryClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,7 +96,7 @@ func (h *Handler) listRegistrationCountries(writer http.ResponseWriter, request
|
|||||||
Meta: authRequestMeta(request, ""),
|
Meta: authRequestMeta(request, ""),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,13 +104,13 @@ func (h *Handler) listRegistrationCountries(writer http.ResponseWriter, request
|
|||||||
for _, country := range resp.GetCountries() {
|
for _, country := range resp.GetCountries() {
|
||||||
countries = append(countries, registrationCountryData(country))
|
countries = append(countries, registrationCountryData(country))
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"countries": countries})
|
httpkit.WriteOK(writer, request, map[string]any{"countries": countries})
|
||||||
}
|
}
|
||||||
|
|
||||||
// getMyIdentity 返回当前登录用户的短号状态。
|
// getMyIdentity 返回当前登录用户的短号状态。
|
||||||
func (h *Handler) getMyIdentity(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) getMyIdentity(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.userIdentityClient == nil {
|
if h.userIdentityClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,31 +119,35 @@ func (h *Handler) getMyIdentity(writer http.ResponseWriter, request *http.Reques
|
|||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeOK(writer, request, identityData(resp.GetIdentity(), ""))
|
httpkit.WriteOK(writer, request, identityData(resp.GetIdentity(), ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveDisplayUserID 通过当前有效展示号解析用户。
|
// resolveDisplayUserID 通过当前有效展示号解析用户。
|
||||||
func (h *Handler) resolveDisplayUserID(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) resolveDisplayUserID(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.userIdentityClient == nil {
|
if h.userIdentityClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
displayUserID := strings.TrimPrefix(request.URL.Path, apiV1Prefix+"/users/by-display-user-id/")
|
displayUserID := strings.TrimSpace(request.PathValue("display_user_id"))
|
||||||
|
if displayUserID == "" {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
|
return
|
||||||
|
}
|
||||||
resp, err := h.userIdentityClient.ResolveDisplayUserID(request.Context(), &userv1.ResolveDisplayUserIDRequest{
|
resp, err := h.userIdentityClient.ResolveDisplayUserID(request.Context(), &userv1.ResolveDisplayUserIDRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: authRequestMeta(request, ""),
|
||||||
DisplayUserId: displayUserID,
|
DisplayUserId: displayUserID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeOK(writer, request, identityData(resp.GetIdentity(), ""))
|
httpkit.WriteOK(writer, request, identityData(resp.GetIdentity(), ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
// completeMyOnboarding 是注册页唯一资料提交入口。
|
// completeMyOnboarding 是注册页唯一资料提交入口。
|
||||||
@ -159,7 +164,7 @@ func (h *Handler) completeMyOnboarding(writer http.ResponseWriter, request *http
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if h.userProfileClient == nil {
|
if h.userProfileClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,11 +178,11 @@ func (h *Handler) completeMyOnboarding(writer http.ResponseWriter, request *http
|
|||||||
InviteCode: body.InviteCode,
|
InviteCode: body.InviteCode,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeOK(writer, request, completeOnboardingData{
|
httpkit.WriteOK(writer, request, completeOnboardingData{
|
||||||
userProfileData: profileData(resp.GetUser(), 0),
|
userProfileData: profileData(resp.GetUser(), 0),
|
||||||
Invite: inviteBindingDataFromProto(resp.GetInvite()),
|
Invite: inviteBindingDataFromProto(resp.GetInvite()),
|
||||||
Token: authData(resp.GetToken(), nil),
|
Token: authData(resp.GetToken(), nil),
|
||||||
@ -187,7 +192,7 @@ func (h *Handler) completeMyOnboarding(writer http.ResponseWriter, request *http
|
|||||||
// getMyProfile 返回当前登录用户的基础资料。
|
// getMyProfile 返回当前登录用户的基础资料。
|
||||||
func (h *Handler) getMyProfile(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) getMyProfile(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.userProfileClient == nil {
|
if h.userProfileClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,11 +201,11 @@ func (h *Handler) getMyProfile(writer http.ResponseWriter, request *http.Request
|
|||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeOK(writer, request, profileData(resp.GetUser(), 0))
|
httpkit.WriteOK(writer, request, profileData(resp.GetUser(), 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateMyProfile 修改当前用户的用户名、头像、性别和生日。
|
// updateMyProfile 修改当前用户的用户名、头像、性别和生日。
|
||||||
@ -215,7 +220,7 @@ func (h *Handler) updateMyProfile(writer http.ResponseWriter, request *http.Requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if h.userProfileClient == nil {
|
if h.userProfileClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,11 +233,11 @@ func (h *Handler) updateMyProfile(writer http.ResponseWriter, request *http.Requ
|
|||||||
Birth: body.Birth,
|
Birth: body.Birth,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeOK(writer, request, profileData(resp.GetUser(), 0))
|
httpkit.WriteOK(writer, request, profileData(resp.GetUser(), 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
// changeMyCountry 单独修改当前用户国家;冷却期和审计由 user-service 保证。
|
// changeMyCountry 单独修改当前用户国家;冷却期和审计由 user-service 保证。
|
||||||
@ -244,7 +249,7 @@ func (h *Handler) changeMyCountry(writer http.ResponseWriter, request *http.Requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if h.userProfileClient == nil {
|
if h.userProfileClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,12 +259,12 @@ func (h *Handler) changeMyCountry(writer http.ResponseWriter, request *http.Requ
|
|||||||
Country: body.Country,
|
Country: body.Country,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
h.removeOldRegionBroadcastMemberBestEffort(request, auth.UserIDFromContext(request.Context()), resp.GetOldRegionId(), resp.GetNewRegionId(), "user_country_changed")
|
h.removeOldRegionBroadcastMemberBestEffort(request, auth.UserIDFromContext(request.Context()), resp.GetOldRegionId(), resp.GetNewRegionId(), "user_country_changed")
|
||||||
|
|
||||||
writeOK(writer, request, profileData(resp.GetUser(), resp.GetNextChangeAllowedAtMs()))
|
httpkit.WriteOK(writer, request, profileData(resp.GetUser(), resp.GetNextChangeAllowedAtMs()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) removeOldRegionBroadcastMemberBestEffort(request *http.Request, userID int64, oldRegionID int64, newRegionID int64, reason string) {
|
func (h *Handler) removeOldRegionBroadcastMemberBestEffort(request *http.Request, userID int64, oldRegionID int64, newRegionID int64, reason string) {
|
||||||
@ -302,7 +307,7 @@ func (h *Handler) changeMyDisplayUserID(writer http.ResponseWriter, request *htt
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if h.userIdentityClient == nil {
|
if h.userIdentityClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,11 +319,11 @@ func (h *Handler) changeMyDisplayUserID(writer http.ResponseWriter, request *htt
|
|||||||
OperatorUserId: auth.UserIDFromContext(request.Context()),
|
OperatorUserId: auth.UserIDFromContext(request.Context()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeOK(writer, request, identityData(resp.GetIdentity(), ""))
|
httpkit.WriteOK(writer, request, identityData(resp.GetIdentity(), ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyMyPrettyDisplayUserID 为当前登录用户申请临时靓号。
|
// applyMyPrettyDisplayUserID 为当前登录用户申请临时靓号。
|
||||||
@ -332,7 +337,7 @@ func (h *Handler) applyMyPrettyDisplayUserID(writer http.ResponseWriter, request
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if h.userIdentityClient == nil {
|
if h.userIdentityClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,11 +349,11 @@ func (h *Handler) applyMyPrettyDisplayUserID(writer http.ResponseWriter, request
|
|||||||
PaymentReceiptId: body.PaymentReceiptID,
|
PaymentReceiptId: body.PaymentReceiptID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeOK(writer, request, identityData(resp.GetIdentity(), resp.GetLeaseId()))
|
httpkit.WriteOK(writer, request, identityData(resp.GetIdentity(), resp.GetLeaseId()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func identityData(identity *userv1.UserIdentity, leaseID string) userIdentityData {
|
func identityData(identity *userv1.UserIdentity, leaseID string) userIdentityData {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -12,47 +13,47 @@ import (
|
|||||||
// getMyVIP 返回当前用户 VIP 状态。
|
// getMyVIP 返回当前用户 VIP 状态。
|
||||||
func (h *Handler) getMyVIP(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) getMyVIP(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.walletClient == nil {
|
if h.walletClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.walletClient.GetMyVip(request.Context(), &walletv1.GetMyVipRequest{
|
resp, err := h.walletClient.GetMyVip(request.Context(), &walletv1.GetMyVipRequest{
|
||||||
RequestId: requestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(writer, request, vipFromProto(resp.GetVip()))
|
httpkit.WriteOK(writer, request, vipFromProto(resp.GetVip()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// listVIPPackages 返回可购买 VIP 包和当前会员状态。
|
// listVIPPackages 返回可购买 VIP 包和当前会员状态。
|
||||||
func (h *Handler) listVIPPackages(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) listVIPPackages(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.walletClient == nil {
|
if h.walletClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.walletClient.ListVipPackages(request.Context(), &walletv1.ListVipPackagesRequest{
|
resp, err := h.walletClient.ListVipPackages(request.Context(), &walletv1.ListVipPackagesRequest{
|
||||||
RequestId: requestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
packages := make([]vipPackageData, 0, len(resp.GetPackages()))
|
packages := make([]vipPackageData, 0, len(resp.GetPackages()))
|
||||||
for _, item := range resp.GetPackages() {
|
for _, item := range resp.GetPackages() {
|
||||||
packages = append(packages, vipPackageFromProto(item))
|
packages = append(packages, vipPackageFromProto(item))
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"current_vip": vipFromProto(resp.GetCurrentVip()), "packages": packages})
|
httpkit.WriteOK(writer, request, map[string]any{"current_vip": vipFromProto(resp.GetCurrentVip()), "packages": packages})
|
||||||
}
|
}
|
||||||
|
|
||||||
// purchaseVIP 购买、续期或升级 VIP。
|
// purchaseVIP 购买、续期或升级 VIP。
|
||||||
func (h *Handler) purchaseVIP(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) purchaseVIP(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.walletClient == nil {
|
if h.walletClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var body vipPurchaseRequestBody
|
var body vipPurchaseRequestBody
|
||||||
@ -64,7 +65,7 @@ func (h *Handler) purchaseVIP(writer http.ResponseWriter, request *http.Request)
|
|||||||
commandID = strings.TrimSpace(body.CommandIDAlt)
|
commandID = strings.TrimSpace(body.CommandIDAlt)
|
||||||
}
|
}
|
||||||
if commandID == "" || body.Level <= 0 {
|
if commandID == "" || body.Level <= 0 {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.walletClient.PurchaseVip(request.Context(), &walletv1.PurchaseVipRequest{
|
resp, err := h.walletClient.PurchaseVip(request.Context(), &walletv1.PurchaseVipRequest{
|
||||||
@ -74,10 +75,10 @@ func (h *Handler) purchaseVIP(writer http.ResponseWriter, request *http.Request)
|
|||||||
Level: body.Level,
|
Level: body.Level,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{
|
httpkit.WriteOK(writer, request, map[string]any{
|
||||||
"order_id": resp.GetOrderId(),
|
"order_id": resp.GetOrderId(),
|
||||||
"transaction_id": resp.GetTransactionId(),
|
"transaction_id": resp.GetTransactionId(),
|
||||||
"vip": vipFromProto(resp.GetVip()),
|
"vip": vipFromProto(resp.GetVip()),
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -69,18 +70,18 @@ func (b coinSellerTransferRequestBody) normalizedTargetDisplayUserID() string {
|
|||||||
// getMyBalances 返回当前登录用户的钱包余额。
|
// getMyBalances 返回当前登录用户的钱包余额。
|
||||||
func (h *Handler) getMyBalances(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) getMyBalances(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.walletClient == nil {
|
if h.walletClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.walletClient.GetBalances(request.Context(), &walletv1.GetBalancesRequest{
|
resp, err := h.walletClient.GetBalances(request.Context(), &walletv1.GetBalancesRequest{
|
||||||
RequestId: requestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
AssetTypes: walletAssetTypes(request),
|
AssetTypes: walletAssetTypes(request),
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,13 +94,13 @@ func (h *Handler) getMyBalances(writer http.ResponseWriter, request *http.Reques
|
|||||||
Version: balance.GetVersion(),
|
Version: balance.GetVersion(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
writeOK(writer, request, map[string]any{"balances": balances})
|
httpkit.WriteOK(writer, request, map[string]any{"balances": balances})
|
||||||
}
|
}
|
||||||
|
|
||||||
// transferCoinFromSeller 处理币商给玩家转金币的 App 入口。
|
// transferCoinFromSeller 处理币商给玩家转金币的 App 入口。
|
||||||
func (h *Handler) transferCoinFromSeller(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) transferCoinFromSeller(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.walletClient == nil || h.userHostClient == nil || h.userProfileClient == nil || h.userIdentityClient == nil {
|
if h.walletClient == nil || h.userHostClient == nil || h.userProfileClient == nil || h.userIdentityClient == nil {
|
||||||
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var body coinSellerTransferRequestBody
|
var body coinSellerTransferRequestBody
|
||||||
@ -110,7 +111,7 @@ func (h *Handler) transferCoinFromSeller(writer http.ResponseWriter, request *ht
|
|||||||
targetDisplayUserID := body.normalizedTargetDisplayUserID()
|
targetDisplayUserID := body.normalizedTargetDisplayUserID()
|
||||||
reason := strings.TrimSpace(body.Reason)
|
reason := strings.TrimSpace(body.Reason)
|
||||||
if commandID == "" || targetDisplayUserID == "" || body.Amount <= 0 || reason == "" {
|
if commandID == "" || targetDisplayUserID == "" || body.Amount <= 0 || reason == "" {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,12 +121,12 @@ func (h *Handler) transferCoinFromSeller(writer http.ResponseWriter, request *ht
|
|||||||
UserId: sellerUserID,
|
UserId: sellerUserID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
profile := profileResp.GetCoinSellerProfile()
|
profile := profileResp.GetCoinSellerProfile()
|
||||||
if profile == nil || profile.GetStatus() != "active" || profile.GetMerchantAssetType() != "COIN_SELLER_COIN" {
|
if profile == nil || profile.GetStatus() != "active" || profile.GetMerchantAssetType() != "COIN_SELLER_COIN" {
|
||||||
writeError(writer, request, http.StatusForbidden, codePermissionDenied, "permission denied")
|
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
targetUserID, ok := h.resolveCoinSellerTransferTargetUserID(writer, request, targetDisplayUserID)
|
targetUserID, ok := h.resolveCoinSellerTransferTargetUserID(writer, request, targetDisplayUserID)
|
||||||
@ -148,10 +149,10 @@ func (h *Handler) transferCoinFromSeller(writer http.ResponseWriter, request *ht
|
|||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(writer, request, coinSellerTransferData{
|
httpkit.WriteOK(writer, request, coinSellerTransferData{
|
||||||
TransactionID: resp.GetTransactionId(),
|
TransactionID: resp.GetTransactionId(),
|
||||||
SellerBalanceAfter: resp.GetSellerBalanceAfter(),
|
SellerBalanceAfter: resp.GetSellerBalanceAfter(),
|
||||||
TargetBalanceAfter: resp.GetTargetBalanceAfter(),
|
TargetBalanceAfter: resp.GetTargetBalanceAfter(),
|
||||||
@ -171,13 +172,13 @@ func (h *Handler) resolveCoinSellerTransferTargetUserID(writer http.ResponseWrit
|
|||||||
DisplayUserId: targetDisplayUserID,
|
DisplayUserId: targetDisplayUserID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
identity := resp.GetIdentity()
|
identity := resp.GetIdentity()
|
||||||
targetUserID := identity.GetUserId()
|
targetUserID := identity.GetUserId()
|
||||||
if targetUserID <= 0 {
|
if targetUserID <= 0 {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
return targetUserID, true
|
return targetUserID, true
|
||||||
@ -189,7 +190,7 @@ func (h *Handler) coinSellerTransferRegions(writer http.ResponseWriter, request
|
|||||||
UserId: sellerUserID,
|
UserId: sellerUserID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return 0, 0, false
|
return 0, 0, false
|
||||||
}
|
}
|
||||||
targetResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
targetResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||||
@ -197,18 +198,18 @@ func (h *Handler) coinSellerTransferRegions(writer http.ResponseWriter, request
|
|||||||
UserId: targetUserID,
|
UserId: targetUserID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return 0, 0, false
|
return 0, 0, false
|
||||||
}
|
}
|
||||||
sellerRegionID := sellerResp.GetUser().GetRegionId()
|
sellerRegionID := sellerResp.GetUser().GetRegionId()
|
||||||
targetRegionID := targetResp.GetUser().GetRegionId()
|
targetRegionID := targetResp.GetUser().GetRegionId()
|
||||||
if sellerRegionID <= 0 || targetRegionID <= 0 {
|
if sellerRegionID <= 0 || targetRegionID <= 0 {
|
||||||
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return 0, 0, false
|
return 0, 0, false
|
||||||
}
|
}
|
||||||
if sellerRegionID != targetRegionID {
|
if sellerRegionID != targetRegionID {
|
||||||
// 充值政策当前按单一区域生效;跨区转账必须先设计明确的结算区域,不能静默套用任意一方政策。
|
// 充值政策当前按单一区域生效;跨区转账必须先设计明确的结算区域,不能静默套用任意一方政策。
|
||||||
writeError(writer, request, http.StatusConflict, string(xerr.Conflict), "conflict")
|
httpkit.WriteError(writer, request, http.StatusConflict, string(xerr.Conflict), "conflict")
|
||||||
return 0, 0, false
|
return 0, 0, false
|
||||||
}
|
}
|
||||||
return sellerRegionID, targetRegionID, true
|
return sellerRegionID, targetRegionID, true
|
||||||
@ -222,7 +223,7 @@ func walletAssetTypes(request *http.Request) []string {
|
|||||||
|
|
||||||
assetTypes := make([]string, 0, len(rawValues))
|
assetTypes := make([]string, 0, len(rawValues))
|
||||||
for _, raw := range rawValues {
|
for _, raw := range rawValues {
|
||||||
for _, value := range strings.Split(raw, ",") {
|
for value := range strings.SplitSeq(raw, ",") {
|
||||||
value = strings.TrimSpace(value)
|
value = strings.TrimSpace(value)
|
||||||
if value != "" {
|
if value != "" {
|
||||||
assetTypes = append(assetTypes, value)
|
assetTypes = append(assetTypes, value)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.23-alpine AS builder
|
FROM golang:1.26.3-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
|
|||||||
@ -179,31 +179,25 @@ func (a *App) Run() error {
|
|||||||
}()
|
}()
|
||||||
if a.cfg.PresenceStaleScanInterval > 0 {
|
if a.cfg.PresenceStaleScanInterval > 0 {
|
||||||
// presence worker 只清理本节点已装载 Cell,命令仍走 Room Cell 持久化链路。
|
// presence worker 只清理本节点已装载 Cell,命令仍走 Room Cell 持久化链路。
|
||||||
a.workerWG.Add(1)
|
a.workerWG.Go(func() {
|
||||||
go func() {
|
|
||||||
defer a.workerWG.Done()
|
|
||||||
a.service.RunPresenceStaleWorker(a.workerCtx, a.cfg.PresenceStaleScanInterval)
|
a.service.RunPresenceStaleWorker(a.workerCtx, a.cfg.PresenceStaleScanInterval)
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
if a.cfg.MicPublishScanInterval > 0 {
|
if a.cfg.MicPublishScanInterval > 0 {
|
||||||
// MicUp 只代表业务占麦;后台 worker 负责释放未确认 RTC 发流的 pending_publish 麦位。
|
// MicUp 只代表业务占麦;后台 worker 负责释放未确认 RTC 发流的 pending_publish 麦位。
|
||||||
a.workerWG.Add(1)
|
a.workerWG.Go(func() {
|
||||||
go func() {
|
|
||||||
defer a.workerWG.Done()
|
|
||||||
a.service.RunMicPublishTimeoutWorker(a.workerCtx, a.cfg.MicPublishScanInterval)
|
a.service.RunMicPublishTimeoutWorker(a.workerCtx, a.cfg.MicPublishScanInterval)
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
if a.cfg.OutboxWorker.Enabled {
|
if a.cfg.OutboxWorker.Enabled {
|
||||||
a.workerWG.Add(1)
|
a.workerWG.Go(func() {
|
||||||
go func() {
|
|
||||||
defer a.workerWG.Done()
|
|
||||||
a.service.RunOutboxWorker(a.workerCtx, roomservice.OutboxWorkerOptions{
|
a.service.RunOutboxWorker(a.workerCtx, roomservice.OutboxWorkerOptions{
|
||||||
PollInterval: a.cfg.OutboxWorker.PollInterval,
|
PollInterval: a.cfg.OutboxWorker.PollInterval,
|
||||||
BatchSize: a.cfg.OutboxWorker.BatchSize,
|
BatchSize: a.cfg.OutboxWorker.BatchSize,
|
||||||
PublishTimeout: a.cfg.OutboxWorker.PublishTimeout,
|
PublishTimeout: a.cfg.OutboxWorker.PublishTimeout,
|
||||||
RetryStrategy: a.cfg.OutboxWorker.RetryStrategy,
|
RetryStrategy: a.cfg.OutboxWorker.RetryStrategy,
|
||||||
})
|
})
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
err := a.grpcServer.Serve(a.listener)
|
err := a.grpcServer.Serve(a.listener)
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"slices"
|
||||||
|
|
||||||
roomv1 "hyapp.local/api/proto/room/v1"
|
roomv1 "hyapp.local/api/proto/room/v1"
|
||||||
"hyapp/pkg/appcode"
|
"hyapp/pkg/appcode"
|
||||||
@ -148,10 +149,5 @@ func roomPresenceProjectionFromSnapshot(snapshot *roomv1.RoomSnapshot, updatedAt
|
|||||||
}
|
}
|
||||||
|
|
||||||
func containsUserID(values []int64, target int64) bool {
|
func containsUserID(values []int64, target int64) bool {
|
||||||
for _, value := range values {
|
return slices.Contains(values, target)
|
||||||
if value == target {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
roomv1 "hyapp.local/api/proto/room/v1"
|
roomv1 "hyapp.local/api/proto/room/v1"
|
||||||
@ -137,13 +138,7 @@ func findProtoUser(snapshot *roomv1.RoomSnapshot, userID int64) *roomv1.RoomUser
|
|||||||
|
|
||||||
func containsID(values []int64, target int64) bool {
|
func containsID(values []int64, target int64) bool {
|
||||||
// 快照里的集合以有序数组表达,守卫查询用线性查找即可。
|
// 快照里的集合以有序数组表达,守卫查询用线性查找即可。
|
||||||
for _, value := range values {
|
return slices.Contains(values, target)
|
||||||
if value == target {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneProtoRank(items []state.RankItem) []*roomv1.RankItem {
|
func cloneProtoRank(items []state.RankItem) []*roomv1.RankItem {
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"slices"
|
||||||
|
|
||||||
"hyapp/pkg/appcode"
|
"hyapp/pkg/appcode"
|
||||||
"hyapp/pkg/xerr"
|
"hyapp/pkg/xerr"
|
||||||
@ -85,9 +85,7 @@ func normalizeRoomSeatConfig(config RoomSeatConfig) (RoomSeatConfig, error) {
|
|||||||
seen[value] = true
|
seen[value] = true
|
||||||
allowed = append(allowed, value)
|
allowed = append(allowed, value)
|
||||||
}
|
}
|
||||||
sort.Slice(allowed, func(i int, j int) bool {
|
slices.Sort(allowed)
|
||||||
return allowed[i] < allowed[j]
|
|
||||||
})
|
|
||||||
if len(allowed) == 0 {
|
if len(allowed) == 0 {
|
||||||
return RoomSeatConfig{}, fmt.Errorf("allowed seat counts are empty")
|
return RoomSeatConfig{}, fmt.Errorf("allowed seat counts are empty")
|
||||||
}
|
}
|
||||||
@ -101,10 +99,5 @@ func normalizeRoomSeatConfig(config RoomSeatConfig) (RoomSeatConfig, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func seatCountAllowed(values []int32, target int32) bool {
|
func seatCountAllowed(values []int32, target int32) bool {
|
||||||
for _, value := range values {
|
return slices.Contains(values, target)
|
||||||
if value == target {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -754,9 +755,9 @@ func TestUpdateRoomProfileUpdatesProjectionAndExpandsSeats(t *testing.T) {
|
|||||||
|
|
||||||
resp, err := svc.UpdateRoomProfile(ctx, &roomv1.UpdateRoomProfileRequest{
|
resp, err := svc.UpdateRoomProfile(ctx, &roomv1.UpdateRoomProfileRequest{
|
||||||
Meta: roomservice.NewRequestMeta(roomID, 42),
|
Meta: roomservice.NewRequestMeta(roomID, 42),
|
||||||
RoomName: stringPtr(" New Room "),
|
RoomName: new(" New Room "),
|
||||||
RoomAvatar: stringPtr(" https://cdn.example.com/new.png "),
|
RoomAvatar: new(" https://cdn.example.com/new.png "),
|
||||||
RoomDescription: stringPtr(" new desc "),
|
RoomDescription: new(" new desc "),
|
||||||
SeatCount: int32Ptr(20),
|
SeatCount: int32Ptr(20),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -883,8 +884,8 @@ func TestUpdateRoomProfileRecoveryReplay(t *testing.T) {
|
|||||||
}
|
}
|
||||||
if _, err := serviceA.UpdateRoomProfile(ctx, &roomv1.UpdateRoomProfileRequest{
|
if _, err := serviceA.UpdateRoomProfile(ctx, &roomv1.UpdateRoomProfileRequest{
|
||||||
Meta: roomservice.NewRequestMeta(roomID, 1),
|
Meta: roomservice.NewRequestMeta(roomID, 1),
|
||||||
RoomName: stringPtr("Replay Room"),
|
RoomName: new("Replay Room"),
|
||||||
RoomDescription: stringPtr("replayed desc"),
|
RoomDescription: new("replayed desc"),
|
||||||
SeatCount: int32Ptr(20),
|
SeatCount: int32Ptr(20),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
t.Fatalf("UpdateRoomProfile failed: %v", err)
|
t.Fatalf("UpdateRoomProfile failed: %v", err)
|
||||||
@ -2433,20 +2434,17 @@ func findSeat(snapshot *roomv1.RoomSnapshot, seatNo int32) *roomv1.SeatState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func containsInt64(values []int64, target int64) bool {
|
func containsInt64(values []int64, target int64) bool {
|
||||||
for _, value := range values {
|
return slices.Contains(values, target)
|
||||||
if value == target {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:fix inline
|
||||||
func stringPtr(value string) *string {
|
func stringPtr(value string) *string {
|
||||||
return &value
|
return new(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:fix inline
|
||||||
func int32Ptr(value int32) *int32 {
|
func int32Ptr(value int32) *int32 {
|
||||||
return &value
|
return new(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasSyncEventReason(events []tencentim.RoomEvent, reason string) bool {
|
func hasSyncEventReason(events []tencentim.RoomEvent, reason string) bool {
|
||||||
|
|||||||
@ -2,7 +2,8 @@
|
|||||||
package state
|
package state
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sort"
|
"maps"
|
||||||
|
"slices"
|
||||||
|
|
||||||
roomv1 "hyapp.local/api/proto/room/v1"
|
roomv1 "hyapp.local/api/proto/room/v1"
|
||||||
)
|
)
|
||||||
@ -165,21 +166,13 @@ func (s *RoomState) Clone() *RoomState {
|
|||||||
cloned.OnlineUsers[userID] = &copied
|
cloned.OnlineUsers[userID] = &copied
|
||||||
}
|
}
|
||||||
|
|
||||||
for userID, allowed := range s.AdminUsers {
|
maps.Copy(cloned.AdminUsers, s.AdminUsers)
|
||||||
cloned.AdminUsers[userID] = allowed
|
|
||||||
}
|
|
||||||
|
|
||||||
for userID, banned := range s.BanUsers {
|
maps.Copy(cloned.BanUsers, s.BanUsers)
|
||||||
cloned.BanUsers[userID] = banned
|
|
||||||
}
|
|
||||||
|
|
||||||
for userID, muted := range s.MuteUsers {
|
maps.Copy(cloned.MuteUsers, s.MuteUsers)
|
||||||
cloned.MuteUsers[userID] = muted
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, value := range s.RoomExt {
|
maps.Copy(cloned.RoomExt, s.RoomExt)
|
||||||
cloned.RoomExt[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
return cloned
|
return cloned
|
||||||
}
|
}
|
||||||
@ -244,9 +237,7 @@ func (s *RoomState) ToProto() *roomv1.RoomSnapshot {
|
|||||||
userIDs = append(userIDs, userID)
|
userIDs = append(userIDs, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(userIDs, func(i int, j int) bool {
|
slices.Sort(userIDs)
|
||||||
return userIDs[i] < userIDs[j]
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, userID := range userIDs {
|
for _, userID := range userIDs {
|
||||||
// protobuf 快照只暴露轻量 room user,不包含 user-service 主资料。
|
// protobuf 快照只暴露轻量 room user,不包含 user-service 主资料。
|
||||||
@ -395,10 +386,7 @@ func sortedSetIDs(values map[int64]bool) []int64 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(ids, func(i int, j int) bool {
|
slices.Sort(ids)
|
||||||
// 输出排序保证快照稳定,避免 map 随机顺序影响测试和日志。
|
|
||||||
return ids[i] < ids[j]
|
|
||||||
})
|
|
||||||
|
|
||||||
return ids
|
return ids
|
||||||
}
|
}
|
||||||
@ -412,9 +400,7 @@ func cloneStringMap(input map[string]string) map[string]string {
|
|||||||
// RoomExt 是扩展字段集合,转换时必须复制,避免共享 map 引用。
|
// RoomExt 是扩展字段集合,转换时必须复制,避免共享 map 引用。
|
||||||
cloned := make(map[string]string, len(input))
|
cloned := make(map[string]string, len(input))
|
||||||
|
|
||||||
for key, value := range input {
|
maps.Copy(cloned, input)
|
||||||
cloned[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
return cloned
|
return cloned
|
||||||
}
|
}
|
||||||
|
|||||||
@ -656,12 +656,12 @@ func (r *Repository) ListPendingOutbox(ctx context.Context, limit int) ([]outbox
|
|||||||
// 每条 outbox 记录恢复 envelope,发布器只消费 protobuf 信封。
|
// 每条 outbox 记录恢复 envelope,发布器只消费 protobuf 信封。
|
||||||
var envelopeBytes []byte
|
var envelopeBytes []byte
|
||||||
var record outbox.Record
|
var record outbox.Record
|
||||||
var lockUntil sql.NullInt64
|
var lockUntil sql.Null[int64]
|
||||||
if err := rows.Scan(&record.AppCode, &record.EventID, &record.EventType, &record.RoomID, &record.Status, &record.WorkerID, &lockUntil, &envelopeBytes, &record.CreatedAtMS, &record.RetryCount, &record.LastError); err != nil {
|
if err := rows.Scan(&record.AppCode, &record.EventID, &record.EventType, &record.RoomID, &record.Status, &record.WorkerID, &lockUntil, &envelopeBytes, &record.CreatedAtMS, &record.RetryCount, &record.LastError); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if lockUntil.Valid {
|
if lockUntil.Valid {
|
||||||
record.LockUntilMS = lockUntil.Int64
|
record.LockUntilMS = lockUntil.V
|
||||||
}
|
}
|
||||||
|
|
||||||
var envelope roomeventsv1.EventEnvelope
|
var envelope roomeventsv1.EventEnvelope
|
||||||
@ -725,14 +725,14 @@ func (r *Repository) ClaimPendingOutbox(ctx context.Context, workerID string, li
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var envelopeBytes []byte
|
var envelopeBytes []byte
|
||||||
var record outbox.Record
|
var record outbox.Record
|
||||||
var lockUntilValue sql.NullInt64
|
var lockUntilValue sql.Null[int64]
|
||||||
if err := rows.Scan(&record.AppCode, &record.EventID, &record.EventType, &record.RoomID, &record.Status, &record.WorkerID, &lockUntilValue, &envelopeBytes, &record.CreatedAtMS, &record.RetryCount, &record.LastError); err != nil {
|
if err := rows.Scan(&record.AppCode, &record.EventID, &record.EventType, &record.RoomID, &record.Status, &record.WorkerID, &lockUntilValue, &envelopeBytes, &record.CreatedAtMS, &record.RetryCount, &record.LastError); err != nil {
|
||||||
_ = rows.Close()
|
_ = rows.Close()
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if lockUntilValue.Valid {
|
if lockUntilValue.Valid {
|
||||||
record.LockUntilMS = lockUntilValue.Int64
|
record.LockUntilMS = lockUntilValue.V
|
||||||
}
|
}
|
||||||
|
|
||||||
var envelope roomeventsv1.EventEnvelope
|
var envelope roomeventsv1.EventEnvelope
|
||||||
@ -1286,11 +1286,11 @@ func escapeRoomListLike(value string) string {
|
|||||||
return builder.String()
|
return builder.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func nullableString(value string) sql.NullString {
|
func nullableString(value string) sql.Null[string] {
|
||||||
// 空错误写 NULL,便于 SQL 查询区分没有错误和空字符串。
|
// 空错误写 NULL,便于 SQL 查询区分没有错误和空字符串。
|
||||||
return sql.NullString{
|
return sql.Null[string]{
|
||||||
String: value,
|
V: value,
|
||||||
Valid: value != "",
|
Valid: value != "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -84,7 +84,7 @@ func (r *Repository) OutboxRecord(eventID string) (outbox.Record, bool) {
|
|||||||
|
|
||||||
var envelopeBytes []byte
|
var envelopeBytes []byte
|
||||||
var record outbox.Record
|
var record outbox.Record
|
||||||
var lockUntil sql.NullInt64
|
var lockUntil sql.Null[int64]
|
||||||
if err := row.Scan(&record.AppCode, &record.EventID, &record.EventType, &record.RoomID, &record.Status, &record.WorkerID, &lockUntil, &envelopeBytes, &record.CreatedAtMS, &record.RetryCount, &record.LastError); err != nil {
|
if err := row.Scan(&record.AppCode, &record.EventID, &record.EventType, &record.RoomID, &record.Status, &record.WorkerID, &lockUntil, &envelopeBytes, &record.CreatedAtMS, &record.RetryCount, &record.LastError); err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return outbox.Record{}, false
|
return outbox.Record{}, false
|
||||||
@ -92,7 +92,7 @@ func (r *Repository) OutboxRecord(eventID string) (outbox.Record, bool) {
|
|||||||
r.t.Fatalf("query room outbox record failed: %v", err)
|
r.t.Fatalf("query room outbox record failed: %v", err)
|
||||||
}
|
}
|
||||||
if lockUntil.Valid {
|
if lockUntil.Valid {
|
||||||
record.LockUntilMS = lockUntil.Int64
|
record.LockUntilMS = lockUntil.V
|
||||||
}
|
}
|
||||||
|
|
||||||
var envelope roomeventsv1.EventEnvelope
|
var envelope roomeventsv1.EventEnvelope
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.23-alpine AS builder
|
FROM golang:1.26.3-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
|
|||||||
@ -260,26 +260,22 @@ func (a *App) Run() error {
|
|||||||
a.workerWG.Wait()
|
a.workerWG.Wait()
|
||||||
}()
|
}()
|
||||||
if a.cfg.InviteRechargeWorker.Enabled && a.inviteSvc != nil {
|
if a.cfg.InviteRechargeWorker.Enabled && a.inviteSvc != nil {
|
||||||
a.workerWG.Add(1)
|
a.workerWG.Go(func() {
|
||||||
go func() {
|
|
||||||
defer a.workerWG.Done()
|
|
||||||
a.inviteSvc.RunWalletRechargeWorker(a.workerCtx, inviteservice.WorkerOptions{
|
a.inviteSvc.RunWalletRechargeWorker(a.workerCtx, inviteservice.WorkerOptions{
|
||||||
WorkerID: a.cfg.NodeID,
|
WorkerID: a.cfg.NodeID,
|
||||||
PollInterval: time.Duration(a.cfg.InviteRechargeWorker.PollIntervalSec) * time.Second,
|
PollInterval: time.Duration(a.cfg.InviteRechargeWorker.PollIntervalSec) * time.Second,
|
||||||
BatchSize: a.cfg.InviteRechargeWorker.BatchSize,
|
BatchSize: a.cfg.InviteRechargeWorker.BatchSize,
|
||||||
})
|
})
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
if a.cfg.MicTimeWorker.Enabled && a.micTimeSvc != nil {
|
if a.cfg.MicTimeWorker.Enabled && a.micTimeSvc != nil {
|
||||||
a.workerWG.Add(1)
|
a.workerWG.Go(func() {
|
||||||
go func() {
|
|
||||||
defer a.workerWG.Done()
|
|
||||||
a.micTimeSvc.RunRoomMicWorker(a.workerCtx, mictimeservice.WorkerOptions{
|
a.micTimeSvc.RunRoomMicWorker(a.workerCtx, mictimeservice.WorkerOptions{
|
||||||
WorkerID: a.cfg.NodeID,
|
WorkerID: a.cfg.NodeID,
|
||||||
PollInterval: time.Duration(a.cfg.MicTimeWorker.PollIntervalSec) * time.Second,
|
PollInterval: time.Duration(a.cfg.MicTimeWorker.PollIntervalSec) * time.Second,
|
||||||
BatchSize: a.cfg.MicTimeWorker.BatchSize,
|
BatchSize: a.cfg.MicTimeWorker.BatchSize,
|
||||||
})
|
})
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
err := a.server.Serve(a.listener)
|
err := a.server.Serve(a.listener)
|
||||||
a.health.MarkStopped()
|
a.health.MarkStopped()
|
||||||
|
|||||||
@ -241,7 +241,7 @@ func parseFirebaseCertPublicKey(encodedCert string) (*rsa.PublicKey, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func firebaseCacheExpiry(now func() time.Time, cacheControl string) time.Time {
|
func firebaseCacheExpiry(now func() time.Time, cacheControl string) time.Time {
|
||||||
for _, part := range strings.Split(cacheControl, ",") {
|
for part := range strings.SplitSeq(cacheControl, ",") {
|
||||||
part = strings.TrimSpace(part)
|
part = strings.TrimSpace(part)
|
||||||
if !strings.HasPrefix(strings.ToLower(part), "max-age=") {
|
if !strings.HasPrefix(strings.ToLower(part), "max-age=") {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"maps"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@ -222,8 +223,6 @@ func signFirebaseTestToken(t *testing.T, privateKey *rsa.PrivateKey, kid string,
|
|||||||
|
|
||||||
func cloneFirebaseClaims(input map[string]any) map[string]any {
|
func cloneFirebaseClaims(input map[string]any) map[string]any {
|
||||||
result := make(map[string]any, len(input))
|
result := make(map[string]any, len(input))
|
||||||
for key, value := range input {
|
maps.Copy(result, input)
|
||||||
result[key] = value
|
|
||||||
}
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,8 +44,9 @@ func (a sequenceDisplayUserIDAllocator) NextDisplayUserID(_ int64, attempt int)
|
|||||||
return a.values[attempt]
|
return a.values[attempt]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:fix inline
|
||||||
func strptr(value string) *string {
|
func strptr(value string) *string {
|
||||||
return &value
|
return new(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func containsCountryCode(countries []userdomain.Country, code string) bool {
|
func containsCountryCode(countries []userdomain.Country, code string) bool {
|
||||||
@ -360,7 +361,7 @@ func TestUpdateUserProfile(t *testing.T) {
|
|||||||
now := time.UnixMilli(2000)
|
now := time.UnixMilli(2000)
|
||||||
svc := newUserService(repository, userservice.WithClock(func() time.Time { return now }))
|
svc := newUserService(repository, userservice.WithClock(func() time.Time { return now }))
|
||||||
|
|
||||||
user, err := svc.UpdateUserProfile(context.Background(), 10001, strptr(" new-name "), strptr(" https://cdn.example/a.png "), strptr(" male "), strptr("2000-01-02"))
|
user, err := svc.UpdateUserProfile(context.Background(), 10001, new(" new-name "), new(" https://cdn.example/a.png "), new(" male "), new("2000-01-02"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("UpdateUserProfile failed: %v", err)
|
t.Fatalf("UpdateUserProfile failed: %v", err)
|
||||||
}
|
}
|
||||||
@ -368,11 +369,11 @@ func TestUpdateUserProfile(t *testing.T) {
|
|||||||
t.Fatalf("profile update mismatch: %+v", user)
|
t.Fatalf("profile update mismatch: %+v", user)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = svc.UpdateUserProfile(context.Background(), 10001, nil, nil, nil, strptr("2000/01/02"))
|
_, err = svc.UpdateUserProfile(context.Background(), 10001, nil, nil, nil, new("2000/01/02"))
|
||||||
if !xerr.IsCode(err, xerr.InvalidArgument) {
|
if !xerr.IsCode(err, xerr.InvalidArgument) {
|
||||||
t.Fatalf("expected invalid birth format, got %v", err)
|
t.Fatalf("expected invalid birth format, got %v", err)
|
||||||
}
|
}
|
||||||
_, err = svc.UpdateUserProfile(context.Background(), 10001, nil, nil, nil, strptr("0001-01-01"))
|
_, err = svc.UpdateUserProfile(context.Background(), 10001, nil, nil, nil, new("0001-01-01"))
|
||||||
if !xerr.IsCode(err, xerr.InvalidArgument) {
|
if !xerr.IsCode(err, xerr.InvalidArgument) {
|
||||||
// Go 零值日期虽然能被 time.Parse 接受,但 MySQL DATE 严格模式不能写入,必须在 service 层拒绝。
|
// Go 零值日期虽然能被 time.Parse 接受,但 MySQL DATE 严格模式不能写入,必须在 service 层拒绝。
|
||||||
t.Fatalf("expected invalid birth range, got %v", err)
|
t.Fatalf("expected invalid birth range, got %v", err)
|
||||||
@ -387,23 +388,23 @@ func TestUpdateUserProfile(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "empty username",
|
name: "empty username",
|
||||||
username: strptr(" "),
|
username: new(" "),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "too long username",
|
name: "too long username",
|
||||||
username: strptr(strings.Repeat("a", userdomain.ProfileUsernameMaxRunes+1)),
|
username: new(strings.Repeat("a", userdomain.ProfileUsernameMaxRunes+1)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "too long avatar",
|
name: "too long avatar",
|
||||||
avatar: strptr("https://cdn.example/" + strings.Repeat("a", userdomain.ProfileAvatarMaxRunes)),
|
avatar: new("https://cdn.example/" + strings.Repeat("a", userdomain.ProfileAvatarMaxRunes)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "non http avatar",
|
name: "non http avatar",
|
||||||
avatar: strptr("ftp://cdn.example/a.png"),
|
avatar: new("ftp://cdn.example/a.png"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "empty gender",
|
name: "empty gender",
|
||||||
gender: strptr(" "),
|
gender: new(" "),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range invalidCases {
|
for _, tc := range invalidCases {
|
||||||
@ -415,7 +416,7 @@ func TestUpdateUserProfile(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err = svc.UpdateUserProfile(context.Background(), 10001, nil, strptr(""), nil, strptr(""))
|
user, err = svc.UpdateUserProfile(context.Background(), 10001, nil, new(""), nil, new(""))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("clearing optional profile fields failed: %v", err)
|
t.Fatalf("clearing optional profile fields failed: %v", err)
|
||||||
}
|
}
|
||||||
@ -637,7 +638,7 @@ func TestProfileMutationsRequireCompletedOnboarding(t *testing.T) {
|
|||||||
userservice.WithCountryChangeCooldown(time.Hour),
|
userservice.WithCountryChangeCooldown(time.Hour),
|
||||||
)
|
)
|
||||||
|
|
||||||
if _, err := svc.UpdateUserProfile(ctx, 10001, strptr("new-name"), nil, nil, nil); !xerr.IsCode(err, xerr.ProfileRequired) {
|
if _, err := svc.UpdateUserProfile(ctx, 10001, new("new-name"), nil, nil, nil); !xerr.IsCode(err, xerr.ProfileRequired) {
|
||||||
t.Fatalf("expected PROFILE_REQUIRED for incomplete profile update, got %v", err)
|
t.Fatalf("expected PROFILE_REQUIRED for incomplete profile update, got %v", err)
|
||||||
}
|
}
|
||||||
if _, _, err := svc.ChangeUserCountry(ctx, 10001, "US", "req-before-onboarding"); !xerr.IsCode(err, xerr.ProfileRequired) {
|
if _, _, err := svc.ChangeUserCountry(ctx, 10001, "US", "req-before-onboarding"); !xerr.IsCode(err, xerr.ProfileRequired) {
|
||||||
|
|||||||
@ -73,7 +73,7 @@ func (r *Repository) ReplaceSession(ctx context.Context, oldSessionID string, ne
|
|||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
var oldRevokedAt sql.NullInt64
|
var oldRevokedAt sql.Null[int64]
|
||||||
err = tx.QueryRowContext(ctx, `
|
err = tx.QueryRowContext(ctx, `
|
||||||
SELECT revoked_at_ms
|
SELECT revoked_at_ms
|
||||||
FROM auth_sessions
|
FROM auth_sessions
|
||||||
@ -148,7 +148,7 @@ func (r *Repository) revokeSessionByID(ctx context.Context, sessionID string, re
|
|||||||
func (r *Repository) findSessionByRefreshHash(ctx context.Context, refreshTokenHash string) (authdomain.Session, error) {
|
func (r *Repository) findSessionByRefreshHash(ctx context.Context, refreshTokenHash string) (authdomain.Session, error) {
|
||||||
// refresh token hash 是唯一索引,最多命中一条 session。
|
// refresh token hash 是唯一索引,最多命中一条 session。
|
||||||
var session authdomain.Session
|
var session authdomain.Session
|
||||||
var revokedAt sql.NullInt64
|
var revokedAt sql.Null[int64]
|
||||||
err := r.db.QueryRowContext(ctx, `
|
err := r.db.QueryRowContext(ctx, `
|
||||||
SELECT app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, revoked_at_ms, revoked_reason, revoked_request_id, revoked_by, created_at_ms, updated_at_ms
|
SELECT app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, revoked_at_ms, revoked_reason, revoked_request_id, revoked_by, created_at_ms, updated_at_ms
|
||||||
FROM auth_sessions
|
FROM auth_sessions
|
||||||
@ -162,8 +162,8 @@ func (r *Repository) findSessionByRefreshHash(ctx context.Context, refreshTokenH
|
|||||||
return authdomain.Session{}, err
|
return authdomain.Session{}, err
|
||||||
}
|
}
|
||||||
if revokedAt.Valid {
|
if revokedAt.Valid {
|
||||||
// sql.NullInt64 转换成领域零值语义。
|
// sql.Null[int64] 转换成领域零值语义。
|
||||||
session.RevokedAtMs = revokedAt.Int64
|
session.RevokedAtMs = revokedAt.V
|
||||||
}
|
}
|
||||||
|
|
||||||
return session, nil
|
return session, nil
|
||||||
@ -172,7 +172,7 @@ func (r *Repository) findSessionByRefreshHash(ctx context.Context, refreshTokenH
|
|||||||
func (r *Repository) findSessionByID(ctx context.Context, sessionID string) (authdomain.Session, error) {
|
func (r *Repository) findSessionByID(ctx context.Context, sessionID string) (authdomain.Session, error) {
|
||||||
// session_id 来自已校验 access token 的 sid claim,仍必须回查服务端 session 状态。
|
// session_id 来自已校验 access token 的 sid claim,仍必须回查服务端 session 状态。
|
||||||
var session authdomain.Session
|
var session authdomain.Session
|
||||||
var revokedAt sql.NullInt64
|
var revokedAt sql.Null[int64]
|
||||||
err := r.db.QueryRowContext(ctx, `
|
err := r.db.QueryRowContext(ctx, `
|
||||||
SELECT app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, revoked_at_ms, revoked_reason, revoked_request_id, revoked_by, created_at_ms, updated_at_ms
|
SELECT app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, revoked_at_ms, revoked_reason, revoked_request_id, revoked_by, created_at_ms, updated_at_ms
|
||||||
FROM auth_sessions
|
FROM auth_sessions
|
||||||
@ -186,7 +186,7 @@ func (r *Repository) findSessionByID(ctx context.Context, sessionID string) (aut
|
|||||||
return authdomain.Session{}, err
|
return authdomain.Session{}, err
|
||||||
}
|
}
|
||||||
if revokedAt.Valid {
|
if revokedAt.Valid {
|
||||||
session.RevokedAtMs = revokedAt.Int64
|
session.RevokedAtMs = revokedAt.V
|
||||||
}
|
}
|
||||||
|
|
||||||
return session, nil
|
return session, nil
|
||||||
|
|||||||
@ -35,7 +35,7 @@ func (r *Repository) assertDisplayUserIDAvailable(ctx context.Context, tx *sql.T
|
|||||||
|
|
||||||
func (r *Repository) assertChangeCooldown(ctx context.Context, tx *sql.Tx, command userdomain.DisplayUserIDChangeCommand) error {
|
func (r *Repository) assertChangeCooldown(ctx context.Context, tx *sql.Tx, command userdomain.DisplayUserIDChangeCommand) error {
|
||||||
// 冷却期只统计默认短号修改,不统计靓号 apply/expire。
|
// 冷却期只统计默认短号修改,不统计靓号 apply/expire。
|
||||||
var lastChangedAt sql.NullInt64
|
var lastChangedAt sql.Null[int64]
|
||||||
err := tx.QueryRowContext(ctx, `
|
err := tx.QueryRowContext(ctx, `
|
||||||
SELECT MAX(created_at_ms)
|
SELECT MAX(created_at_ms)
|
||||||
FROM display_user_id_change_logs
|
FROM display_user_id_change_logs
|
||||||
@ -44,7 +44,7 @@ func (r *Repository) assertChangeCooldown(ctx context.Context, tx *sql.Tx, comma
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if lastChangedAt.Valid && command.ChangedAtMs-lastChangedAt.Int64 < command.CooldownMs {
|
if lastChangedAt.Valid && command.ChangedAtMs-lastChangedAt.V < command.CooldownMs {
|
||||||
// 仍在冷却期内时拒绝修改默认短号。
|
// 仍在冷却期内时拒绝修改默认短号。
|
||||||
return xerr.New(xerr.DisplayUserIDCooldown, "display_user_id change is cooling down")
|
return xerr.New(xerr.DisplayUserIDCooldown, "display_user_id change is cooling down")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -43,7 +44,7 @@ func EnsurePrimaryCodeForUser(ctx context.Context, tx *sql.Tx, appCode string, o
|
|||||||
return code, nil
|
return code, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for attempt := 0; attempt < 16; attempt++ {
|
for range 16 {
|
||||||
candidate := generateInviteCode(generatedCodeLength)
|
candidate := generateInviteCode(generatedCodeLength)
|
||||||
_, err := tx.ExecContext(ctx, `
|
_, err := tx.ExecContext(ctx, `
|
||||||
INSERT INTO user_invite_codes (app_code, owner_user_id, code, code_type, status, created_at_ms, updated_at_ms)
|
INSERT INTO user_invite_codes (app_code, owner_user_id, code, code_type, status, created_at_ms, updated_at_ms)
|
||||||
@ -489,12 +490,7 @@ func parseEligibleTypes(raw string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func eligibleRechargeType(types []string, eventType string) bool {
|
func eligibleRechargeType(types []string, eventType string) bool {
|
||||||
for _, item := range types {
|
return slices.Contains(types, eventType)
|
||||||
if item == eventType {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func inviteEventID(eventType string, aggregateID int64) string {
|
func inviteEventID(eventType string, aggregateID int64) string {
|
||||||
|
|||||||
@ -258,7 +258,7 @@ func applyPublishConfirmed(ctx context.Context, tx *sql.Tx, event mictimedomain.
|
|||||||
// RTC/client clocks can arrive slightly earlier than server MicUp time; clamp to avoid negative online windows.
|
// RTC/client clocks can arrive slightly earlier than server MicUp time; clamp to avoid negative online windows.
|
||||||
publishAt = session.SeatStartedAtMs
|
publishAt = session.SeatStartedAtMs
|
||||||
}
|
}
|
||||||
if session.PublishingStartedAtMs.Valid && publishAt <= session.PublishingStartedAtMs.Int64 {
|
if session.PublishingStartedAtMs.Valid && publishAt <= session.PublishingStartedAtMs.V {
|
||||||
if err := updateSessionLastEvent(ctx, tx, event); err != nil {
|
if err := updateSessionLastEvent(ctx, tx, event); err != nil {
|
||||||
return mictimedomain.ApplyResult{}, err
|
return mictimedomain.ApplyResult{}, err
|
||||||
}
|
}
|
||||||
@ -363,10 +363,7 @@ func closeMicSession(ctx context.Context, tx *sql.Tx, session micSessionRow, inp
|
|||||||
seatOccupiedMs := maxInt64(0, input.EndAtMs-session.SeatStartedAtMs)
|
seatOccupiedMs := maxInt64(0, input.EndAtMs-session.SeatStartedAtMs)
|
||||||
micOnlineMs := int64(0)
|
micOnlineMs := int64(0)
|
||||||
if session.PublishingStartedAtMs.Valid {
|
if session.PublishingStartedAtMs.Valid {
|
||||||
publishStart := session.PublishingStartedAtMs.Int64
|
publishStart := max(session.PublishingStartedAtMs.V, session.SeatStartedAtMs)
|
||||||
if publishStart < session.SeatStartedAtMs {
|
|
||||||
publishStart = session.SeatStartedAtMs
|
|
||||||
}
|
|
||||||
micOnlineMs = maxInt64(0, input.EndAtMs-publishStart)
|
micOnlineMs = maxInt64(0, input.EndAtMs-publishStart)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -431,10 +428,7 @@ func incrementDaily(ctx context.Context, tx *sql.Tx, session micSessionRow, inpu
|
|||||||
seatParts := splitByUTCDay(session.SeatStartedAtMs, input.EndAtMs)
|
seatParts := splitByUTCDay(session.SeatStartedAtMs, input.EndAtMs)
|
||||||
micParts := map[string]dayDuration{}
|
micParts := map[string]dayDuration{}
|
||||||
if session.PublishingStartedAtMs.Valid {
|
if session.PublishingStartedAtMs.Valid {
|
||||||
publishStart := session.PublishingStartedAtMs.Int64
|
publishStart := max(session.PublishingStartedAtMs.V, session.SeatStartedAtMs)
|
||||||
if publishStart < session.SeatStartedAtMs {
|
|
||||||
publishStart = session.SeatStartedAtMs
|
|
||||||
}
|
|
||||||
micParts = splitByUTCDay(publishStart, input.EndAtMs)
|
micParts = splitByUTCDay(publishStart, input.EndAtMs)
|
||||||
}
|
}
|
||||||
dates := map[string]struct{}{}
|
dates := map[string]struct{}{}
|
||||||
@ -502,8 +496,8 @@ type micSessionRow struct {
|
|||||||
CurrentSeatNo int32
|
CurrentSeatNo int32
|
||||||
Status string
|
Status string
|
||||||
SeatStartedAtMs int64
|
SeatStartedAtMs int64
|
||||||
PublishingStartedAtMs sql.NullInt64
|
PublishingStartedAtMs sql.Null[int64]
|
||||||
EndedAtMs sql.NullInt64
|
EndedAtMs sql.Null[int64]
|
||||||
LastRoomVersion int64
|
LastRoomVersion int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,7 @@ func (r *Repository) ResolveEnabledCountryByCode(ctx context.Context, countryCod
|
|||||||
// ListRegistrationCountries 返回注册页可选国家,只包含 enabled 的国家。
|
// ListRegistrationCountries 返回注册页可选国家,只包含 enabled 的国家。
|
||||||
|
|
||||||
func (r *Repository) ListRegistrationCountries(ctx context.Context) ([]userdomain.Country, error) {
|
func (r *Repository) ListRegistrationCountries(ctx context.Context) ([]userdomain.Country, error) {
|
||||||
return listCountries(ctx, r.db, userdomain.CountryFilter{Enabled: boolPtr(true)})
|
return listCountries(ctx, r.db, userdomain.CountryFilter{Enabled: new(true)})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolveActiveRegionByCountry 按国家码读取当前 active 区域映射;无显式映射时由 regions.go 返回 GLOBAL。
|
// ResolveActiveRegionByCountry 按国家码读取当前 active 区域映射;无显式映射时由 regions.go 返回 GLOBAL。
|
||||||
@ -130,8 +130,9 @@ func scanCountry(scanner interface{ Scan(dest ...any) error }) (userdomain.Count
|
|||||||
return country, err
|
return country, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:fix inline
|
||||||
func boolPtr(value bool) *bool {
|
func boolPtr(value bool) *bool {
|
||||||
return &value
|
return new(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func canonicalCountryCodes(ctx context.Context, q queryer, countries []string) ([]string, error) {
|
func canonicalCountryCodes(ctx context.Context, q queryer, countries []string) ([]string, error) {
|
||||||
|
|||||||
@ -201,11 +201,11 @@ func lockClaimedRegionRebuildTask(ctx context.Context, tx *sql.Tx, taskID int64,
|
|||||||
|
|
||||||
func scanRegionRebuildTask(scanner interface{ Scan(dest ...any) error }) (userdomain.UserRegionRebuildTask, error) {
|
func scanRegionRebuildTask(scanner interface{ Scan(dest ...any) error }) (userdomain.UserRegionRebuildTask, error) {
|
||||||
var task userdomain.UserRegionRebuildTask
|
var task userdomain.UserRegionRebuildTask
|
||||||
var regionID sql.NullInt64
|
var regionID sql.Null[int64]
|
||||||
var targetRegionID sql.NullInt64
|
var targetRegionID sql.Null[int64]
|
||||||
var lockedBy sql.NullString
|
var lockedBy sql.Null[string]
|
||||||
var lockedUntil sql.NullInt64
|
var lockedUntil sql.Null[int64]
|
||||||
var errorMessage sql.NullString
|
var errorMessage sql.Null[string]
|
||||||
err := scanner.Scan(
|
err := scanner.Scan(
|
||||||
&task.AppCode,
|
&task.AppCode,
|
||||||
&task.TaskID,
|
&task.TaskID,
|
||||||
@ -226,19 +226,19 @@ func scanRegionRebuildTask(scanner interface{ Scan(dest ...any) error }) (userdo
|
|||||||
return userdomain.UserRegionRebuildTask{}, err
|
return userdomain.UserRegionRebuildTask{}, err
|
||||||
}
|
}
|
||||||
if regionID.Valid {
|
if regionID.Valid {
|
||||||
task.RegionID = regionID.Int64
|
task.RegionID = regionID.V
|
||||||
}
|
}
|
||||||
if targetRegionID.Valid {
|
if targetRegionID.Valid {
|
||||||
task.TargetRegionID = targetRegionID.Int64
|
task.TargetRegionID = targetRegionID.V
|
||||||
}
|
}
|
||||||
if lockedBy.Valid {
|
if lockedBy.Valid {
|
||||||
task.LockedBy = lockedBy.String
|
task.LockedBy = lockedBy.V
|
||||||
}
|
}
|
||||||
if lockedUntil.Valid {
|
if lockedUntil.Valid {
|
||||||
task.LockedUntilMs = lockedUntil.Int64
|
task.LockedUntilMs = lockedUntil.V
|
||||||
}
|
}
|
||||||
if errorMessage.Valid {
|
if errorMessage.Valid {
|
||||||
task.ErrorMessage = errorMessage.String
|
task.ErrorMessage = errorMessage.V
|
||||||
}
|
}
|
||||||
|
|
||||||
return task, nil
|
return task, nil
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -387,11 +388,5 @@ func unionCountryCodes(left []string, right []string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func stringSliceContains(values []string, target string) bool {
|
func stringSliceContains(values []string, target string) bool {
|
||||||
for _, value := range values {
|
return slices.Contains(values, target)
|
||||||
if value == target {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.23-alpine AS builder
|
FROM golang:1.26.3-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -1127,10 +1128,7 @@ func (r *Repository) applyEntitlement(ctx context.Context, tx *sql.Tx, userID in
|
|||||||
newRemaining += quantity
|
newRemaining += quantity
|
||||||
newQuantity += quantity
|
newQuantity += quantity
|
||||||
if durationMS > 0 {
|
if durationMS > 0 {
|
||||||
base := nowMs
|
base := max(existing.ExpiresAtMS, nowMs)
|
||||||
if existing.ExpiresAtMS > nowMs {
|
|
||||||
base = existing.ExpiresAtMS
|
|
||||||
}
|
|
||||||
if durationMS > math.MaxInt64-base {
|
if durationMS > math.MaxInt64-base {
|
||||||
return "", xerr.New(xerr.InvalidArgument, "duration overflow")
|
return "", xerr.New(xerr.InvalidArgument, "duration overflow")
|
||||||
}
|
}
|
||||||
@ -2325,9 +2323,7 @@ func normalizeRegionIDs(values []int64) []int64 {
|
|||||||
seen[value] = struct{}{}
|
seen[value] = struct{}{}
|
||||||
out = append(out, value)
|
out = append(out, value)
|
||||||
}
|
}
|
||||||
sort.Slice(out, func(left int, right int) bool {
|
slices.Sort(out)
|
||||||
return out[left] < out[right]
|
|
||||||
})
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user