fix(backend): 商品编码改用最大序号生成不复用软删号 + 加 (shop_id,code) 唯一约束,根治重复编码
Deploy Server / release-deploy-server (push) Successful in 53s

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZ4DskSRKsSiheQonFtQvx
This commit is contained in:
wangjia
2026-06-21 10:51:34 +08:00
parent 71ed15b40b
commit 1a1d9aa15e
6 changed files with 151 additions and 40 deletions
+67
View File
@@ -2,13 +2,16 @@ package handler
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"github.com/wangjia/jiu/backend/internal/model"
"github.com/wangjia/jiu/backend/testutil"
)
@@ -199,3 +202,67 @@ func TestProductHandler_Create_ShopIDFromToken(t *testing.T) {
dataBytes, _ := json.Marshal(data)
_ = dataBytes
}
// 自动编码按最大序号递增:P001 → P002 → P003。
func TestProductHandler_AutoCode_Increment(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "AC001")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
var codes []string
for i := 0; i < 3; i++ {
w := makeRequest(r, "POST", "/api/v1/products", token, map[string]interface{}{
"name": fmt.Sprintf("AutoP %d", i), "unit": "个",
})
require.Equal(t, http.StatusCreated, w.Code)
data := parseResponse(w)["data"].(map[string]interface{})
codes = append(codes, data["code"].(string))
}
assert.Equal(t, []string{"P001", "P002", "P003"}, codes)
}
// 软删商品后,新建不复用被删的号(旧 count+1 逻辑会复用 → 重复,此为根因修复回归测试)。
func TestProductHandler_AutoCode_NoReuseAfterSoftDelete(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "AC002")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
var lastID uint64
for i := 0; i < 3; i++ { // P001 P002 P003
w := makeRequest(r, "POST", "/api/v1/products", token, map[string]interface{}{
"name": fmt.Sprintf("NR %d", i), "unit": "个",
})
require.Equal(t, http.StatusCreated, w.Code)
lastID = extractID(w)
}
// 软删 P003
w := makeRequest(r, "DELETE", fmt.Sprintf("/api/v1/products/%d", lastID), token, nil)
require.Equal(t, http.StatusOK, w.Code)
// 再建 → 必须是 P004,不能复用已软删的 P003
w = makeRequest(r, "POST", "/api/v1/products", token, map[string]interface{}{
"name": "NR new", "unit": "个",
})
require.Equal(t, http.StatusCreated, w.Code)
data := parseResponse(w)["data"].(map[string]interface{})
assert.Equal(t, "P004", data["code"])
}
// (shop_id, code) 唯一约束生效,且重复被翻译成 gorm.ErrDuplicatedKeyCreate/FindOrCreate 重试的前提)。
func TestProductHandler_UniqueShopCode(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "UQ001")
require.NoError(t, db.Exec("CREATE UNIQUE INDEX uk_shop_code ON products(shop_id, code)").Error)
p1 := model.Product{TenantBase: model.TenantBase{ShopID: shop.ID}, Name: "A", Code: "P001"}
require.NoError(t, db.Create(&p1).Error)
p2 := model.Product{TenantBase: model.TenantBase{ShopID: shop.ID}, Name: "B", Code: "P001"}
err := db.Create(&p2).Error
require.Error(t, err)
assert.True(t, errors.Is(err, gorm.ErrDuplicatedKey))
}