diff --git a/README.md b/README.md index 97390ce..a949544 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ X-Request-ID: X-Device-ID: ``` -默认租户通过 `OLONS_SERVER_DEFAULT_TENANT_ID` 配置,现有 `olons_admin` 数据使用 `tenant_id=0`。服务不接受客户端传入租户身份。 +服务采用单租户模式,登录仅按手机号查找账号,不读取或接受租户环境变量。现有数据库中的 `tenant_id` 字段仅作为兼容字段保留,新注册数据固定写入 `0`。 ## 验证 diff --git a/configs/olons_server.local.json b/configs/olons_server.local.json index 730defc..c6a7dad 100644 --- a/configs/olons_server.local.json +++ b/configs/olons_server.local.json @@ -18,7 +18,6 @@ "api_keys": [], "session_key_pattern": "olons_server:session:token:%s", "session_ttl_seconds": 1209600, - "default_tenant_id": 0, "finance_master_key": "olons-finance-master-key-v1-20261013", "allow_dev_header_auth": true, "required_api_key": false diff --git a/configs/olons_server.test.json b/configs/olons_server.test.json index 1e9d004..898a0f9 100644 --- a/configs/olons_server.test.json +++ b/configs/olons_server.test.json @@ -18,7 +18,6 @@ "api_keys": [], "session_key_pattern": "olons_server:session:token:%s", "session_ttl_seconds": 1209600, - "default_tenant_id": 0, "finance_master_key": "olons-finance-master-key-v1-20261013", "required_api_key": true } diff --git a/docs/移动端任务与账户平台前后端对接文档.md b/docs/移动端任务与账户平台前后端对接文档.md index 8c50863..c374a7c 100644 --- a/docs/移动端任务与账户平台前后端对接文档.md +++ b/docs/移动端任务与账户平台前后端对接文档.md @@ -54,7 +54,7 @@ | `Authorization: Bearer ` | 受保护接口必填 | 登录成功后取得的用户会话 | | `Idempotency-Key` | 取出提交必填 | 8–80 位请求唯一键;相同业务重试必须复用,不同请求禁止复用 | -`tenant_id` 由后端部署配置确定。客户端不得在 Header、Query 或 Body 中传入租户身份。 +服务采用单租户模式,客户端不得在 Header、Query 或 Body 中传入租户身份。登录仅按手机号查找账号;数据库中的 `tenant_id` 仅作为兼容字段保留。 ### 3.3 登录会话 diff --git a/internal/accounts/profile_test.go b/internal/accounts/profile_test.go index fa07a19..a0550b2 100644 --- a/internal/accounts/profile_test.go +++ b/internal/accounts/profile_test.go @@ -19,7 +19,7 @@ type profileRepository struct { func (r *profileRepository) RegisterAccount(context.Context, store.AccountRegistration) (*store.Account, error) { return nil, errors.New("not implemented") } -func (r *profileRepository) AccountByMobile(context.Context, int64, string) (*store.Account, error) { +func (r *profileRepository) AccountByMobile(context.Context, string) (*store.Account, error) { return nil, errors.New("not implemented") } func (r *profileRepository) RecordLogin(context.Context, int64, int64) error { return nil } diff --git a/internal/accounts/service.go b/internal/accounts/service.go index 5ee0762..1af1e17 100644 --- a/internal/accounts/service.go +++ b/internal/accounts/service.go @@ -11,7 +11,6 @@ import ( "fmt" "math/big" "regexp" - "strconv" "strings" "time" "unicode" @@ -47,7 +46,7 @@ var ( type Repository interface { RegisterAccount(context.Context, store.AccountRegistration) (*store.Account, error) - AccountByMobile(context.Context, int64, string) (*store.Account, error) + AccountByMobile(context.Context, string) (*store.Account, error) RecordLogin(context.Context, int64, int64) error MemberOverview(context.Context, int64, int64) (*store.MemberOverview, error) UpdateAvatar(context.Context, int64, int64, int64) (*store.Avatar, error) @@ -179,7 +178,7 @@ func (s *Service) Register(ctx context.Context, input RegisterInput) (*Result, e return nil, err } account, err := s.repository.RegisterAccount(ctx, store.AccountRegistration{ - TenantID: s.cfg.DefaultTenantID, + TenantID: 0, MemberNo: memberNo, Username: input.Username, Mobile: input.Phone, @@ -237,7 +236,7 @@ func (s *Service) Login(ctx context.Context, input LoginInput) (*Result, error) if attempts, err := s.redis.Get(ctx, rateKey).Int(); err == nil && attempts >= 10 { return nil, ErrRateLimited } - account, err := s.repository.AccountByMobile(ctx, s.cfg.DefaultTenantID, phone) + account, err := s.repository.AccountByMobile(ctx, phone) if err != nil { if errors.Is(err, store.ErrInvalidCredentials) { s.recordLoginFailure(ctx, rateKey) @@ -387,7 +386,7 @@ func (s *Service) issueSession(ctx context.Context, account *store.Account, devi func (s *Service) loginRateKey(phone string) string { digest := sha256.Sum256([]byte(phone)) - return "olons_server:auth:login-rate:" + strconv.FormatInt(s.cfg.DefaultTenantID, 10) + ":" + hex.EncodeToString(digest[:]) + return "olons_server:auth:login-rate:" + hex.EncodeToString(digest[:]) } func (s *Service) recordLoginFailure(ctx context.Context, key string) { diff --git a/internal/accounts/service_test.go b/internal/accounts/service_test.go index 3ba0674..28db536 100644 --- a/internal/accounts/service_test.go +++ b/internal/accounts/service_test.go @@ -14,7 +14,7 @@ type fakeRepository struct{} func (fakeRepository) RegisterAccount(context.Context, store.AccountRegistration) (*store.Account, error) { return nil, errors.New("should not be called") } -func (fakeRepository) AccountByMobile(context.Context, int64, string) (*store.Account, error) { +func (fakeRepository) AccountByMobile(context.Context, string) (*store.Account, error) { return nil, errors.New("should not be called") } func (fakeRepository) RecordLogin(context.Context, int64, int64) error { diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 7fff5f0..7ccd513 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -46,17 +46,13 @@ func New(cfg config.AuthConfig, redisClient *redis.Client) *Authenticator { return &Authenticator{cfg: cfg, redis: redisClient} } -func (a *Authenticator) DefaultTenantID() int64 { - return a.cfg.DefaultTenantID -} - func (a *Authenticator) Authenticate(req *http.Request) (*Principal, error) { if err := a.ValidateAPIKey(req); err != nil { return nil, err } if a.cfg.AllowDevHeaderAuth { if memberID := parseInt64Header(req, "X-Member-ID"); memberID > 0 { - return &Principal{TenantID: a.cfg.DefaultTenantID, MemberID: memberID, + return &Principal{TenantID: 0, MemberID: memberID, DeviceID: strings.TrimSpace(req.Header.Get("X-Device-ID"))}, nil } } diff --git a/internal/config/config.go b/internal/config/config.go index 2d70cd3..797a54f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -67,7 +67,6 @@ type AuthConfig struct { APIKeys []string `json:"api_keys"` SessionKeyPattern string `json:"session_key_pattern"` SessionTTLSeconds int `json:"session_ttl_seconds"` - DefaultTenantID int64 `json:"default_tenant_id"` DataEncryptionKey string `json:"data_encryption_key"` LookupHMACKey string `json:"lookup_hmac_key"` FinanceMasterKey string `json:"finance_master_key"` @@ -106,7 +105,6 @@ func defaultConfig() *Config { Auth: AuthConfig{ SessionKeyPattern: "olons_server:session:token:%s", SessionTTLSeconds: 14 * 24 * 60 * 60, - DefaultTenantID: 0, RequiredAPIKey: true, }, Upload: UploadConfig{Driver: "local", Dir: "storage/uploads", PublicPath: "/uploads", Region: "us-east-1"}, @@ -133,7 +131,6 @@ func applyEnv(cfg *Config) { setInt(&cfg.Redis.DB, "OLONS_SERVER_REDIS_DB") setString(&cfg.Auth.SessionKeyPattern, "OLONS_SERVER_SESSION_KEY_PATTERN") setInt(&cfg.Auth.SessionTTLSeconds, "OLONS_SERVER_SESSION_TTL_SECONDS") - setInt64(&cfg.Auth.DefaultTenantID, "OLONS_SERVER_DEFAULT_TENANT_ID") setString(&cfg.Auth.DataEncryptionKey, "OLONS_SERVER_DATA_ENCRYPTION_KEY") setString(&cfg.Auth.LookupHMACKey, "OLONS_SERVER_LOOKUP_HMAC_KEY") setString(&cfg.Auth.FinanceMasterKey, "OLONS_SERVER_FINANCE_MASTER_KEY") @@ -177,9 +174,6 @@ func (c *Config) validate() error { if c.Auth.SessionTTLSeconds <= 0 { c.Auth.SessionTTLSeconds = 14 * 24 * 60 * 60 } - if c.Auth.DefaultTenantID < 0 { - return errors.New("default tenant id must not be negative") - } if err := ValidateUpload(c.Upload); err != nil { return err } @@ -261,14 +255,6 @@ func setInt(target *int, key string) { } } -func setInt64(target *int64, key string) { - if value := strings.TrimSpace(os.Getenv(key)); value != "" { - if parsed, err := strconv.ParseInt(value, 10, 64); err == nil { - *target = parsed - } - } -} - func setBool(target *bool, key string) { switch strings.ToLower(strings.TrimSpace(os.Getenv(key))) { case "1", "true", "yes", "on": diff --git a/internal/config/config_test.go b/internal/config/config_test.go index a8cd467..fe77552 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -7,9 +7,6 @@ func TestDefaultConfigUsesOlonsAdmin(t *testing.T) { if err := cfg.validate(); err != nil { t.Fatal(err) } - if cfg.Auth.DefaultTenantID != 0 { - t.Fatalf("DefaultTenantID = %d", cfg.Auth.DefaultTenantID) - } } func TestConfigRejectsOtherDatabase(t *testing.T) { diff --git a/internal/httpapi/portal.go b/internal/httpapi/portal.go index ad0ebe5..6a9f6ed 100644 --- a/internal/httpapi/portal.go +++ b/internal/httpapi/portal.go @@ -85,7 +85,7 @@ func (a *API) content(w http.ResponseWriter, r *http.Request) { writePortalError(w, r, portal.ErrContentKeyInvalid) return } - tenantID := a.auth.DefaultTenantID() + tenantID := int64(0) if _, public := publicContentKeys[key]; !public { principal, ok := a.authenticateMember(w, r) if !ok { diff --git a/internal/store/accounts.go b/internal/store/accounts.go index 09bbead..79af28e 100644 --- a/internal/store/accounts.go +++ b/internal/store/accounts.go @@ -95,11 +95,11 @@ func (s *Store) RegisterAccount(ctx context.Context, input AccountRegistration) InviteCode: input.InviteCode, MemberStatus: memberStatusActive, MemberCategory: memberCategoryReal}, nil } -func (s *Store) AccountByMobile(ctx context.Context, tenantID int64, mobile string) (*Account, error) { +func (s *Store) AccountByMobile(ctx context.Context, mobile string) (*Account, error) { if s == nil || s.db == nil { return nil, errors.New("store is unavailable") } - return accountByMobile(ctx, s.db, tenantID, mobile) + return accountByMobile(ctx, s.db, mobile) } func (s *Store) RecordLogin(ctx context.Context, tenantID, memberID int64) error { @@ -112,11 +112,11 @@ type queryer interface { GetContext(context.Context, any, string, ...any) error } -func accountByMobile(ctx context.Context, db queryer, tenantID int64, mobile string) (*Account, error) { +func accountByMobile(ctx context.Context, db queryer, mobile string) (*Account, error) { var row Account err := db.GetContext(ctx, &row, `SELECT tenant_id, id AS member_id, member_no, username, mobile, password_hash, invite_code, member_status, risk_status AS member_category - FROM hg_app_member WHERE tenant_id=? AND mobile=? AND deleted_at IS NULL LIMIT 1`, tenantID, mobile) + FROM hg_app_member WHERE mobile=? AND deleted_at IS NULL LIMIT 1`, mobile) if errors.Is(err, sql.ErrNoRows) { return nil, ErrInvalidCredentials } diff --git a/internal/store/accounts_test.go b/internal/store/accounts_test.go new file mode 100644 index 0000000..af500cf --- /dev/null +++ b/internal/store/accounts_test.go @@ -0,0 +1,37 @@ +package store + +import ( + "context" + "strings" + "testing" +) + +type accountQueryRecorder struct { + query string + args []any +} + +func (r *accountQueryRecorder) GetContext(_ context.Context, dest any, query string, args ...any) error { + r.query = query + r.args = args + account := dest.(*Account) + *account = Account{MemberID: 9, Mobile: "13311111111", MemberStatus: memberStatusActive} + return nil +} + +func TestAccountByMobileDoesNotFilterTenant(t *testing.T) { + recorder := &accountQueryRecorder{} + account, err := accountByMobile(context.Background(), recorder, "13311111111") + if err != nil { + t.Fatal(err) + } + if account.MemberID != 9 { + t.Fatalf("MemberID = %d, want 9", account.MemberID) + } + if strings.Contains(strings.ToLower(recorder.query), "tenant_id=?") { + t.Fatalf("query must not filter tenant: %s", recorder.query) + } + if len(recorder.args) != 1 || recorder.args[0] != "13311111111" { + t.Fatalf("query args = %#v", recorder.args) + } +}