⚠️ 应用类型约束:当前实现仅支持「钉钉登录-企业内部应用」(DingTalk 开放平台
internal_app 类型)。第三方个人应用、第三方企业应用类型暂不支持——OAuth 流程
相同但 corp 校验、跨企业行为不同。backend 通过 DingTalkAppKind 校验对非
internal_app 类型 fail-closed(硬约束)。
钉钉 OAuth 登录主链
- 4 步 OAuth 链:ExchangeCodeForUserToken / GetUnionIdByUserToken /
GetUserIdByUnionId / GetStaffInfoByUserId;app token 缓存
- pending session 机制持久化 OAuth 中间态;cookie-only token 持久化
- 三种分流:bind_login_required / email_completion / choose_account_action
- corp_restriction_policy 支持 none + internal_only;stale "whitelist" 在
加载层与写入层均静默 coerce 为 none + slog.Warn
- bypass_registration 开关:企业内部模式豁免全局 REGISTRATION_DISABLED
- isReservedEmail / signup_source / canUnbindProvider / OAuth pending flow
等横切点支持 dingtalk provider
- migration 136:4 表 CHECK 约束加入 'dingtalk' provider 值
internal_only 模式同步企业邮箱/姓名/部门到用户属性
- SyncCorpEmail / SyncDisplayName / SyncDept 三个独立开关 + 对应
SyncXxxAttrKey 目标属性 key(默认 dingtalk_email / dingtalk_name /
dingtalk_department);非 internal_only policy 在写入层与加载层均
coerce 为 false,admin handler 与 setting_service 双层兜底
- 同步语义:首次注册写 users.username(昵称优先 → 企业姓名 fallback),
之后每次登录刷新 3 个属性;空值也写入以覆盖旧值
- 邮箱三级 fallback:org_email > email > extension["企业邮箱"]
(钉钉自定义字段 JSON)
- 部门路径递归向上拼接,跳过 dept_id=1 选首个真实子部门,剥离根组织名
- GetUnionIdByUserToken 同时返回 OIDC /contact/users/me 的 nick 字段;
新增 GetDeptInfo 调用 OAPI /topapi/v2/department/get
- AuthHandler 注入 UserAttributeService;OAuth pending flow 在
createPendingOAuthAccount / bindPendingOAuthLogin 分别派发到
AfterRegistration(syncUsername=true)/ AfterLogin
- migration 137 seed dingtalk_email/name/department 三个用户属性定义
附带修复(同集成路径暴露的两个 OAuth 注册回归)
- LoginOrRegisterOAuthWithTokenPair 新建用户分支用 inferLegacySignupSource
覆写 caller 显式传入的 signupSource,导致 dingtalk/linuxdo/oidc/wechat
渠道授权按 email 渠道读取;改为只在 caller 未显式传入时回退邮箱推断
- mergeProviderDefaultGrantSettings 把 parse fallback 默认值
(Concurrency=5 / Balance=0) 当作"未配置"哨兵,admin 显式设 5 时被误判
退回全局默认(复现:全局默认 1 + 渠道默认并发 5 + grant_on_signup → 新
用户实际 concurrency=1);去掉哨兵,admin 任何 >=0 值都覆盖 globalDefaults
前端
- DingTalk Login / Callback / EmailCompletion / ChoiceAccount / Error
视图;router + auth API client
- admin SettingsView:corp policy radio(none / internal_only)+ bypass
注册开关 + i18n;internal_only 下展示三同步开关 + 目标 attr key 下拉
(拉取 user attribute definitions),展示 fieldEmail /
qyapi_get_department_list 钉钉权限申请提示
- Profile:S1 主动绑定 / S5 解绑钉钉按钮 + 合成邮箱防自锁
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Database Migrations
Overview
This directory contains SQL migration files for database schema changes. The migration system uses SHA256 checksums to ensure migration immutability and consistency across environments.
Migration File Naming
Format: NNN_description.sql
NNN: Sequential number (e.g., 001, 002, 003)description: Brief description in snake_case
Example: 017_add_gemini_tier_id.sql
_notx.sql 命名与执行语义(并发索引专用)
当迁移包含 CREATE INDEX CONCURRENTLY 或 DROP INDEX CONCURRENTLY 时,必须使用 _notx.sql 后缀,例如:
062_add_accounts_priority_indexes_notx.sql063_drop_legacy_indexes_notx.sql
运行规则:
*.sql(不带_notx)按事务执行。*_notx.sql按非事务执行,不会包裹在BEGIN/COMMIT中。*_notx.sql仅允许并发索引语句,不允许混入事务控制语句或其他 DDL/DML。
幂等要求(必须):
- 创建索引:
CREATE INDEX CONCURRENTLY IF NOT EXISTS ... - 删除索引:
DROP INDEX CONCURRENTLY IF EXISTS ...
这样可以保证灾备重放、重复执行时不会因对象已存在/不存在而失败。
Migration File Structure
This project uses a custom migration runner (internal/repository/migrations_runner.go) that executes the full SQL file content as-is.
- Regular migrations (
*.sql): executed in a transaction. - Non-transactional migrations (
*_notx.sql): split by statement and executed without transaction (forCONCURRENTLY).
-- Forward-only migration (recommended)
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS example_column VARCHAR(100);
⚠️ Do not place executable "Down" SQL in the same file. The runner does not parse goose Up/Down sections and will execute all SQL statements in the file.
Important Rules
⚠️ Immutability Principle
Once a migration is applied to ANY environment (dev, staging, production), it MUST NOT be modified.
Why?
- Each migration has a SHA256 checksum stored in the
schema_migrationstable - Modifying an applied migration causes checksum mismatch errors
- Different environments would have inconsistent database states
- Breaks audit trail and reproducibility
✅ Correct Workflow
-
Create new migration
# Create new file with next sequential number touch migrations/018_your_change.sql -
Write forward-only migration SQL
- Put only the intended schema change in the file
- If rollback is needed, create a new migration file to revert
-
Test locally
# Apply migration make migrate-up # Test rollback make migrate-down -
Commit and deploy
git add migrations/018_your_change.sql git commit -m "feat(db): add your change"
❌ What NOT to Do
- ❌ Modify an already-applied migration file
- ❌ Delete migration files
- ❌ Change migration file names
- ❌ Reorder migration numbers
🔧 If You Accidentally Modified an Applied Migration
Error message:
migration 017_add_gemini_tier_id.sql checksum mismatch (db=abc123... file=def456...)
Solution:
# 1. Find the original version
git log --oneline -- migrations/017_add_gemini_tier_id.sql
# 2. Revert to the commit when it was first applied
git checkout <commit-hash> -- migrations/017_add_gemini_tier_id.sql
# 3. Create a NEW migration for your changes
touch migrations/018_your_new_change.sql
Migration System Details
- Checksum Algorithm: SHA256 of trimmed file content
- Tracking Table:
schema_migrations(filename, checksum, applied_at) - Runner:
internal/repository/migrations_runner.go - Auto-run: Migrations run automatically on service startup
Best Practices
-
Keep migrations small and focused
- One logical change per migration
- Easier to review and rollback
-
Write reversible migrations
- Always provide a working Down migration
- Test rollback before committing
-
Use transactions
- Wrap DDL statements in transactions when possible
- Ensures atomicity
-
Add comments
- Explain WHY the change is needed
- Document any special considerations
-
Test in development first
- Apply migration locally
- Verify data integrity
- Test rollback
Example Migration
-- Add tier_id field to Gemini OAuth accounts for quota tracking
UPDATE accounts
SET credentials = jsonb_set(
credentials,
'{tier_id}',
'"LEGACY"',
true
)
WHERE platform = 'gemini'
AND type = 'oauth'
AND credentials->>'tier_id' IS NULL;
Troubleshooting
Checksum Mismatch
See "If You Accidentally Modified an Applied Migration" above.
Migration Failed
# Check migration status
psql -d sub2api -c "SELECT * FROM schema_migrations ORDER BY applied_at DESC;"
# Manually rollback if needed (use with caution)
# Better to fix the migration and create a new one
Need to Skip a Migration (Emergency Only)
-- DANGEROUS: Only use in development or with extreme caution
INSERT INTO schema_migrations (filename, checksum, applied_at)
VALUES ('NNN_migration.sql', 'calculated_checksum', NOW());
References
- Migration runner:
internal/repository/migrations_runner.go - PostgreSQL docs: https://www.postgresql.org/docs/