@@ -2,17 +2,8 @@ package bootstrap
22
33import (
44 "context"
5- "crypto/sha256"
6- stdsql "database/sql"
7- "embed"
8- "encoding/hex"
9- "fmt"
10- "io/fs"
115 "log/slog"
12- "path/filepath"
13- "sort"
146 "strings"
15- "time"
167
178 entsql "entgo.io/ent/dialect/sql"
189
@@ -23,277 +14,17 @@ import (
2314 "github.com/DouDOU-start/airgate-core/internal/auth"
2415)
2516
26- //go:embed migrations/*.sql
27- var startupMigrationFiles embed.FS
28-
2917// RunStartupTasks 运行启动阶段的整理任务。
3018func RunStartupTasks (db * ent.Client , drv * entsql.Driver , apiKeySecret string ) {
3119 slog .Info ("bootstrap_startup_tasks_start" )
3220 backfillKeyHints (db , apiKeySecret )
3321 backfillResellerMarkupColumns (drv )
3422 migrateAccountState (drv )
3523 migrateUserHistoryRefs (drv )
36- runStartupMigrations (drv )
24+ runSystemUpgrades (drv )
3725 slog .Info ("bootstrap_startup_tasks_done" )
3826}
3927
40- type startupMigration struct {
41- ID string
42- Description string
43- Checksum string
44- SQL string
45- }
46-
47- func runStartupMigrations (drv * entsql.Driver ) {
48- if drv == nil {
49- return
50- }
51- migrations := loadStartupMigrations ()
52- if len (migrations ) == 0 {
53- return
54- }
55-
56- ctx := context .Background ()
57- conn , err := drv .DB ().Conn (ctx )
58- if err != nil {
59- panicStartupMigration ("open startup migration connection" , err )
60- }
61- defer func () { _ = conn .Close () }()
62-
63- const createTableSQL = `CREATE TABLE IF NOT EXISTS public.bootstrap_migrations (
64- id text PRIMARY KEY,
65- description text NOT NULL DEFAULT '',
66- checksum text NOT NULL DEFAULT '',
67- applied_at timestamptz NOT NULL DEFAULT now(),
68- duration_ms bigint NOT NULL DEFAULT 0
69- )`
70- if _ , err := conn .ExecContext (ctx , createTableSQL ); err != nil {
71- panicStartupMigration ("create bootstrap_migrations table" , err )
72- }
73- if _ , err := conn .ExecContext (ctx , `ALTER TABLE public.bootstrap_migrations ADD COLUMN IF NOT EXISTS checksum text NOT NULL DEFAULT ''` ); err != nil {
74- panicStartupMigration ("add bootstrap_migrations checksum column" , err )
75- }
76-
77- const lockKey int64 = 2026052809517
78- if _ , err := conn .ExecContext (ctx , `SELECT pg_advisory_lock($1)` , lockKey ); err != nil {
79- panicStartupMigration ("lock startup migrations" , err )
80- }
81- defer func () {
82- if _ , err := conn .ExecContext (context .Background (), `SELECT pg_advisory_unlock($1)` , lockKey ); err != nil {
83- slog .Warn ("bootstrap_migration_unlock_failed" , sdk .LogFieldError , err )
84- }
85- }()
86-
87- for _ , migration := range migrations {
88- var appliedChecksum stdsql.NullString
89- const appliedSQL = `SELECT checksum FROM public.bootstrap_migrations WHERE id = $1`
90- err := conn .QueryRowContext (ctx , appliedSQL , migration .ID ).Scan (& appliedChecksum )
91- if err == nil {
92- if appliedChecksum .Valid && appliedChecksum .String != "" && appliedChecksum .String != migration .Checksum {
93- panicStartupMigration ("verify startup migration checksum " + migration .ID , fmt .Errorf ("recorded=%s current=%s" , appliedChecksum .String , migration .Checksum ))
94- }
95- if ! appliedChecksum .Valid || appliedChecksum .String == "" {
96- const updateSQL = `UPDATE public.bootstrap_migrations
97- SET checksum = $2, description = $3
98- WHERE id = $1 AND checksum = ''`
99- if _ , err := conn .ExecContext (ctx , updateSQL , migration .ID , migration .Checksum , migration .Description ); err != nil {
100- panicStartupMigration ("backfill startup migration checksum " + migration .ID , err )
101- }
102- }
103- continue
104- }
105- if err != stdsql .ErrNoRows {
106- panicStartupMigration ("check startup migration " + migration .ID , err )
107- }
108-
109- start := time .Now ()
110- slog .Info ("bootstrap_migration_start" , "id" , migration .ID )
111- if err := executeStartupMigrationSQL (ctx , conn , migration ); err != nil {
112- panicStartupMigration ("run startup migration " + migration .ID , err )
113- }
114- duration := time .Since (start ).Milliseconds ()
115- const insertSQL = `INSERT INTO public.bootstrap_migrations (id, description, checksum, duration_ms)
116- VALUES ($1, $2, $3, $4)
117- ON CONFLICT (id) DO NOTHING`
118- if _ , err := conn .ExecContext (ctx , insertSQL , migration .ID , migration .Description , migration .Checksum , duration ); err != nil {
119- panicStartupMigration ("record startup migration " + migration .ID , err )
120- }
121- slog .Info ("bootstrap_migration_done" , "id" , migration .ID , "duration_ms" , duration )
122- }
123- }
124-
125- func loadStartupMigrations () []startupMigration {
126- entries , err := fs .ReadDir (startupMigrationFiles , "migrations" )
127- if err != nil {
128- panicStartupMigration ("read startup migration files" , err )
129- }
130- sort .Slice (entries , func (i , j int ) bool {
131- return entries [i ].Name () < entries [j ].Name ()
132- })
133-
134- migrations := make ([]startupMigration , 0 , len (entries ))
135- for _ , entry := range entries {
136- if entry .IsDir () || filepath .Ext (entry .Name ()) != ".sql" {
137- continue
138- }
139- path := filepath .ToSlash (filepath .Join ("migrations" , entry .Name ()))
140- data , err := startupMigrationFiles .ReadFile (path )
141- if err != nil {
142- panicStartupMigration ("read startup migration " + entry .Name (), err )
143- }
144- hash := sha256 .Sum256 (data )
145- id := strings .TrimSuffix (entry .Name (), ".sql" )
146- sql := string (data )
147- migrations = append (migrations , startupMigration {
148- ID : id ,
149- Description : startupMigrationDescription (sql , id ),
150- Checksum : hex .EncodeToString (hash [:]),
151- SQL : sql ,
152- })
153- }
154- return migrations
155- }
156-
157- func startupMigrationDescription (sql , fallback string ) string {
158- for _ , line := range strings .Split (sql , "\n " ) {
159- line = strings .TrimSpace (line )
160- if strings .HasPrefix (line , "-- description:" ) {
161- return strings .TrimSpace (strings .TrimPrefix (line , "-- description:" ))
162- }
163- }
164- return fallback
165- }
166-
167- func executeStartupMigrationSQL (ctx context.Context , conn * stdsql.Conn , migration startupMigration ) error {
168- for _ , stmt := range splitSQLStatements (migration .SQL ) {
169- if _ , err := conn .ExecContext (ctx , stmt ); err != nil {
170- return fmt .Errorf ("execute statement in %s: %w" , migration .ID , err )
171- }
172- }
173- return nil
174- }
175-
176- func splitSQLStatements (sql string ) []string {
177- var statements []string
178- start := 0
179- inSingleQuote := false
180- inDoubleQuote := false
181- inLineComment := false
182- inBlockComment := false
183- dollarTag := ""
184-
185- for i := 0 ; i < len (sql ); i ++ {
186- ch := sql [i ]
187- next := byte (0 )
188- if i + 1 < len (sql ) {
189- next = sql [i + 1 ]
190- }
191-
192- switch {
193- case inLineComment :
194- if ch == '\n' {
195- inLineComment = false
196- }
197- continue
198- case inBlockComment :
199- if ch == '*' && next == '/' {
200- inBlockComment = false
201- i ++
202- }
203- continue
204- case dollarTag != "" :
205- if strings .HasPrefix (sql [i :], dollarTag ) {
206- i += len (dollarTag ) - 1
207- dollarTag = ""
208- }
209- continue
210- case inSingleQuote :
211- if ch == '\'' {
212- if next == '\'' {
213- i ++
214- } else {
215- inSingleQuote = false
216- }
217- }
218- continue
219- case inDoubleQuote :
220- if ch == '"' {
221- if next == '"' {
222- i ++
223- } else {
224- inDoubleQuote = false
225- }
226- }
227- continue
228- }
229-
230- if ch == '-' && next == '-' {
231- inLineComment = true
232- i ++
233- continue
234- }
235- if ch == '/' && next == '*' {
236- inBlockComment = true
237- i ++
238- continue
239- }
240- if ch == '\'' {
241- inSingleQuote = true
242- continue
243- }
244- if ch == '"' {
245- inDoubleQuote = true
246- continue
247- }
248- if ch == '$' {
249- if tag , ok := sqlDollarTag (sql [i :]); ok {
250- dollarTag = tag
251- i += len (tag ) - 1
252- continue
253- }
254- }
255- if ch == ';' {
256- stmt := strings .TrimSpace (sql [start :i ])
257- if stmt != "" {
258- statements = append (statements , stmt )
259- }
260- start = i + 1
261- }
262- }
263-
264- tail := strings .TrimSpace (sql [start :])
265- if tail != "" {
266- statements = append (statements , tail )
267- }
268- return statements
269- }
270-
271- func sqlDollarTag (sql string ) (string , bool ) {
272- if sql == "" || sql [0 ] != '$' {
273- return "" , false
274- }
275- for i := 1 ; i < len (sql ); i ++ {
276- ch := sql [i ]
277- if ch == '$' {
278- return sql [:i + 1 ], true
279- }
280- if ! ((ch >= 'a' && ch <= 'z' ) || (ch >= 'A' && ch <= 'Z' ) || (ch >= '0' && ch <= '9' ) || ch == '_' ) {
281- return "" , false
282- }
283- }
284- return "" , false
285- }
286-
287- func panicStartupMigration (action string , err error ) {
288- if err != nil {
289- err = fmt .Errorf ("%s: %w" , action , err )
290- } else {
291- err = fmt .Errorf ("%s" , action )
292- }
293- slog .Error ("bootstrap_startup_migration_failed" , sdk .LogFieldError , err )
294- panic (err )
295- }
296-
29728// migrateUserHistoryRefs 允许硬删除用户,同时保留历史使用记录和余额流水。
29829// 用量/计费聚合依赖 usage_logs 的成本快照字段;这里把历史表的 user 外键改为 SET NULL,
29930// 并回填 user_id/user_email 快照,避免删除用户后历史记录丢失归属信息。
0 commit comments