docs: tick completed plan checkboxes, mark spec as implemented
This commit is contained in:
@@ -71,14 +71,14 @@ docker-compose.yml # web, api(xN), postgres, redis, testprofile
|
||||
- Consumes: 无
|
||||
- Produces: `config.Config{Addr, DatabaseURL, RedisURL string, JWTSecret []byte, AdminUser, AdminPassword, BooksDir, CacheDir string, ScanInterval time.Duration, UploadMaxMB int64}`;`config.Load() (*Config, error)`;`api.NewRouter(cfg *config.Config) *gin.Engine`
|
||||
|
||||
- [ ] **Step 1: 初始化 module**
|
||||
- [x] **Step 1: 初始化 module**
|
||||
|
||||
```bash
|
||||
cd backend && go mod init booklib
|
||||
go get github.com/gin-gonic/gin@latest github.com/jackc/pgx/v5@latest github.com/redis/go-redis/v9@latest github.com/golang-jwt/jwt/v5@latest golang.org/x/crypto@latest
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 写失败测试 `internal/api/router_test.go`**
|
||||
- [x] **Step 2: 写失败测试 `internal/api/router_test.go`**
|
||||
|
||||
```go
|
||||
package api
|
||||
@@ -107,12 +107,12 @@ func TestHealthz(t *testing.T) {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 跑测试确认失败**
|
||||
- [x] **Step 3: 跑测试确认失败**
|
||||
|
||||
Run: `cd backend && go test ./internal/api/ -run TestHealthz -v`
|
||||
Expected: 编译失败 "undefined: NewRouter"
|
||||
|
||||
- [ ] **Step 4: 实现 config + router**
|
||||
- [x] **Step 4: 实现 config + router**
|
||||
|
||||
`internal/config/config.go`:
|
||||
|
||||
@@ -202,12 +202,12 @@ func NewRouter(cfg *config.Config) *gin.Engine {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 跑测试确认通过**
|
||||
- [x] **Step 5: 跑测试确认通过**
|
||||
|
||||
Run: `cd backend && go test ./internal/api/ -v`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
- [x] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git init -b main . && git add backend && git commit -m "feat(backend): module scaffold, config, gin router with healthz"
|
||||
@@ -268,7 +268,7 @@ func db.Connect(ctx, url string) (*pgxpool.Pool, error)
|
||||
func db.Migrate(ctx, p *pgxpool.Pool) error // 执行内嵌 schema.sql,幂等
|
||||
```
|
||||
|
||||
- [ ] **Step 1: 起测试用 PG/Redis**
|
||||
- [x] **Step 1: 起测试用 PG/Redis**
|
||||
|
||||
`deploy/docker-compose.dev.yml`:
|
||||
|
||||
@@ -287,7 +287,7 @@ services:
|
||||
Run: `docker compose -f deploy/docker-compose.dev.yml up -d --wait`
|
||||
Expected: 两个 healthy。后续所有集成测试用 `DATABASE_URL=postgres://lib:lib@localhost:5433/lib?sslmode=disable`。
|
||||
|
||||
- [ ] **Step 2: 写 schema.sql(内嵌,幂等)**
|
||||
- [x] **Step 2: 写 schema.sql(内嵌,幂等)**
|
||||
|
||||
`internal/db/schema.sql`:
|
||||
|
||||
@@ -315,7 +315,7 @@ CREATE TABLE IF NOT EXISTS reading_progress (
|
||||
PRIMARY KEY (user_id, library_id, book_path));
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 写失败测试 `store_test.go`(全部 SQL 行为在此锁死)**
|
||||
- [x] **Step 3: 写失败测试 `store_test.go`(全部 SQL 行为在此锁死)**
|
||||
|
||||
```go
|
||||
package store
|
||||
@@ -438,12 +438,12 @@ func TestProgressUpsertAndJoin(t *testing.T) {
|
||||
|
||||
(BookMeta 只带 diff 需要的 ID/Size/ModTS/Format;page_count 走 GetBook,测试里已有覆盖。)
|
||||
|
||||
- [ ] **Step 4: 跑测试确认失败**
|
||||
- [x] **Step 4: 跑测试确认失败**
|
||||
|
||||
Run: `cd backend && DATABASE_URL=postgres://lib:lib@localhost:5433/lib?sslmode=disable go test ./internal/store/ -v`
|
||||
Expected: 编译失败 "booklib/internal/db: no such package"
|
||||
|
||||
- [ ] **Step 5: 实现 db.go 与 store.go**
|
||||
- [x] **Step 5: 实现 db.go 与 store.go**
|
||||
|
||||
`internal/db/db.go`:
|
||||
|
||||
@@ -790,12 +790,12 @@ func (s *Store) GetProgress(ctx context.Context, userID, libID int64, bookPath s
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: 跑测试确认通过**
|
||||
- [x] **Step 6: 跑测试确认通过**
|
||||
|
||||
Run: `cd backend && DATABASE_URL=postgres://lib:lib@localhost:5433/lib?sslmode=disable go test ./internal/store/ -v`
|
||||
Expected: 2 个测试 PASS(无 DATABASE_URL 时 SKIP)
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
- [x] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add backend deploy && git commit -m "feat(backend): pg schema + store queries incl. path-keyed progress"
|
||||
@@ -824,7 +824,7 @@ auth.Parse(secret []byte, token string) (*auth.Claims, error)
|
||||
seed.Admin(ctx context.Context, s *store.Store, user, pass string) error // users 为空才建;user/pass 空则跳过;失败仅由调用方记日志
|
||||
```
|
||||
|
||||
- [ ] **Step 1: 写失败测试**
|
||||
- [x] **Step 1: 写失败测试**
|
||||
|
||||
`internal/auth/auth_test.go`:
|
||||
|
||||
@@ -917,12 +917,12 @@ func TestSeedOnlyWhenEmpty(t *testing.T) {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑,确认编译失败**
|
||||
- [x] **Step 2: 跑,确认编译失败**
|
||||
|
||||
Run: `cd backend && go test ./internal/auth/ ./internal/seed/ -v`
|
||||
Expected: FAIL — undefined
|
||||
|
||||
- [ ] **Step 3: 实现**
|
||||
- [x] **Step 3: 实现**
|
||||
|
||||
`internal/auth/auth.go`:
|
||||
|
||||
@@ -1017,12 +1017,12 @@ func Admin(ctx context.Context, s *store.Store, user, pass string) error {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 跑测试确认通过**
|
||||
- [x] **Step 4: 跑测试确认通过**
|
||||
|
||||
Run: `cd backend && go test ./internal/auth/ -v && DATABASE_URL=... go test ./internal/seed/ -v`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
- [x] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend && git commit -m "feat(backend): bcrypt+jwt auth package and admin seeding"
|
||||
@@ -1055,7 +1055,7 @@ api 中间件设置 ctx key: "uid" int64 / "role" string;helpers api.uid(c)/api.
|
||||
NewRouter(cfg *config.Config, st *store.Store, rdb *redispkg.R) *gin.Engine // scanner 参数 Task 9 再加
|
||||
```
|
||||
|
||||
- [ ] **Step 1: 写失败测试 `auth_test.go`**
|
||||
- [x] **Step 1: 写失败测试 `auth_test.go`**
|
||||
|
||||
```go
|
||||
package api
|
||||
@@ -1193,12 +1193,12 @@ func TestDisabledIsSafe(t *testing.T) {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑,确认失败**
|
||||
- [x] **Step 2: 跑,确认失败**
|
||||
|
||||
Run: `cd backend && DATABASE_URL=... go test ./internal/api/ ./internal/redispkg/ -v`
|
||||
Expected: 编译失败 undefined: redispkg / NewRouter 签名不符
|
||||
|
||||
- [ ] **Step 3: 实现 redispkg**
|
||||
- [x] **Step 3: 实现 redispkg**
|
||||
|
||||
`internal/redispkg/redis.go`:
|
||||
|
||||
@@ -1283,7 +1283,7 @@ func (r *R) Lock(ctx context.Context, key string, ttl time.Duration) (func(), bo
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 实现 api struct/错误/中间件 + auth handlers,重写 NewRouter**
|
||||
- [x] **Step 4: 实现 api struct/错误/中间件 + auth handlers,重写 NewRouter**
|
||||
|
||||
`internal/api/api.go`:
|
||||
|
||||
@@ -1442,12 +1442,12 @@ func NewRouter(cfg *config.Config, st *store.Store, rdb *redispkg.R) *gin.Engine
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 跑测试确认通过**
|
||||
- [x] **Step 5: 跑测试确认通过**
|
||||
|
||||
Run: `cd backend && DATABASE_URL=postgres://lib:lib@localhost:5433/lib?sslmode=disable REDIS_URL=redis://localhost:6380 go test ./internal/api/ ./internal/redispkg/ -v`
|
||||
Expected: PASS(REDIS_URL 不给也应 PASS — no-op 路径)
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
- [x] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add backend && git commit -m "feat(backend): jwt middleware, login with redis rate limit, /auth/me"
|
||||
@@ -1470,7 +1470,7 @@ git add backend && git commit -m "feat(backend): jwt middleware, login with redi
|
||||
`bookfile.FormatFromExt(name string) string` — `.cbz→"cbz" .pdf→"pdf" .epub→"epub" .txt→"txt" .md→"md"`,其他 `"..."` → `""`
|
||||
`bookfile.SafeName(s string) string` — 去目录、去控制符、trim、限长 200、保留空格中文
|
||||
|
||||
- [ ] **Step 1: 写失败测试(bookfile 先行,纯函数)**
|
||||
- [x] **Step 1: 写失败测试(bookfile 先行,纯函数)**
|
||||
|
||||
`internal/bookfile/bookfile_test.go`:
|
||||
|
||||
@@ -1511,7 +1511,7 @@ func TestSafeName(t *testing.T) {
|
||||
|
||||
(`"tar.gz"→""`:扩展名是 `.gz` 不在白名单,正确。`"con\ntl.bin"` → base 后去 `\t` 得 `cntl.bin`。)
|
||||
|
||||
- [ ] **Step 2: 跑确认失败;实现 `bookfile.go`**
|
||||
- [x] **Step 2: 跑确认失败;实现 `bookfile.go`**
|
||||
|
||||
```go
|
||||
package bookfile
|
||||
@@ -1560,7 +1560,7 @@ func SafeName(s string) string {
|
||||
|
||||
Run: `go test ./internal/bookfile/ -v` → PASS。
|
||||
|
||||
- [ ] **Step 3: 写失败测试 users/libraries(API)**
|
||||
- [x] **Step 3: 写失败测试 users/libraries(API)**
|
||||
|
||||
`internal/api/users_test.go`:
|
||||
|
||||
@@ -1732,7 +1732,7 @@ func uploadBody(filename string, content []byte) (*bytes.Buffer, *multipart.Writ
|
||||
|
||||
(`libraries_test.go` 需 import `bytes`;`httptest.NewRequest("POST", url, buf)` 直接收 `*bytes.Buffer`。)
|
||||
|
||||
- [ ] **Step 4: 跑确认失败;实现 users.go / libraries.go / router 接线**
|
||||
- [x] **Step 4: 跑确认失败;实现 users.go / libraries.go / router 接线**
|
||||
|
||||
先修 `auth_test.go` 的 setupAPI(upload 测试需要,libRoot 校验依赖 BooksDir;auth_test.go 补 import `path/filepath`):
|
||||
|
||||
@@ -2038,12 +2038,12 @@ func (a *api) uniquePath(root, name string) (string, error) {
|
||||
libs.POST("/:id/upload", a.adminOnly(), a.upload)
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 跑测试确认通过**
|
||||
- [x] **Step 5: 跑测试确认通过**
|
||||
|
||||
Run: `cd backend && DATABASE_URL=... go test ./internal/api/ ./internal/bookfile/ -v`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
- [x] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add backend && git commit -m "feat(backend): user + library admin API, atomic sanitized upload"
|
||||
@@ -2068,7 +2068,7 @@ bookfile.NaturalLess(a, b string) bool
|
||||
bookfile.Hash(size, modTS int64) string // sha256("size:mtime") hex 前 16 字符
|
||||
```
|
||||
|
||||
- [ ] **Step 1: 写失败测试**
|
||||
- [x] **Step 1: 写失败测试**
|
||||
|
||||
`internal/bookfile/zip_test.go`。测试内构造 zip 夹具(不落盘,bytes.Buffer + zip.Writer):
|
||||
|
||||
@@ -2162,12 +2162,12 @@ func TestHash(t *testing.T) {
|
||||
|
||||
注意 `{"007","7"}` 与 `{"A","a"}` 两个用例:数字相等时按原始串比较(`"007" < "7"`,字典序),纯 ASCII 大小写按字节序。
|
||||
|
||||
- [ ] **Step 2: 跑,确认失败**
|
||||
- [x] **Step 2: 跑,确认失败**
|
||||
|
||||
Run: `cd backend && go test ./internal/bookfile/ -run 'PageIndex|ReadEntry|Natural|Hash' -v`
|
||||
Expected: 编译失败 undefined
|
||||
|
||||
- [ ] **Step 3: 实现 `zip.go` 与 `hash.go`**
|
||||
- [x] **Step 3: 实现 `zip.go` 与 `hash.go`**
|
||||
|
||||
`internal/bookfile/zip.go`:
|
||||
|
||||
@@ -2290,7 +2290,7 @@ func Hash(size, modTS int64) string {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 跑确认通过;Commit**
|
||||
- [x] **Step 4: 跑确认通过;Commit**
|
||||
|
||||
Run: `cd backend && go test ./internal/bookfile/ -v` → PASS
|
||||
|
||||
@@ -2315,7 +2315,7 @@ bookfile.CBZCover(f io.ReaderAt, size int64) (img []byte, ext string, err error)
|
||||
bookfile.EPUBCover(f io.ReaderAt, size int64) (img []byte, ext string, err error) // 无封面 → err
|
||||
```
|
||||
|
||||
- [ ] **Step 1: 写失败测试**
|
||||
- [x] **Step 1: 写失败测试**
|
||||
|
||||
`internal/bookfile/cover_test.go`(内存构造真 epub:container.xml + OPF + cover.png):
|
||||
|
||||
@@ -2398,7 +2398,7 @@ func TestEPUBNoCover(t *testing.T) {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑确认失败;实现 `cover.go`**
|
||||
- [x] **Step 2: 跑确认失败;实现 `cover.go`**
|
||||
|
||||
```go
|
||||
package bookfile
|
||||
@@ -2530,7 +2530,7 @@ func EPUBCover(f io.ReaderAt, size int64) ([]byte, string, error) {
|
||||
|
||||
(cover_test 中 `_ = url.PathUnescape` 那行不写,url 只在实现里 import。)
|
||||
|
||||
- [ ] **Step 3: 跑确认通过;Commit**
|
||||
- [x] **Step 3: 跑确认通过;Commit**
|
||||
|
||||
Run: `cd backend && go test ./internal/bookfile/ -v` → PASS
|
||||
|
||||
@@ -2561,7 +2561,7 @@ bookfile.SweepStale(cacheDir string, live map[string]bool) (int, error) // 删
|
||||
|
||||
衍生文件永不算旧即永不原地改:更新的书 hash 变 → 新 key 目录;旧 key 目录由清扫删除。
|
||||
|
||||
- [ ] **Step 1: 写失败测试**
|
||||
- [x] **Step 1: 写失败测试**
|
||||
|
||||
`internal/bookfile/cache_test.go`:
|
||||
|
||||
@@ -2621,7 +2621,7 @@ func TestPaths(t *testing.T) {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑确认失败;实现 `cache.go`**
|
||||
- [x] **Step 2: 跑确认失败;实现 `cache.go`**
|
||||
|
||||
```go
|
||||
package bookfile
|
||||
@@ -2687,7 +2687,7 @@ func (s *Store) BookHashes(ctx context.Context) (map[int64][2]int64, error) {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 跑确认通过;Commit**
|
||||
- [x] **Step 3: 跑确认通过;Commit**
|
||||
|
||||
Run: `cd backend && go test ./internal/bookfile/ -v` → PASS
|
||||
|
||||
@@ -2718,7 +2718,7 @@ NewRouter(cfg *config.Config, st *store.Store, rdb *redispkg.R, sc *scanner.Scan
|
||||
|
||||
行为规则(spec §3.1/§6.1):锁内执行;单本失败 → `state=error` 继续;删行不删进度(path 键控);扫描尾清扫缓存。**不删磁盘文件**(删文件只有 API DELETE)。
|
||||
|
||||
- [ ] **Step 1: 写失败测试 `scanner_test.go`(集成,依赖 PG;Redis 可为空走 no-op)**
|
||||
- [x] **Step 1: 写失败测试 `scanner_test.go`(集成,依赖 PG;Redis 可为空走 no-op)**
|
||||
|
||||
```go
|
||||
package scanner
|
||||
@@ -2864,11 +2864,11 @@ func TestScanBrokenCBZStateError(t *testing.T) {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑确认失败**
|
||||
- [x] **Step 2: 跑确认失败**
|
||||
|
||||
Run: `cd backend && DATABASE_URL=... go test ./internal/scanner/ -v` → 编译失败
|
||||
|
||||
- [ ] **Step 3: 实现 scanner**
|
||||
- [x] **Step 3: 实现 scanner**
|
||||
|
||||
`internal/scanner/scanner.go`:
|
||||
|
||||
@@ -3126,7 +3126,7 @@ func (s *Scanner) sweepCache(ctx context.Context) {
|
||||
|
||||
坏 cbz 的判定规则已合并在上面 `add`/`update` 终版中:`PageIndex` 失败(含 `ErrNotZip`、`ErrUnsafeZip`,Task 6 已定义)→ `state=error`;cover 提取失败只 log。
|
||||
|
||||
- [ ] **Step 4: 接线 API**
|
||||
- [x] **Step 4: 接线 API**
|
||||
|
||||
`NewRouter` 加第 4 参 `sc *scanner.Scanner`,存进 `api` struct(`sc *scanner.Scanner`);`internal/api/libraries.go` 的 `scanLibrary` 整函数替换:
|
||||
|
||||
@@ -3153,7 +3153,7 @@ func (a *api) scanLibrary(c *gin.Context) {
|
||||
|
||||
(Task 10 会把 setupAPI 整体替换为 4 返回值终版,这里先保编译。)`api` struct 增加字段 `sc *scanner.Scanner`。
|
||||
|
||||
- [ ] **Step 5: 跑全部后端测试确认通过;Commit**
|
||||
- [x] **Step 5: 跑全部后端测试确认通过;Commit**
|
||||
|
||||
Run: `cd backend && DATABASE_URL=... REDIS_URL=... go test -p 1 ./... -v`
|
||||
Expected: 全 PASS
|
||||
@@ -3217,7 +3217,7 @@ func setupAPI(t *testing.T) (*store.Store, *scanner.Scanner, http.Handler, strin
|
||||
|
||||
既有 call site 全部改为按需要接收:`_, _, h, _ := setupAPI(t)`(共 5 处:auth_test 2、users_test 2、libraries_test 1);`TestMemberCannotWriteUsers` 里那行无效的 `tok, _ := auth.Sign(...)` 删除,`auth_test.go` 的 `booklib/internal/auth` import 随之删除。setupAPI 需补 import:`path/filepath`、`booklib/internal/scanner`。
|
||||
|
||||
- [ ] **Step 1: 写失败测试**
|
||||
- [x] **Step 1: 写失败测试**
|
||||
|
||||
`internal/api/books_test.go`(fixture 助手 `newLibrary/writeCBZ/scanNow/loginAs/adminToken` 定义在这里,Task 11/12 的测试文件复用):
|
||||
|
||||
@@ -3372,7 +3372,7 @@ func loginAs(t *testing.T, h http.Handler, user, pass string) string {
|
||||
|
||||
`adminToken` 终版(users_test.go 中):`func adminToken(t *testing.T, h http.Handler) string { return loginAs(t, h, "alice", "pw12345") }`。
|
||||
|
||||
- [ ] **Step 2: 跑确认失败;实现 books.go + 路由**
|
||||
- [x] **Step 2: 跑确认失败;实现 books.go + 路由**
|
||||
|
||||
`internal/api/books.go`:
|
||||
|
||||
@@ -3521,7 +3521,7 @@ func (a *api) deleteBook(c *gin.Context) {
|
||||
p.DELETE("/books/:id", a.adminOnly(), a.deleteBook)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 跑确认通过;Commit**
|
||||
- [x] **Step 3: 跑确认通过;Commit**
|
||||
|
||||
Run: `cd backend && DATABASE_URL=... go test ./internal/api/ -v` → PASS
|
||||
|
||||
@@ -3551,7 +3551,7 @@ GET /api/books/:id/pages/:n → 第 n 页(0-based),落盘缓存后吐;immutable
|
||||
(*api) pageIndex(c, b store.Book, root string) ([]string, error) // redis pagesidx 缓存 7d
|
||||
```
|
||||
|
||||
- [ ] **Step 1: 写失败测试**
|
||||
- [x] **Step 1: 写失败测试**
|
||||
|
||||
`internal/api/content_test.go`(fixture 复用 books_test 的 `newLibrary/writeCBZ/scanNow/loginAs/adminToken`):
|
||||
|
||||
@@ -3659,7 +3659,7 @@ func TestFileRangeETag(t *testing.T) {
|
||||
|
||||
(`content_test.go` import 需含 `net/http/httptest`、`encoding/json`、`strings`、`os`、`path/filepath`、`testing`、`net/http`。)
|
||||
|
||||
- [ ] **Step 2: 跑确认失败;实现 content.go**
|
||||
- [x] **Step 2: 跑确认失败;实现 content.go**
|
||||
|
||||
```go
|
||||
package api
|
||||
@@ -3853,7 +3853,7 @@ router 追加:
|
||||
p.GET("/books/:id/pages/:n", a.page)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 跑确认通过;Commit**
|
||||
- [x] **Step 3: 跑确认通过;Commit**
|
||||
|
||||
Run: `cd backend && DATABASE_URL=... REDIS_URL=... go test ./internal/api/ -v` → PASS
|
||||
|
||||
@@ -3874,7 +3874,7 @@ git add backend && git commit -m "feat(backend): cover/file/pages endpoints —
|
||||
- Consumes: Task 2/10
|
||||
- Produces:`PUT /api/books/:id/progress`、`GET /api/progress`
|
||||
|
||||
- [ ] **Step 1: 写失败测试**
|
||||
- [x] **Step 1: 写失败测试**
|
||||
|
||||
`internal/api/progress_test.go`:
|
||||
|
||||
@@ -3959,7 +3959,7 @@ func TestProgressRoundTrip(t *testing.T) {
|
||||
|
||||
(progress_test 不用 context,import 列表不要 `context`。)
|
||||
|
||||
- [ ] **Step 2: 跑确认失败;实现 progress.go**
|
||||
- [x] **Step 2: 跑确认失败;实现 progress.go**
|
||||
|
||||
```go
|
||||
package api
|
||||
@@ -4028,7 +4028,7 @@ router 追加:
|
||||
p.GET("/progress", a.listProgress)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 跑确认通过;Commit**
|
||||
- [x] **Step 3: 跑确认通过;Commit**
|
||||
|
||||
Run: `cd backend && DATABASE_URL=... go test ./internal/api/ -v` → PASS
|
||||
|
||||
@@ -4049,7 +4049,7 @@ git add backend && git commit -m "feat(backend): reading progress upsert + listi
|
||||
- Consumes: 全部
|
||||
- Produces: 可执行 `/server`,env 驱动;`NewRouter` 最终签名不变。
|
||||
|
||||
- [ ] **Step 1: config 单测(先失败)**
|
||||
- [x] **Step 1: config 单测(先失败)**
|
||||
|
||||
`internal/config/config_test.go`:
|
||||
|
||||
@@ -4083,7 +4083,7 @@ func TestLoad(t *testing.T) {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 实现 main.go**
|
||||
- [x] **Step 2: 实现 main.go**
|
||||
|
||||
`cmd/server/main.go`:
|
||||
|
||||
@@ -4149,7 +4149,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: PG 不可用 → 503(spec §9 降级)**
|
||||
- [x] **Step 3: PG 不可用 → 503(spec §9 降级)**
|
||||
|
||||
`internal/api/api.go` 追加(`import` 补 `errors`、`io`、`log`、`net`、`syscall`、`github.com/jackc/pgx/v5/pgxpool`):
|
||||
|
||||
@@ -4168,7 +4168,7 @@ func dbErr(c *gin.Context, e error) {
|
||||
|
||||
把所有 handler 中 `"internal", "db error"` 的 500 分支替换为 `dbErr(c, <该处的 error 变量>)`:auth.go(login 1 处)、users.go(listUsers/createUser/deleteUser 各 1 处)、libraries.go(listLibraries)、books.go(listBooks/deleteBook)、content.go 无 DB 500 分支、progress.go(putProgress/listProgress)。替换后 `go test -p 1 ./...` 全绿即可(既有测试不覆盖 PG 宕机路径)。
|
||||
|
||||
- [ ] **Step 4: 全量回归 + 本地起服烟测**
|
||||
- [x] **Step 4: 全量回归 + 本地起服烟测**
|
||||
|
||||
```bash
|
||||
cd backend && go vet ./... && DATABASE_URL=postgres://lib:lib@localhost:5433/lib?sslmode=disable REDIS_URL=redis://localhost:6380 go test -p 1 ./...
|
||||
@@ -4187,7 +4187,7 @@ kill %1
|
||||
|
||||
Expected: 全 PASS + 末行输出含 `"title":"a"`。
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
- [x] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend && git commit -m "feat(backend): main wiring, graceful shutdown, local smoke green"
|
||||
@@ -4206,7 +4206,7 @@ git add backend && git commit -m "feat(backend): main wiring, graceful shutdown,
|
||||
- Consumes: Task 13 的可运行服务
|
||||
- Produces: `docker compose up -d --scale api=2` 起 web/api×2/pg/redis;`scripts/smoke.sh` 一条命令验收整个后端
|
||||
|
||||
- [ ] **Step 1: 部署文件**
|
||||
- [x] **Step 1: 部署文件**
|
||||
|
||||
`deploy/Dockerfile.api`:
|
||||
|
||||
@@ -4309,7 +4309,7 @@ ADMIN_PASSWORD=change-me-min-8
|
||||
SCAN_INTERVAL_SEC=60
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 冒烟脚本 `scripts/smoke.sh`**
|
||||
- [x] **Step 2: 冒烟脚本 `scripts/smoke.sh`**
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
@@ -4370,7 +4370,7 @@ curl -fsS -o /dev/null -X DELETE "$API/books/$BID" -H "$AUTH" || die "delete"
|
||||
say "ALL SMOKE TESTS PASSED"
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 跑通**
|
||||
- [x] **Step 3: 跑通**
|
||||
|
||||
```bash
|
||||
cp .env.example .env && sed -i '' -E "s/change-me-openssl-rand-hex-32/$(openssl rand -hex 32)/; s/^ADMIN_PASSWORD=.*/ADMIN_PASSWORD=smokeadmin1/" .env
|
||||
@@ -4382,7 +4382,7 @@ docker compose down -v && rm -f library_upload_note.txt
|
||||
|
||||
Expected: `ALL SMOKE TESTS PASSED`。(Linux 上 `sed -i` 去掉 `''` 参数。)
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
- [x] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add docker-compose.yml .env.example deploy scripts && git commit -m "feat(deploy): compose w/ nginx api-prefix ingress, scaled stateless api, e2e smoke"
|
||||
|
||||
Reference in New Issue
Block a user