feat(backend): 商品价格历史接口(从入库单价派生)
GET /products/:id/price-history:从已审核入库单明细 unit_price 按 order_date 倒序派生进价变更点,去相邻重复价,守多租户 shop_id(零额外表)。 新增测试 PriceHistory(含草稿排除 + 倒序断言)。go build/vet/test 全过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSKEiHsvauyxYUW2itzUXX
This commit is contained in:
@@ -233,6 +233,39 @@ func (h *ProductHandler) Detail(c *gin.Context) {
|
||||
util.RespondSuccess(c, product)
|
||||
}
|
||||
|
||||
// PriceHistory GET /api/v1/products/:id/price-history
|
||||
// 进价变更历史:从已审核入库单的明细单价按时间派生(零额外表)。
|
||||
// 返回按日期倒序的 {date, price} 点;前端据相邻点算涨跌。
|
||||
func (h *ProductHandler) PriceHistory(c *gin.Context) {
|
||||
shopID := middleware.GetShopID(c)
|
||||
id := c.Param("id")
|
||||
|
||||
type pricePoint struct {
|
||||
Date string `json:"date"`
|
||||
Price float64 `json:"price"`
|
||||
}
|
||||
points := make([]pricePoint, 0)
|
||||
// 守多租户:order 的 shop_id 必须等于当前店;只取已审核单的正单价。
|
||||
h.db.Raw(`
|
||||
SELECT o.order_date AS date, sii.unit_price AS price
|
||||
FROM stock_in_items sii
|
||||
JOIN stock_in_orders o ON o.id = sii.order_id
|
||||
WHERE sii.product_id = ? AND o.shop_id = ? AND o.status = 'approved'
|
||||
AND sii.unit_price > 0
|
||||
ORDER BY o.order_date DESC, sii.id DESC
|
||||
LIMIT 20`, id, shopID).Scan(&points)
|
||||
|
||||
// 去掉相邻重复价(只保留价格发生变化的点)
|
||||
deduped := make([]pricePoint, 0, len(points))
|
||||
for i, p := range points {
|
||||
if i > 0 && points[i-1].Price == p.Price {
|
||||
continue
|
||||
}
|
||||
deduped = append(deduped, p)
|
||||
}
|
||||
util.RespondSuccess(c, deduped)
|
||||
}
|
||||
|
||||
// QRCode GET /api/v1/products/:id/qrcode
|
||||
func (h *ProductHandler) QRCode(c *gin.Context) {
|
||||
shopID := middleware.GetShopID(c)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -266,3 +267,41 @@ func TestProductHandler_UniqueShopCode(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Is(err, gorm.ErrDuplicatedKey))
|
||||
}
|
||||
|
||||
func TestProductHandler_PriceHistory(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "PH001")
|
||||
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
||||
wh := testutil.CreateTestWarehouse(db, shop.ID, "W")
|
||||
product := testutil.CreateTestProduct(db, shop.ID, "Maotai")
|
||||
token := getAuthToken(user.ID, shop.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
mkOrder := func(no string, daysAgo int, price float64, status string) {
|
||||
o := &model.StockInOrder{
|
||||
TenantBase: model.TenantBase{ShopID: shop.ID},
|
||||
OrderNo: no,
|
||||
WarehouseID: wh.ID,
|
||||
OperatorID: user.ID,
|
||||
Status: status,
|
||||
OrderDate: model.Date{Time: time.Now().AddDate(0, 0, -daysAgo)},
|
||||
}
|
||||
require.NoError(t, db.Create(o).Error)
|
||||
require.NoError(t, db.Create(&model.StockInItem{
|
||||
OrderID: o.ID, ShopID: shop.ID, ProductID: product.ID,
|
||||
UnitPrice: price, Quantity: 1,
|
||||
}).Error)
|
||||
}
|
||||
mkOrder("RK-PH-1", 60, 2580, "approved")
|
||||
mkOrder("RK-PH-2", 30, 2620, "approved")
|
||||
mkOrder("RK-PH-3", 5, 2680, "approved")
|
||||
mkOrder("RK-PH-4", 1, 9999, "draft") // 草稿不计入
|
||||
|
||||
w := makeRequest(r, "GET",
|
||||
fmt.Sprintf("/api/v1/products/%d/price-history", product.ID), token, nil)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
data := parseResponse(w)["data"].([]interface{})
|
||||
assert.Len(t, data, 3) // 三个已审核不同价;草稿排除
|
||||
// 倒序:最新价在前
|
||||
assert.Equal(t, float64(2680), data[0].(map[string]interface{})["price"])
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ func setupProtectedRouter(db *gorm.DB) *gin.Engine {
|
||||
products := api.Group("/products")
|
||||
products.GET("", productH.List)
|
||||
products.POST("", productH.Create)
|
||||
products.GET("/:id/price-history", productH.PriceHistory)
|
||||
products.PUT("/:id", productH.Update)
|
||||
products.DELETE("/:id", productH.Delete)
|
||||
|
||||
|
||||
@@ -117,6 +117,7 @@ func Setup(r *gin.Engine, db *gorm.DB) {
|
||||
products.POST("", productH.Create)
|
||||
products.POST("/find-or-create", productH.FindOrCreate)
|
||||
products.GET("/:id/detail", productH.Detail)
|
||||
products.GET("/:id/price-history", productH.PriceHistory)
|
||||
products.GET("/:id/qrcode", productH.QRCode)
|
||||
products.PUT("/:id", productH.Update)
|
||||
products.DELETE("/:id", productH.Delete)
|
||||
|
||||
Reference in New Issue
Block a user