diff --git a/controller/redemption.go b/controller/redemption.go index 76c35bc..63cf8ec 100644 --- a/controller/redemption.go +++ b/controller/redemption.go @@ -87,6 +87,7 @@ func AddRedemption(c *gin.Context) { cleanRedemption := model.Redemption{ UserId: c.GetInt("id"), Name: redemption.Name, + Remark: redemption.Remark, Key: key, CreatedTime: common.GetTimestamp(), Quota: redemption.Quota, @@ -146,6 +147,7 @@ func UpdateRedemption(c *gin.Context) { } // If you add more fields, please also update redemption.Update() cleanRedemption.Name = redemption.Name + cleanRedemption.Remark = redemption.Remark cleanRedemption.Quota = redemption.Quota cleanRedemption.ExpiredTime = redemption.ExpiredTime } diff --git a/controller/redemption_test.go b/controller/redemption_test.go new file mode 100644 index 0000000..6d3b664 --- /dev/null +++ b/controller/redemption_test.go @@ -0,0 +1,159 @@ +package controller + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/model" + "github.com/glebarez/sqlite" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gorm.io/gorm" +) + +func setupRedemptionControllerDB(t *testing.T) *gorm.DB { + t.Helper() + + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + require.NoError(t, err) + + sqlDB, _ := db.DB() + sqlDB.SetMaxOpenConns(1) + + origDB := model.DB + origLogDB := model.LOG_DB + model.DB = db + model.LOG_DB = db + common.UsingSQLite = true + common.RedisEnabled = false + + require.NoError(t, db.AutoMigrate(&model.User{}, &model.Redemption{}, &model.Log{})) + + t.Cleanup(func() { + model.DB = origDB + model.LOG_DB = origLogDB + _ = sqlDB.Close() + }) + + return db +} + +func setupRedemptionRouter() *gin.Engine { + gin.SetMode(gin.TestMode) + r := gin.New() + r.Use(func(c *gin.Context) { + c.Set("id", 1) + c.Next() + }) + + g := r.Group("/api/redemption") + g.POST("/", AddRedemption) + g.PUT("/", UpdateRedemption) + + return r +} + +func TestAddRedemptionStoresRemark(t *testing.T) { + db := setupRedemptionControllerDB(t) + router := setupRedemptionRouter() + + body, err := json.Marshal(map[string]interface{}{ + "name": "campaign-a", + "remark": "admin only note", + "quota": 500000, + "count": 1, + "expired_time": 0, + }) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "/api/redemption/", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var rows []model.Redemption + require.NoError(t, db.Find(&rows).Error) + require.Len(t, rows, 1) + assert.Equal(t, "campaign-a", rows[0].Name) + assert.Equal(t, "admin only note", rows[0].Remark) +} + +func TestUpdateRedemptionStoresRemark(t *testing.T) { + db := setupRedemptionControllerDB(t) + router := setupRedemptionRouter() + + row := model.Redemption{ + Id: 1, + UserId: 1, + Key: "update-remark-key", + Name: "campaign-b", + Remark: "before update", + Status: common.RedemptionCodeStatusEnabled, + Quota: 500000, + CreatedTime: common.GetTimestamp(), + } + require.NoError(t, db.Create(&row).Error) + + body, err := json.Marshal(map[string]interface{}{ + "id": 1, + "name": "campaign-b", + "remark": "after update", + "quota": 500000, + "expired_time": 0, + }) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPut, "/api/redemption/", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var stored model.Redemption + require.NoError(t, db.First(&stored, 1).Error) + assert.Equal(t, "after update", stored.Remark) +} + +func TestUpdateRedemptionStatusOnlyKeepsRemark(t *testing.T) { + db := setupRedemptionControllerDB(t) + router := setupRedemptionRouter() + + row := model.Redemption{ + Id: 1, + UserId: 1, + Key: "status-only-remark-key", + Name: "campaign-c", + Remark: "keep this remark", + Status: common.RedemptionCodeStatusEnabled, + Quota: 500000, + CreatedTime: common.GetTimestamp(), + } + require.NoError(t, db.Create(&row).Error) + + body, err := json.Marshal(map[string]interface{}{ + "id": 1, + "status": common.RedemptionCodeStatusDisabled, + "remark": "should not overwrite", + }) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPut, "/api/redemption/?status_only=true", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var stored model.Redemption + require.NoError(t, db.First(&stored, 1).Error) + assert.Equal(t, common.RedemptionCodeStatusDisabled, stored.Status) + assert.Equal(t, "keep this remark", stored.Remark) +} diff --git a/docs/DATABASE_SCHEMA.md b/docs/DATABASE_SCHEMA.md index fb541ff..02277e8 100644 --- a/docs/DATABASE_SCHEMA.md +++ b/docs/DATABASE_SCHEMA.md @@ -189,6 +189,7 @@ | key | string | 兑换码(32字符,唯一) | | status | int | 状态:1=启用,2=已使用,3=已禁用 | | name | string | 兑换码名称 | +| remark | string | 备注 | | quota | int | 额度值 | | created_time | int64 | 创建时间 | | redeemed_time | int64 | 兑换时间 | diff --git a/model/redemption.go b/model/redemption.go index 69ce6cd..b4988f4 100644 --- a/model/redemption.go +++ b/model/redemption.go @@ -20,7 +20,7 @@ type Redemption struct { Key string `json:"key" gorm:"type:char(32);uniqueIndex"` Status int `json:"status" gorm:"default:1"` Name string `json:"name" gorm:"index"` - Remark string `json:"remark"` + Remark string `json:"remark" gorm:"index"` Quota int `json:"quota" gorm:"default:100"` CreatedTime int64 `json:"created_time" gorm:"bigint"` RedeemedTime int64 `json:"redeemed_time" gorm:"bigint"` diff --git a/model/redemption_test.go b/model/redemption_test.go index 14d0947..9d88962 100644 --- a/model/redemption_test.go +++ b/model/redemption_test.go @@ -276,7 +276,7 @@ func TestRedemptionUpdateRemarkPersistsRemark(t *testing.T) { assert.Equal(t, "vip-updated", updated.Remark) } -func TestSearchRedemptionsByRemarkRemark(t *testing.T) { +func TestSearchRedemptionsByRemark(t *testing.T) { db := setupRedemptionDB(t) require.NoError(t, db.Create(&Redemption{ @@ -307,3 +307,35 @@ func TestSearchRedemptionsByRemarkRemark(t *testing.T) { assert.Equal(t, 1, results[0].Id) assert.Equal(t, "vip benefit", results[0].Remark) } + +func TestSearchRedemptionsByRemarkWithNumericKeyword(t *testing.T) { + db := setupRedemptionDB(t) + + require.NoError(t, db.Create(&Redemption{ + Id: 1, + UserId: 1, + Key: "remark-search-numeric", + Status: common.RedemptionCodeStatusEnabled, + Name: "starter-pack", + Remark: "123-vip benefit", + Quota: 100, + CreatedTime: common.GetTimestamp(), + }).Error) + require.NoError(t, db.Create(&Redemption{ + Id: 2, + UserId: 1, + Key: "remark-search-other-numeric", + Status: common.RedemptionCodeStatusEnabled, + Name: "basic-pack", + Remark: "standard benefit", + Quota: 100, + CreatedTime: common.GetTimestamp(), + }).Error) + + results, total, err := SearchRedemptions("123", 0, 10) + require.NoError(t, err) + require.Equal(t, int64(1), total) + require.Len(t, results, 1) + assert.Equal(t, 1, results[0].Id) + assert.Equal(t, "123-vip benefit", results[0].Remark) +} diff --git a/web/src/components/table/redemptions/RedemptionsColumnDefs.jsx b/web/src/components/table/redemptions/RedemptionsColumnDefs.jsx index efe1114..fb606ec 100644 --- a/web/src/components/table/redemptions/RedemptionsColumnDefs.jsx +++ b/web/src/components/table/redemptions/RedemptionsColumnDefs.jsx @@ -27,9 +27,6 @@ import { REDEMPTION_ACTIONS, } from '../../../constants/redemption.constants'; -/** - * Check if redemption code is expired - */ export const isExpired = (record) => { return ( record.status === REDEMPTION_STATUS.UNUSED && @@ -38,16 +35,10 @@ export const isExpired = (record) => { ); }; -/** - * Render timestamp - */ const renderTimestamp = (timestamp) => { return <>{timestamp2string(timestamp)}>; }; -/** - * Render redemption code status - */ const renderStatus = (status, record, t) => { if (isExpired(record)) { return ( @@ -73,18 +64,12 @@ const renderStatus = (status, record, t) => { ); }; -/** - * Get redemption code table column definitions - */ export const getRedemptionsColumns = ({ t, manageRedemption, copyText, setEditingRedemption, setShowEdit, - refresh, - redemptions, - activePage, showDeleteRedemptionModal, }) => { return [ @@ -96,6 +81,17 @@ export const getRedemptionsColumns = ({ title: t('名称'), dataIndex: 'name', }, + { + title: t('备注'), + dataIndex: 'remark', + render: (text) => { + return ( +