fix(backend): 架构质量改进批次二 (#32-37)

- #32 License 激活迁移到 license_devices 表:Activate/Verify/Deactivate 全部改用
  license_devices,新增 max_devices 校验和 GET /license/devices 端点;
  Activate 现在校验 shop_id 防跨租户激活
- #33 checkInventory 从 StockOutHandler 移到 StockService.CheckInventoryAvailability
- #34 新增 util/response.go 统一错误响应工具(RespondError/RespondSuccess/RespondCreated)
- #35 生产模式 CORS Origin='*' 启动时 Fatal
- #36 生产模式 License 私钥未配置启动时 Fatal
- #37 新增 util/page.go ValidatePageSize,应用到 partner/product/stock_in/stock_out handler

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-11 00:38:49 +08:00
parent 51cfe5fc6d
commit 3ab78dbf7a
15 changed files with 554 additions and 429 deletions
+79 -57
View File
@@ -15,71 +15,106 @@ func TestLicenseService_Activate_Success(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "LIC001")
// 创建许可证
expiry := time.Now().Add(30 * 24 * time.Hour)
lic := &model.License{
ShopID: shop.ID,
ShopID: shop.ID,
LicenseKey: "AAAAA-BBBBB-CCCCC-DDDDD",
IsActive: true,
ExpiresAt: &expiry,
MaxDevices: 3,
}
require.NoError(t, db.Create(lic).Error)
svc := NewLicenseService(db)
result, err := svc.Activate("AAAAA-BBBBB-CCCCC-DDDDD", "device-001")
result, err := svc.Activate(shop.ID, "AAAAA-BBBBB-CCCCC-DDDDD", "device-001", "Test PC", "windows")
require.NoError(t, err)
require.NotNil(t, result)
assert.Equal(t, "device-001", result.DeviceID)
assert.NotNil(t, result.ActivatedAt)
// Verify device record was created
var dev model.LicenseDevice
require.NoError(t, db.Where("license_id = ? AND device_id = ?", lic.ID, "device-001").First(&dev).Error)
assert.Equal(t, "Test PC", dev.DeviceName)
assert.Equal(t, "windows", dev.Platform)
}
func TestLicenseService_Activate_AlreadyBoundToDifferentDevice(t *testing.T) {
func TestLicenseService_Activate_SameDeviceIdempotent(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "LIC002")
lic := &model.License{
ShopID: shop.ID,
ShopID: shop.ID,
LicenseKey: "EEEEE-FFFFF-GGGGG-HHHHH",
DeviceID: "existing-device",
IsActive: true,
MaxDevices: 3,
}
require.NoError(t, db.Create(lic).Error)
// Pre-bind the device
require.NoError(t, db.Create(&model.LicenseDevice{
LicenseID: lic.ID, ShopID: shop.ID, DeviceID: "same-device",
}).Error)
svc := NewLicenseService(db)
result, err := svc.Activate("EEEEE-FFFFF-GGGGG-HHHHH", "new-device")
// Re-activating same device should succeed (idempotent)
result, err := svc.Activate(shop.ID, "EEEEE-FFFFF-GGGGG-HHHHH", "same-device", "Updated Name", "windows")
assert.Error(t, err)
assert.Equal(t, ErrDeviceMismatch, err)
assert.Nil(t, result)
require.NoError(t, err)
require.NotNil(t, result)
// Still only one device record
var count int64
db.Model(&model.LicenseDevice{}).Where("license_id = ?", lic.ID).Count(&count)
assert.Equal(t, int64(1), count)
}
func TestLicenseService_Activate_SameDevice(t *testing.T) {
func TestLicenseService_Activate_DeviceLimitExceeded(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "LIC003")
lic := &model.License{
ShopID: shop.ID,
ShopID: shop.ID,
LicenseKey: "IIIII-JJJJJ-KKKKK-LLLLL",
DeviceID: "same-device",
IsActive: true,
MaxDevices: 2,
}
require.NoError(t, db.Create(lic).Error)
// Fill up the device limit
require.NoError(t, db.Create(&model.LicenseDevice{LicenseID: lic.ID, ShopID: shop.ID, DeviceID: "dev-1"}).Error)
require.NoError(t, db.Create(&model.LicenseDevice{LicenseID: lic.ID, ShopID: shop.ID, DeviceID: "dev-2"}).Error)
svc := NewLicenseService(db)
// 同一设备重新激活应该成功
result, err := svc.Activate("IIIII-JJJJJ-KKKKK-LLLLL", "same-device")
result, err := svc.Activate(shop.ID, "IIIII-JJJJJ-KKKKK-LLLLL", "dev-3", "", "")
require.NoError(t, err)
require.NotNil(t, result)
assert.Equal(t, "same-device", result.DeviceID)
assert.Error(t, err)
assert.Equal(t, ErrDeviceLimitExceed, err)
assert.Nil(t, result)
}
func TestLicenseService_Activate_NotFound(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "LIC004")
svc := NewLicenseService(db)
result, err := svc.Activate("NONEX-ISTEN-TTTTT-LICCC", "device-001")
result, err := svc.Activate(shop.ID, "NONEX-ISTEN-TTTTT-LICCC", "device-001", "", "")
assert.Error(t, err)
assert.Equal(t, ErrLicenseNotFound, err)
assert.Nil(t, result)
}
func TestLicenseService_Activate_WrongShop(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "LIC004B")
otherShop := testutil.CreateTestShop(db, "LIC004C")
lic := &model.License{
ShopID: shop.ID, LicenseKey: "OTHSH-BBBBB-CCCCC-DDDDD", IsActive: true, MaxDevices: 3,
}
require.NoError(t, db.Create(lic).Error)
svc := NewLicenseService(db)
// otherShop cannot activate a license belonging to shop
result, err := svc.Activate(otherShop.ID, "OTHSH-BBBBB-CCCCC-DDDDD", "device-001", "", "")
assert.Error(t, err)
assert.Equal(t, ErrLicenseNotFound, err)
@@ -88,20 +123,16 @@ func TestLicenseService_Activate_NotFound(t *testing.T) {
func TestLicenseService_Activate_Inactive(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "LIC004")
shop := testutil.CreateTestShop(db, "LIC005")
// 先创建激活的许可证,再禁用(避免 GORM 零值跳过问题)
lic := &model.License{
ShopID: shop.ID,
LicenseKey: "MMMMM-NNNNN-OOOOO-PPPPP",
IsActive: true,
ShopID: shop.ID, LicenseKey: "MMMMM-NNNNN-OOOOO-PPPPP", IsActive: true, MaxDevices: 3,
}
require.NoError(t, db.Create(lic).Error)
// 禁用
require.NoError(t, db.Model(lic).Update("is_active", false).Error)
svc := NewLicenseService(db)
result, err := svc.Activate("MMMMM-NNNNN-OOOOO-PPPPP", "device-001")
result, err := svc.Activate(shop.ID, "MMMMM-NNNNN-OOOOO-PPPPP", "device-001", "", "")
assert.Error(t, err)
assert.Equal(t, ErrLicenseInactive, err)
@@ -110,20 +141,16 @@ func TestLicenseService_Activate_Inactive(t *testing.T) {
func TestLicenseService_Activate_Expired(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "LIC005")
shop := testutil.CreateTestShop(db, "LIC006")
// 已过期
expiry := time.Now().Add(-24 * time.Hour)
lic := &model.License{
ShopID: shop.ID,
LicenseKey: "QQQQQ-RRRRR-SSSSS-TTTTT",
IsActive: true,
ExpiresAt: &expiry,
ShopID: shop.ID, LicenseKey: "QQQQQ-RRRRR-SSSSS-TTTTT", IsActive: true, ExpiresAt: &expiry, MaxDevices: 3,
}
require.NoError(t, db.Create(lic).Error)
svc := NewLicenseService(db)
result, err := svc.Activate("QQQQQ-RRRRR-SSSSS-TTTTT", "device-001")
result, err := svc.Activate(shop.ID, "QQQQQ-RRRRR-SSSSS-TTTTT", "device-001", "", "")
assert.Error(t, err)
assert.Equal(t, ErrLicenseExpired, err)
@@ -132,40 +159,37 @@ func TestLicenseService_Activate_Expired(t *testing.T) {
func TestLicenseService_Verify_Success(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "LIC006")
shop := testutil.CreateTestShop(db, "LIC007")
expiry := time.Now().Add(30 * 24 * time.Hour)
lic := &model.License{
ShopID: shop.ID,
LicenseKey: "UUUUU-VVVVV-WWWWW-XXXXX",
DeviceID: "my-device",
IsActive: true,
ExpiresAt: &expiry,
ShopID: shop.ID, LicenseKey: "UUUUU-VVVVV-WWWWW-XXXXX", IsActive: true, ExpiresAt: &expiry,
}
require.NoError(t, db.Create(lic).Error)
require.NoError(t, db.Create(&model.LicenseDevice{
LicenseID: lic.ID, ShopID: shop.ID, DeviceID: "my-device",
}).Error)
svc := NewLicenseService(db)
result, err := svc.Verify(shop.ID, "my-device")
require.NoError(t, err)
require.NotNil(t, result)
assert.Equal(t, "my-device", result.DeviceID)
assert.Equal(t, lic.ID, result.ID)
}
func TestLicenseService_Verify_Expired(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "LIC007")
shop := testutil.CreateTestShop(db, "LIC008")
// 已过期
expiry := time.Now().Add(-1 * time.Hour)
lic := &model.License{
ShopID: shop.ID,
LicenseKey: "YYYYY-ZZZZZ-AAAAA-BBBBB",
DeviceID: "expired-device",
IsActive: true,
ExpiresAt: &expiry,
ShopID: shop.ID, LicenseKey: "YYYYY-ZZZZZ-AAAAA-BBBBB", IsActive: true, ExpiresAt: &expiry,
}
require.NoError(t, db.Create(lic).Error)
require.NoError(t, db.Create(&model.LicenseDevice{
LicenseID: lic.ID, ShopID: shop.ID, DeviceID: "expired-device",
}).Error)
svc := NewLicenseService(db)
result, err := svc.Verify(shop.ID, "expired-device")
@@ -177,7 +201,7 @@ func TestLicenseService_Verify_Expired(t *testing.T) {
func TestLicenseService_Verify_NotFound(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "LIC008")
shop := testutil.CreateTestShop(db, "LIC009")
svc := NewLicenseService(db)
result, err := svc.Verify(shop.ID, "nonexistent-device")
@@ -189,17 +213,15 @@ func TestLicenseService_Verify_NotFound(t *testing.T) {
func TestLicenseService_Verify_NoExpiry(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "LIC009")
shop := testutil.CreateTestShop(db, "LIC010")
// 永久许可证(无过期时间)
lic := &model.License{
ShopID: shop.ID,
LicenseKey: "CCCCC-DDDDD-EEEEE-FFFFF",
DeviceID: "lifetime-device",
IsActive: true,
ExpiresAt: nil,
ShopID: shop.ID, LicenseKey: "CCCCC-DDDDD-EEEEE-FFFFF", IsActive: true, ExpiresAt: nil,
}
require.NoError(t, db.Create(lic).Error)
require.NoError(t, db.Create(&model.LicenseDevice{
LicenseID: lic.ID, ShopID: shop.ID, DeviceID: "lifetime-device",
}).Error)
svc := NewLicenseService(db)
result, err := svc.Verify(shop.ID, "lifetime-device")