// 功能描述:购物车并发覆盖、数量边界、归档重试及账户隔离回归;版本:1.0.0。 package user import ( "fmt" "strings" "testing" "time" common "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common" "git.apinb.com/heqiapp/platforms/backend/api/internal/models" "github.com/DATA-DOG/go-sqlmock" ) func TestCartConditionalMutation(t *testing.T) { stamp := time.Date(2026, 9, 8, 1, 2, 3, 123456000, time.UTC) for _, tc := range []struct { name string quantity, current, status, stock, code int selected bool revision string write bool }{ {"相同目标重试不重复加量", 3, 3, 1, 10, 0, true, "old", false}, {"过期版本不可覆盖", 4, 3, 1, 10, 2403, true, "old", false}, {"增加超过库存被拒绝", 4, 3, 1, 3, 2402, true, "cart:2026-09-08T01:02:03.123456Z", false}, {"减少数量保留真实结果", 2, 3, 1, 10, 0, true, "cart:2026-09-08T01:02:03.123456Z", true}, {"取消勾选即使库存不足", 3, 3, 1, 0, 0, false, "cart:2026-09-08T01:02:03.123456Z", true}, {"删除归档不物理删除", 0, 3, 1, 0, 0, false, "cart:2026-09-08T01:02:03.123456Z", true}, {"重复删除无副作用", 0, 3, common.StatusArchived, 0, 0, false, "old", false}, } { t.Run(tc.name, func(t *testing.T) { mock := primaryTestDB(t) expectAddressAccount(mock) mock.ExpectBegin() mock.ExpectQuery(`SELECT .* FROM "user_account".*FOR UPDATE`).WithArgs(1, 1).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1)) mock.ExpectQuery(`SELECT .* FROM "ec_product"`).WithArgs("product", 1).WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "name", "status", "price_amount", "stock_quantity"}).AddRow(3, "product", "报警器", 1, 16900, tc.stock)) mock.ExpectQuery(`SELECT .* FROM "ec_cart".*user_account_id = \$1 AND ec_product_id = \$2`).WithArgs(1, 3). WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "status", "quantity", "selected", "updated_at"}).AddRow(4, "cart", tc.status, tc.current, true, stamp)) if tc.write { mock.ExpectExec(`UPDATE "ec_cart" SET .*`).WillReturnResult(sqlmock.NewResult(0, 1)) } if tc.code == 0 { mock.ExpectQuery(`SELECT .* FROM "ec_product_image"`).WithArgs(3, 1).WillReturnRows(sqlmock.NewRows([]string{"id"})) mock.ExpectCommit() } else { mock.ExpectRollback() } ctx, response := addressContext(fmt.Sprintf(`{"quantity":%d,"selected":%t,"revision":%q}`, tc.quantity, tc.selected, tc.revision), "product") SetCartItem(ctx) if !strings.Contains(response.Body.String(), fmt.Sprintf(`"code":%d`, tc.code)) { t.Fatal(response.Body.String()) } if tc.code == 0 && !strings.Contains(response.Body.String(), fmt.Sprintf(`"quantity":%d`, tc.quantity)) { t.Fatal("数量返回错误", response.Body.String()) } if strings.Contains(response.Body.String(), `"user_account_id"`) || strings.Contains(response.Body.String(), `"ec_product_id"`) { t.Fatal("泄漏内部标识") } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } }) } } func TestCartQuantityValidation(t *testing.T) { for _, body := range []string{`{"quantity":-1,"selected":true}`, `{"quantity":1000,"selected":true}`, `{"quantity":1}`, `{"selected":true}`, `{"quantity":1.5,"selected":true}`} { mock := primaryTestDB(t) expectAddressAccount(mock) ctx, response := addressContext(body, "product") SetCartItem(ctx) if !strings.Contains(response.Body.String(), `"code":1704`) { t.Fatal(response.Body.String()) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } } // 首次建行需要兼容 GORM 的 true 默认值,同时保留客户端明确取消勾选。 func TestCartFirstAddUnselected(t *testing.T) { mock := primaryTestDB(t) expectAddressAccount(mock) mock.ExpectBegin() mock.ExpectQuery(`SELECT .* FROM "user_account".*FOR UPDATE`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1)) mock.ExpectQuery(`SELECT .* FROM "ec_product"`).WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "status", "stock_quantity"}).AddRow(3, "product", 1, 10)) mock.ExpectQuery(`SELECT .* FROM "ec_cart"`).WithArgs(1, 3).WillReturnRows(sqlmock.NewRows([]string{"id"})) mock.ExpectQuery(`SELECT count\(\*\) FROM "ec_cart"`).WithArgs(1, common.StatusArchived).WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) mock.ExpectQuery(`INSERT INTO "ec_cart"`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(4)) mock.ExpectExec(`UPDATE "ec_cart" SET .*`).WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectQuery(`SELECT .* FROM "ec_product_image"`).WillReturnRows(sqlmock.NewRows([]string{"id"})) mock.ExpectCommit() ctx, response := addressContext(`{"quantity":2,"selected":false,"revision":""}`, "product") SetCartItem(ctx) if !strings.Contains(response.Body.String(), `"selected":false`) || !strings.Contains(response.Body.String(), `"quantity":2`) { t.Fatal(response.Body.String()) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } func TestCheckoutRejectsInvalidItemQuantityAndDuplicates(t *testing.T) { for _, items := range []string{`[{"product_identity":"p1","quantity":-1}]`, `[{"product_identity":"p1","quantity":0}]`, `[{"product_identity":"p1","quantity":1000}]`, `[{"product_identity":"p1","quantity":1},{"product_identity":"p1","quantity":2}]`} { mock := primaryTestDB(t) expectAddressAccount(mock) ctx, response := addressContext(fmt.Sprintf(`{"request_no":"request","address_identity":"a1","contact_name":"用户","contact_phone":"13800000001","items":%s}`, items), "") CreateShopOrder(ctx) if !strings.Contains(response.Body.String(), `"code":1704`) { t.Fatal(response.Body.String()) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } } func TestCartRevisionDatabasePrecision(t *testing.T) { item := models.EcCart{Entity: models.Entity{ID: 1, Identity: "cart", UpdatedAt: time.Unix(10, 123456789)}} before := cartRevision(item) item.UpdatedAt = item.UpdatedAt.Truncate(time.Microsecond) if before != cartRevision(item) { t.Fatal("写入响应和数据库微秒时间戳必须产生相同版本") } } func TestCartCheckoutRollbackOnStaleVersion(t *testing.T) { mock := primaryTestDB(t) expectAddressAccount(mock) mock.ExpectQuery(`SELECT .* FROM "ec_order"`).WillReturnRows(sqlmock.NewRows([]string{"id"})) mock.ExpectQuery(`SELECT .* FROM "user_address"`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(2)) mock.ExpectBegin() mock.ExpectQuery(`SELECT .* FROM "user_account".*FOR UPDATE`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1)) mock.ExpectQuery(`SELECT .* FROM "ec_product".*FOR UPDATE`).WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "price_amount", "stock_quantity"}).AddRow(3, "p1", 12800, 30)) mock.ExpectQuery(`SELECT .* FROM "ec_cart".*user_account_id = \$1 AND ec_product_id = \$2`).WithArgs(1, 3). WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "status", "quantity", "selected"}).AddRow(4, "cart", 1, 2, true)) mock.ExpectRollback() mock.ExpectQuery(`SELECT .* FROM "ec_order"`).WillReturnRows(sqlmock.NewRows([]string{"id"})) ctx, response := addressContext(`{"request_no":"cart-order","address_identity":"owned","contact_name":"收货人","contact_phone":"13800000001","items":[{"product_identity":"p1","quantity":2,"cart_revision":"old"}]}`, "") CreateShopOrder(ctx) if !strings.Contains(response.Body.String(), `"code":2403`) { t.Fatal(response.Body.String()) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } func TestCartCheckoutTwoProductsAtomic(t *testing.T) { mock := primaryTestDB(t) expectAddressAccount(mock) mock.ExpectQuery(`SELECT .* FROM "ec_order"`).WillReturnRows(sqlmock.NewRows([]string{"id"})) mock.ExpectQuery(`SELECT .* FROM "user_address"`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(2)) mock.ExpectBegin() mock.ExpectQuery(`SELECT .* FROM "user_account".*FOR UPDATE`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1)) for i := 1; i <= 2; i++ { mock.ExpectQuery(`SELECT .* FROM "ec_product".*FOR UPDATE`).WithArgs(fmt.Sprintf("p%d", i), 1, i, 1). WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "price_amount", "stock_quantity"}).AddRow(i, fmt.Sprintf("p%d", i), 10000, 30)) mock.ExpectQuery(`SELECT .* FROM "ec_cart"`).WithArgs(1, i).WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "status", "quantity", "selected"}).AddRow(i, fmt.Sprintf("c%d", i), 1, i, true)) mock.ExpectExec(`UPDATE "ec_cart" SET "status"`).WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectExec(`UPDATE "ec_product" SET "stock_quantity"`).WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectQuery(`SELECT .* FROM "ec_product_image"`). WillReturnRows(sqlmock.NewRows([]string{"id", "image_uri"}).AddRow(i, fmt.Sprintf("/p%d.png", i))) } mock.ExpectQuery(`INSERT INTO "ec_order"`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(5)) for i := 1; i <= 2; i++ { mock.ExpectQuery(`INSERT INTO "ec_order_item"`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(i)) } mock.ExpectCommit() ctx, response := addressContext(`{"request_no":"cart-order","address_identity":"owned","contact_name":"收货人","contact_phone":"13800000001","expected_payable_amount":30000,"items":[{"product_identity":"p2","quantity":2,"cart_revision":"c2:0001-01-01T00:00:00Z"},{"product_identity":"p1","quantity":1,"cart_revision":"c1:0001-01-01T00:00:00Z"}]}`, "") CreateShopOrder(ctx) if !strings.Contains(response.Body.String(), `"payable_amount":30000`) { t.Fatal(response.Body.String()) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } }