# User Service Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Build a GoFrame v2 RESTful user service with SQLite, providing CRUD endpoints with built-in parameter validation. **Architecture:** Standard GoFrame layered architecture — Controller → Service → DAO. The `gf init` scaffold generates the project skeleton; `gf gen ctrl` generates controller stubs from API struct definitions. DAO/entity/do files are written manually (since `gf gen dao` requires a MySQL connection or custom gf build for SQLite). The SQLite table is auto-created on startup via `g.DB().Exec()`. **Tech Stack:** GoFrame v2.10+, SQLite (via `github.com/gogf/gf/contrib/drivers/sqlite/v2`, requires CGO/Xcode), Go 1.21+ --- ## Task 1: Install gf CLI & Verify Environment **Step 1: Install gf CLI** ```bash go install github.com/gogf/gf/cmd/gf/v2@latest ``` **Step 2: Verify installation** ```bash gf version go version ``` Expected: `gf` version v2.x.x and `go` version 1.21+. **Step 3: Verify CGO is available (required for SQLite driver)** ```bash CGO_ENABLED=1 go env CGO_ENABLED ``` Expected: `1`. On macOS, install Xcode Command Line Tools if needed: `xcode-select --install`. --- ## Task 2: Scaffold Project with gf init **Step 1: Initialize project** From the working directory (`/Users/john/Temp/superpowers-demo`): ```bash gf init user-service -g "user-service" cd user-service ``` **Step 2: Verify scaffold structure** ```bash ls -la ``` Expected directories: `api/`, `hack/`, `internal/`, `manifest/`, `main.go`, `go.mod`. **Step 3: Initialize git repo** ```bash git init git add . git commit -m "feat: init GoFrame project scaffold" ``` --- ## Task 3: Add Dependencies **Files:** - Modify: `go.mod` **Step 1: Add SQLite driver** ```bash go get github.com/gogf/gf/contrib/drivers/sqlite/v2 ``` **Step 2: Tidy dependencies** ```bash go mod tidy ``` **Step 3: Verify go.mod** ```bash grep sqlite go.mod ``` Expected: line containing `github.com/gogf/gf/contrib/drivers/sqlite/v2`. **Step 4: Commit** ```bash git add go.mod go.sum git commit -m "feat: add SQLite driver dependency" ``` --- ## Task 4: Configure Database **Files:** - Modify: `manifest/config/config.yaml` **Step 1: Replace config file content** ```yaml # HTTP Server server: address: ":8080" openapiPath: "/api.json" swaggerPath: "/swagger" # Logging logger: level: "all" stdout: true # Database database: logger: level: "all" stdout: true default: link: "sqlite:./data/user.db" debug: true ``` **Step 2: Create data directory** ```bash mkdir -p data echo "*.db" >> .gitignore ``` **Step 3: Commit** ```bash git add manifest/config/config.yaml .gitignore git commit -m "feat: configure SQLite database" ``` --- ## Task 5: Write Entity Model **Files:** - Create: `internal/model/entity/user.go` > Note: Normally generated by `gf gen dao`. Written manually here because SQLite requires a special gf build for gen dao. The json:"-" tag on Password ensures it is never serialized in API responses. **Step 1: Create file** ```go // internal/model/entity/user.go // ================================================================================= // Code generated by GoFrame CLI tool. DO NOT EDIT. // ================================================================================= package entity import "github.com/gogf/gf/v2/os/gtime" // User is the golang structure for table user. type User struct { Id uint `json:"id" description:"User ID"` Username string `json:"username" description:"Username"` Email string `json:"email" description:"Email"` Password string `json:"-" description:"Password hash"` CreatedAt *gtime.Time `json:"createdAt" description:"Created Time"` UpdatedAt *gtime.Time `json:"updatedAt" description:"Updated Time"` } ``` **Step 2: Commit** ```bash git add internal/model/entity/user.go git commit -m "feat: add User entity model" ``` --- ## Task 6: Write DO (Data Object) Model **Files:** - Create: `internal/model/do/user.go` > The `do:true` tag tells GoFrame ORM to auto-filter nil fields in WHERE/Data operations. All non-time fields use `interface{}` so nil means "skip this field". **Step 1: Create file** ```go // internal/model/do/user.go // ================================================================================= // Code generated by GoFrame CLI tool. DO NOT EDIT. // ================================================================================= package do import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" ) // User is the golang structure of table user for DAO operations like Where/Data. type User struct { g.Meta `orm:"table:user, do:true"` Id interface{} Username interface{} Email interface{} Password interface{} CreatedAt *gtime.Time UpdatedAt *gtime.Time } ``` **Step 2: Commit** ```bash git add internal/model/do/user.go git commit -m "feat: add User DO model" ``` --- ## Task 7: Write DAO Internal Layer **Files:** - Create: `internal/dao/internal/user.go` > This is the inner DAO generated by `gf gen dao`. It wraps the gdb.Model with typed column names. **Step 1: Create file** ```go // internal/dao/internal/user.go // ========================================================================== // Code generated by GoFrame CLI tool. DO NOT EDIT. // ========================================================================== package internal import ( "context" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" ) // UserDao is the data access object for table user. type UserDao struct { table string group string columns UserColumns } // UserColumns defines and stores column names for table user. type UserColumns struct { Id string Username string Email string Password string CreatedAt string UpdatedAt string } var userColumns = UserColumns{ Id: "id", Username: "username", Email: "email", Password: "password", CreatedAt: "created_at", UpdatedAt: "updated_at", } // NewUserDao creates and returns a new DAO object for table data access. func NewUserDao() *UserDao { return &UserDao{ group: "default", table: "user", columns: userColumns, } } // DB retrieves and returns the underlying raw database management object of current DAO. func (dao *UserDao) DB() gdb.DB { return g.DB(dao.group) } // Table returns the table name of current dao. func (dao *UserDao) Table() string { return dao.table } // Columns returns all column names of current dao. func (dao *UserDao) Columns() UserColumns { return dao.columns } // Group returns the configuration group name of database of current dao. func (dao *UserDao) Group() string { return dao.group } // Ctx creates and returns the Model for current DAO. It automatically sets the context. func (dao *UserDao) Ctx(ctx context.Context) *gdb.Model { return dao.DB().Model(dao.table).Safe().Ctx(ctx) } // Transaction wraps the transaction logic using function f. // It rolls back on error and commits on nil. func (dao *UserDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { return dao.Ctx(ctx).Transaction(ctx, f) } ``` **Step 2: Commit** ```bash git add internal/dao/internal/user.go git commit -m "feat: add DAO internal layer" ``` --- ## Task 8: Write DAO Index File **Files:** - Create: `internal/dao/user.go` > This outer DAO file is the one you customize. It wraps the internal DAO and exposes the global User object. **Step 1: Create file** ```go // internal/dao/user.go // ================================================================================= // This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. // ================================================================================= package dao import "user-service/internal/dao/internal" // internalUserDao is internal type for wrapping internal DAO implements. type internalUserDao = *internal.UserDao // userDao is the data access object for table user. // You can define custom methods on it to extend its functionality as you wish. type userDao struct { internalUserDao } var ( // User is globally public accessible object for table user operations. User = userDao{ internal.NewUserDao(), } ) ``` **Step 2: Commit** ```bash git add internal/dao/user.go git commit -m "feat: add DAO index file" ``` --- ## Task 9: Write API Request/Response Structs **Files:** - Create: `api/user/v1/user_create.go` - Create: `api/user/v1/user_get_list.go` - Create: `api/user/v1/user_get_one.go` - Create: `api/user/v1/user_update.go` - Create: `api/user/v1/user_delete.go` > The `g.Meta` tag defines the HTTP route. GoFrame's `v` tag handles validation. The `d` tag sets default values. Pointer fields in UpdateReq enable partial updates — nil means "don't update this field". **Step 1: Create api/user/v1/user_create.go** ```go package v1 import "github.com/gogf/gf/v2/frame/g" // CreateReq defines the request for creating a new user. type CreateReq struct { g.Meta `path:"/users" method:"post" tags:"User" summary:"Create a new user"` Username string `v:"required|length:3,32" json:"username" dc:"Username (3–32 chars)"` Email string `v:"required|email" json:"email" dc:"Email address"` Password string `v:"required|length:6,32" json:"password" dc:"Password (6–32 chars)"` } // CreateRes defines the response after creating a user. type CreateRes struct { Id uint `json:"id" dc:"ID of the newly created user"` } ``` **Step 2: Create api/user/v1/user_get_list.go** ```go package v1 import ( "github.com/gogf/gf/v2/frame/g" "user-service/internal/model/entity" ) // GetListReq defines the request for listing users with pagination. type GetListReq struct { g.Meta `path:"/users" method:"get" tags:"User" summary:"Get paginated user list"` Page int `v:"min:1" json:"page" d:"1" dc:"Page number (default 1)"` PageSize int `v:"min:1|max:100" json:"pageSize" d:"10" dc:"Items per page (default 10, max 100)"` } // GetListRes defines the response for the user list. type GetListRes struct { List []*entity.User `json:"list" dc:"List of users"` Total int `json:"total" dc:"Total number of users"` Page int `json:"page" dc:"Current page number"` } ``` **Step 3: Create api/user/v1/user_get_one.go** ```go package v1 import ( "github.com/gogf/gf/v2/frame/g" "user-service/internal/model/entity" ) // GetOneReq defines the request for getting a single user by ID. type GetOneReq struct { g.Meta `path:"/users/{id}" method:"get" tags:"User" summary:"Get user by ID"` Id uint `v:"required|min:1" json:"id" dc:"User ID"` } // GetOneRes defines the response for a single user. type GetOneRes struct { *entity.User } ``` **Step 4: Create api/user/v1/user_update.go** ```go package v1 import "github.com/gogf/gf/v2/frame/g" // UpdateReq defines the request for updating a user. // All fields except Id are optional — only provided fields are updated. type UpdateReq struct { g.Meta `path:"/users/{id}" method:"put" tags:"User" summary:"Update user (partial update supported)"` Id uint `v:"required|min:1" json:"id" dc:"User ID"` Username *string `v:"omitempty|length:3,32" json:"username" dc:"New username (optional)"` Email *string `v:"omitempty|email" json:"email" dc:"New email (optional)"` Password *string `v:"omitempty|length:6,32" json:"password" dc:"New password (optional)"` } // UpdateRes defines the response after updating a user. type UpdateRes struct{} ``` **Step 5: Create api/user/v1/user_delete.go** ```go package v1 import "github.com/gogf/gf/v2/frame/g" // DeleteReq defines the request for deleting a user. type DeleteReq struct { g.Meta `path:"/users/{id}" method:"delete" tags:"User" summary:"Delete user by ID"` Id uint `v:"required|min:1" json:"id" dc:"User ID"` } // DeleteRes defines the response after deleting a user. type DeleteRes struct{} ``` **Step 6: Build to verify no compile errors** ```bash go build ./... ``` Expected: no errors (the api/user/user.go interface file doesn't exist yet, which is fine). **Step 7: Commit** ```bash git add api/ git commit -m "feat: add user API request/response structs" ``` --- ## Task 10: Generate Controller Scaffold **Step 1: Run gf gen ctrl** ```bash gf gen ctrl ``` Expected output: generates files including: - `api/user/user.go` — IUserV1 interface (auto-maintained, do not edit) - `internal/controller/user/user_new.go` — ControllerV1 constructor - `internal/controller/user/user_v1_create.go` — stub - `internal/controller/user/user_v1_get_list.go` — stub - `internal/controller/user/user_v1_get_one.go` — stub - `internal/controller/user/user_v1_update.go` — stub - `internal/controller/user/user_v1_delete.go` — stub **Step 2: Review generated interface** ```bash cat api/user/user.go ``` Expected: an `IUserV1` interface with 5 methods matching our Req/Res structs. **Step 3: Build to verify scaffold compiles** ```bash go build ./... ``` Expected: no errors (stubs return zero values which satisfies the interface). **Step 4: Commit** ```bash git add api/user/user.go internal/controller/ git commit -m "feat: generate controller scaffold via gf gen ctrl" ``` --- ## Task 11: Write Service Layer **Files:** - Create: `internal/service/user/user.go` > The service handles all business logic: uniqueness checks, password hashing, and DAO calls. Password is hashed with SHA256 (use bcrypt in production). The DO pattern with pointer fields enables clean partial updates — nil interface{} fields are auto-skipped by the ORM. **Step 1: Create file** ```go // internal/service/user/user.go package user import ( "context" "crypto/sha256" "encoding/hex" "user-service/internal/dao" "user-service/internal/model/do" "user-service/internal/model/entity" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" ) // Service provides user-related business logic. type Service struct{} // New creates and returns a new Service instance. func New() *Service { return &Service{} } // hashPassword returns a SHA256 hex digest of the password. // Note: use bcrypt in production for proper password hashing. func hashPassword(password string) string { h := sha256.Sum256([]byte(password)) return hex.EncodeToString(h[:]) } // ─── Create ────────────────────────────────────────────────────────────────── // CreateInput defines the input for Create. type CreateInput struct { Username string Email string Password string } // CreateOutput defines the output for Create. type CreateOutput struct { Id uint } // Create registers a new user after uniqueness checks. func (s *Service) Create(ctx context.Context, in CreateInput) (out CreateOutput, err error) { // Verify username is not taken. count, err := dao.User.Ctx(ctx).Where(do.User{Username: in.Username}).Count() if err != nil { return out, err } if count > 0 { return out, gerror.Newf(`Username "%s" is already taken`, in.Username) } // Verify email is not registered. count, err = dao.User.Ctx(ctx).Where(do.User{Email: in.Email}).Count() if err != nil { return out, err } if count > 0 { return out, gerror.Newf(`Email "%s" is already registered`, in.Email) } // Insert user inside a transaction. var id int64 err = dao.User.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { result, insertErr := dao.User.Ctx(ctx).Data(do.User{ Username: in.Username, Email: in.Email, Password: hashPassword(in.Password), }).Insert() if insertErr != nil { return insertErr } id, insertErr = result.LastInsertId() return insertErr }) if err != nil { return out, err } return CreateOutput{Id: uint(id)}, nil } // ─── GetList ───────────────────────────────────────────────────────────────── // GetListInput defines the input for GetList. type GetListInput struct { Page int PageSize int } // GetListOutput defines the output for GetList. type GetListOutput struct { List []*entity.User Total int } // GetList retrieves a paginated list of users (password field excluded). func (s *Service) GetList(ctx context.Context, in GetListInput) (out GetListOutput, err error) { m := dao.User.Ctx(ctx).Fields("id,username,email,created_at,updated_at") out.Total, err = m.Count() if err != nil { return } err = m.Page(in.Page, in.PageSize).Scan(&out.List) return } // ─── GetOne ────────────────────────────────────────────────────────────────── // GetOne retrieves a single user by ID (password field excluded). func (s *Service) GetOne(ctx context.Context, id uint) (user *entity.User, err error) { err = dao.User.Ctx(ctx). Fields("id,username,email,created_at,updated_at"). Where(do.User{Id: id}). Scan(&user) if err != nil { return nil, err } if user == nil { return nil, gerror.Newf(`User with ID %d not found`, id) } return user, nil } // ─── Update ────────────────────────────────────────────────────────────────── // UpdateInput defines the input for Update. Pointer fields are optional. type UpdateInput struct { Id uint Username *string Email *string Password *string } // Update applies partial updates to an existing user. func (s *Service) Update(ctx context.Context, in UpdateInput) error { // Verify user exists. count, err := dao.User.Ctx(ctx).Where(do.User{Id: in.Id}).Count() if err != nil { return err } if count == 0 { return gerror.Newf(`User with ID %d not found`, in.Id) } // Check username uniqueness (excluding current user). if in.Username != nil { count, err = dao.User.Ctx(ctx). Where("username=? AND id!=?", *in.Username, in.Id).Count() if err != nil { return err } if count > 0 { return gerror.Newf(`Username "%s" is already taken`, *in.Username) } } // Check email uniqueness (excluding current user). if in.Email != nil { count, err = dao.User.Ctx(ctx). Where("email=? AND id!=?", *in.Email, in.Id).Count() if err != nil { return err } if count > 0 { return gerror.Newf(`Email "%s" is already registered`, *in.Email) } } // Build update data. Nil interface{} fields are auto-filtered by the ORM. var hashedPwd interface{} if in.Password != nil { hashedPwd = hashPassword(*in.Password) } _, err = dao.User.Ctx(ctx). Where(do.User{Id: in.Id}). Data(do.User{ Username: in.Username, Email: in.Email, Password: hashedPwd, }).Update() return err } // ─── Delete ────────────────────────────────────────────────────────────────── // Delete removes a user by ID. func (s *Service) Delete(ctx context.Context, id uint) error { count, err := dao.User.Ctx(ctx).Where(do.User{Id: id}).Count() if err != nil { return err } if count == 0 { return gerror.Newf(`User with ID %d not found`, id) } _, err = dao.User.Ctx(ctx).Where(do.User{Id: id}).Delete() return err } ``` **Step 2: Build to verify** ```bash go build ./... ``` Expected: no errors. **Step 3: Commit** ```bash git add internal/service/user/user.go git commit -m "feat: implement user service layer" ``` --- ## Task 12: Update Controller New File (Add Service Injection) **Files:** - Modify: `internal/controller/user/user_new.go` > `gf gen ctrl` generated a bare ControllerV1 struct. We add the service dependency here. **Step 1: Read the generated file** ```bash cat internal/controller/user/user_new.go ``` **Step 2: Replace with service-injected version** ```go // internal/controller/user/user_new.go // ================================================================================= // Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. // ================================================================================= package user import ( userapi "user-service/api/user" usersvc "user-service/internal/service/user" ) // ControllerV1 handles HTTP requests for the user API. type ControllerV1 struct { userSvc *usersvc.Service } // NewV1 creates and returns a new ControllerV1 instance. func NewV1() userapi.IUserV1 { return &ControllerV1{ userSvc: usersvc.New(), } } ``` **Step 3: Build to verify** ```bash go build ./... ``` Expected: no errors. --- ## Task 13: Fill Controller Handler Stubs **Files:** - Modify: `internal/controller/user/user_v1_create.go` - Modify: `internal/controller/user/user_v1_get_list.go` - Modify: `internal/controller/user/user_v1_get_one.go` - Modify: `internal/controller/user/user_v1_update.go` - Modify: `internal/controller/user/user_v1_delete.go` **Step 1: Fill user_v1_create.go** ```go package user import ( "context" "user-service/api/user/v1" usersvc "user-service/internal/service/user" ) // Create handles POST /api/v1/users — creates a new user. func (c *ControllerV1) Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error) { out, err := c.userSvc.Create(ctx, usersvc.CreateInput{ Username: req.Username, Email: req.Email, Password: req.Password, }) if err != nil { return nil, err } return &v1.CreateRes{Id: out.Id}, nil } ``` **Step 2: Fill user_v1_get_list.go** ```go package user import ( "context" "user-service/api/user/v1" usersvc "user-service/internal/service/user" ) // GetList handles GET /api/v1/users — returns a paginated user list. func (c *ControllerV1) GetList(ctx context.Context, req *v1.GetListReq) (res *v1.GetListRes, err error) { out, err := c.userSvc.GetList(ctx, usersvc.GetListInput{ Page: req.Page, PageSize: req.PageSize, }) if err != nil { return nil, err } return &v1.GetListRes{ List: out.List, Total: out.Total, Page: req.Page, }, nil } ``` **Step 3: Fill user_v1_get_one.go** ```go package user import ( "context" "user-service/api/user/v1" ) // GetOne handles GET /api/v1/users/{id} — returns a single user. func (c *ControllerV1) GetOne(ctx context.Context, req *v1.GetOneReq) (res *v1.GetOneRes, err error) { user, err := c.userSvc.GetOne(ctx, req.Id) if err != nil { return nil, err } return &v1.GetOneRes{User: user}, nil } ``` **Step 4: Fill user_v1_update.go** ```go package user import ( "context" "user-service/api/user/v1" usersvc "user-service/internal/service/user" ) // Update handles PUT /api/v1/users/{id} — partial update of a user. func (c *ControllerV1) Update(ctx context.Context, req *v1.UpdateReq) (res *v1.UpdateRes, err error) { err = c.userSvc.Update(ctx, usersvc.UpdateInput{ Id: req.Id, Username: req.Username, Email: req.Email, Password: req.Password, }) if err != nil { return nil, err } return &v1.UpdateRes{}, nil } ``` **Step 5: Fill user_v1_delete.go** ```go package user import ( "context" "user-service/api/user/v1" ) // Delete handles DELETE /api/v1/users/{id} — deletes a user. func (c *ControllerV1) Delete(ctx context.Context, req *v1.DeleteReq) (res *v1.DeleteRes, err error) { err = c.userSvc.Delete(ctx, req.Id) if err != nil { return nil, err } return &v1.DeleteRes{}, nil } ``` **Step 6: Build to verify all handlers compile** ```bash go build ./... ``` Expected: no errors. **Step 7: Commit** ```bash git add internal/controller/user/ git commit -m "feat: implement controller handlers" ``` --- ## Task 14: Wire Routes in cmd.go & Update main.go **Files:** - Modify: `internal/cmd/cmd.go` - Modify: `main.go` **Step 1: Replace internal/cmd/cmd.go** > Registers the user routes under `/api/v1`, creates the SQLite table on startup, and enables the Swagger UI at `/swagger`. ```go // internal/cmd/cmd.go package cmd import ( "context" "user-service/internal/controller/user" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gcmd" ) var ( Main = gcmd.Command{ Name: "main", Usage: "main", Brief: "start http server for user service", Func: mainFunc, } ) func mainFunc(ctx context.Context, parser *gcmd.Parser) (err error) { // Create SQLite table if it does not exist. if err = initDB(ctx); err != nil { return err } s := g.Server() // MiddlewareHandlerResponse wraps all handler responses in: // {"code": 0, "message": "OK", "data": } s.Use(ghttp.MiddlewareHandlerResponse) s.Group("/api/v1", func(group *ghttp.RouterGroup) { group.Middleware(ghttp.MiddlewareCORS) group.Bind(user.NewV1()) }) s.Run() return nil } // initDB creates the user table if it does not already exist. func initDB(ctx context.Context) error { _, err := g.DB().Exec(ctx, ` CREATE TABLE IF NOT EXISTS user ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL, email TEXT NOT NULL, password TEXT NOT NULL, created_at DATETIME, updated_at DATETIME, UNIQUE(username), UNIQUE(email) ) `) return err } ``` **Step 2: Replace main.go** > Imports the SQLite driver (blank import for side-effect registration) and starts the server. ```go // main.go package main import ( _ "github.com/gogf/gf/contrib/drivers/sqlite/v2" "github.com/gogf/gf/v2/os/gctx" "user-service/internal/cmd" ) func main() { cmd.Main.Run(gctx.New()) } ``` **Step 3: Build final binary** ```bash go build -o user-service . ``` Expected: produces `user-service` binary with no errors. **Step 4: Commit** ```bash git add internal/cmd/cmd.go main.go git commit -m "feat: wire routes and SQLite init in cmd" ``` --- ## Task 15: Run & Test **Step 1: Start the server** ```bash gf run main.go ``` Expected: server starts on `:8080`, logs show table creation SQL and route map. **Step 2: Test — Create user** ```bash curl -s -X POST http://localhost:8080/api/v1/users \ -H "Content-Type: application/json" \ -d '{"username":"alice","email":"alice@example.com","password":"secret123"}' | jq . ``` Expected: ```json {"code":0,"message":"","data":{"id":1}} ``` **Step 3: Test — List users** ```bash curl -s "http://localhost:8080/api/v1/users?page=1&pageSize=10" | jq . ``` Expected: `data.list` contains alice, `data.total` is 1. **Step 4: Test — Get one user** ```bash curl -s http://localhost:8080/api/v1/users/1 | jq . ``` Expected: user data without `password` field. **Step 5: Test — Update user (partial)** ```bash curl -s -X PUT http://localhost:8080/api/v1/users/1 \ -H "Content-Type: application/json" \ -d '{"username":"alice2"}' | jq . ``` Expected: `{"code":0,"message":"","data":{}}` **Step 6: Test — Validation error** ```bash curl -s -X POST http://localhost:8080/api/v1/users \ -H "Content-Type: application/json" \ -d '{"username":"ab","email":"notanemail","password":"123"}' | jq . ``` Expected: `code` is non-zero with validation error message. **Step 7: Test — Delete user** ```bash curl -s -X DELETE http://localhost:8080/api/v1/users/1 | jq . ``` Expected: `{"code":0,"message":"","data":{}}` **Step 8: View Swagger UI** Open browser: `http://localhost:8080/swagger` Expected: interactive Swagger UI listing all 5 endpoints. **Step 9: Final commit** ```bash git add . git commit -m "docs: add data directory gitignore" ``` --- ## Summary | Layer | File | Role | |-------|------|------| | API | `api/user/v1/*.go` | Request/response structs with validation tags | | API Interface | `api/user/user.go` | IUserV1 interface (auto-generated by gf gen ctrl) | | Controller | `internal/controller/user/*.go` | HTTP binding → service calls | | Service | `internal/service/user/user.go` | Business logic, password hashing, uniqueness checks | | DAO | `internal/dao/user.go` + `internal/dao/internal/user.go` | Type-safe DB access via GoFrame ORM | | Model | `internal/model/entity/user.go` | DB entity struct | | Model | `internal/model/do/user.go` | DO struct for ORM filter operations | | Config | `manifest/config/config.yaml` | Server port + SQLite DSN | | Entry | `main.go` + `internal/cmd/cmd.go` | Server startup, SQLite table init, route wiring |