Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -5,17 +5,22 @@ import (
|
||||
"bssapp-backend/models"
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetProductPricingList(ctx context.Context, limit int, offset int) ([]models.ProductPricing, error) {
|
||||
func GetProductPricingList(ctx context.Context, limit int, afterProductCode string) ([]models.ProductPricing, error) {
|
||||
if limit <= 0 {
|
||||
limit = 500
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
afterProductCode = strings.TrimSpace(afterProductCode)
|
||||
|
||||
cursorFilter := ""
|
||||
args := make([]any, 0, 1)
|
||||
if afterProductCode != "" {
|
||||
cursorFilter = "WHERE bp.ProductCode > @p1"
|
||||
args = append(args, afterProductCode)
|
||||
}
|
||||
|
||||
query := `
|
||||
@@ -36,11 +41,11 @@ func GetProductPricingList(ctx context.Context, limit int, offset int) ([]models
|
||||
AND LEN(LTRIM(RTRIM(ProductCode))) = 13
|
||||
),
|
||||
paged_products AS (
|
||||
SELECT
|
||||
SELECT TOP (` + strconv.Itoa(limit) + `)
|
||||
bp.ProductCode
|
||||
FROM base_products bp
|
||||
` + cursorFilter + `
|
||||
ORDER BY bp.ProductCode
|
||||
OFFSET %d ROWS FETCH NEXT %d ROWS ONLY
|
||||
),
|
||||
latest_base_price AS (
|
||||
SELECT
|
||||
@@ -182,7 +187,6 @@ func GetProductPricingList(ctx context.Context, limit int, offset int) ([]models
|
||||
ON st.ItemCode = bp.ProductCode
|
||||
ORDER BY bp.ProductCode;
|
||||
`
|
||||
query = fmt.Sprintf(query, offset, limit)
|
||||
|
||||
var (
|
||||
rows *sql.Rows
|
||||
@@ -190,7 +194,7 @@ func GetProductPricingList(ctx context.Context, limit int, offset int) ([]models
|
||||
)
|
||||
for attempt := 1; attempt <= 3; attempt++ {
|
||||
var err error
|
||||
rows, err = db.MssqlDB.QueryContext(ctx, query)
|
||||
rows, err = db.MssqlDB.QueryContext(ctx, query, args...)
|
||||
if err == nil {
|
||||
rowsErr = nil
|
||||
break
|
||||
|
||||
@@ -35,14 +35,9 @@ func GetProductPricingListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
limit = parsed
|
||||
}
|
||||
}
|
||||
offset := 0
|
||||
if raw := strings.TrimSpace(r.URL.Query().Get("offset")); raw != "" {
|
||||
if parsed, err := strconv.Atoi(raw); err == nil && parsed >= 0 && parsed <= 1000000 {
|
||||
offset = parsed
|
||||
}
|
||||
}
|
||||
afterProductCode := strings.TrimSpace(r.URL.Query().Get("after_product_code"))
|
||||
|
||||
rows, err := queries.GetProductPricingList(ctx, limit+1, offset)
|
||||
rows, err := queries.GetProductPricingList(ctx, limit+1, afterProductCode)
|
||||
if err != nil {
|
||||
if isPricingTimeoutLike(err, ctx.Err()) {
|
||||
log.Printf(
|
||||
@@ -71,16 +66,21 @@ func GetProductPricingListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if hasMore {
|
||||
rows = rows[:limit]
|
||||
}
|
||||
nextCursor := ""
|
||||
if hasMore && len(rows) > 0 {
|
||||
nextCursor = strings.TrimSpace(rows[len(rows)-1].ProductCode)
|
||||
}
|
||||
|
||||
log.Printf(
|
||||
"[ProductPricing] trace=%s success user=%s id=%d limit=%d offset=%d count=%d has_more=%t duration_ms=%d",
|
||||
"[ProductPricing] trace=%s success user=%s id=%d limit=%d after=%q count=%d has_more=%t next=%q duration_ms=%d",
|
||||
traceID,
|
||||
claims.Username,
|
||||
claims.ID,
|
||||
limit,
|
||||
offset,
|
||||
afterProductCode,
|
||||
len(rows),
|
||||
hasMore,
|
||||
nextCursor,
|
||||
time.Since(started).Milliseconds(),
|
||||
)
|
||||
|
||||
@@ -90,6 +90,9 @@ func GetProductPricingListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
w.Header().Set("X-Has-More", "false")
|
||||
}
|
||||
if nextCursor != "" {
|
||||
w.Header().Set("X-Next-Cursor", nextCursor)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(rows)
|
||||
}
|
||||
|
||||
|
||||
@@ -315,7 +315,7 @@ import { useProductPricingStore } from 'src/stores/ProductPricingStore'
|
||||
|
||||
const store = useProductPricingStore()
|
||||
const FETCH_LIMIT = 500
|
||||
const currentOffset = ref(0)
|
||||
const nextCursor = ref('')
|
||||
const loadingMore = ref(false)
|
||||
|
||||
const usdToTry = 38.25
|
||||
@@ -852,14 +852,14 @@ function clearAllCurrencies () {
|
||||
}
|
||||
|
||||
async function fetchChunk ({ reset = false } = {}) {
|
||||
const offset = reset ? 0 : currentOffset.value
|
||||
const afterProductCode = reset ? '' : nextCursor.value
|
||||
const result = await store.fetchRows({
|
||||
limit: FETCH_LIMIT,
|
||||
offset,
|
||||
afterProductCode,
|
||||
append: !reset
|
||||
})
|
||||
const fetched = Number(result?.fetched) || 0
|
||||
currentOffset.value = offset + fetched
|
||||
nextCursor.value = String(result?.nextCursor || '')
|
||||
return fetched
|
||||
}
|
||||
|
||||
@@ -894,7 +894,7 @@ async function reloadData () {
|
||||
console.info('[product-pricing][ui] reload:start', {
|
||||
at: new Date(startedAt).toISOString()
|
||||
})
|
||||
currentOffset.value = 0
|
||||
nextCursor.value = ''
|
||||
await fetchChunk({ reset: true })
|
||||
await ensureEnoughVisibleRows(120, 6)
|
||||
console.info('[product-pricing][ui] reload:done', {
|
||||
|
||||
@@ -10,9 +10,9 @@ function toNumber (value) {
|
||||
return Number.isFinite(n) ? Number(n.toFixed(2)) : 0
|
||||
}
|
||||
|
||||
function mapRow (raw, index, offset = 0) {
|
||||
function mapRow (raw, index, baseIndex = 0) {
|
||||
return {
|
||||
id: offset + index + 1,
|
||||
id: baseIndex + index + 1,
|
||||
productCode: toText(raw?.ProductCode),
|
||||
stockQty: toNumber(raw?.StockQty),
|
||||
stockEntryDate: toText(raw?.StockEntryDate),
|
||||
@@ -64,27 +64,31 @@ export const useProductPricingStore = defineStore('product-pricing-store', {
|
||||
this.loading = true
|
||||
this.error = ''
|
||||
const limit = Number(options?.limit) > 0 ? Number(options.limit) : 500
|
||||
const offset = Number(options?.offset) >= 0 ? Number(options.offset) : 0
|
||||
const afterProductCode = toText(options?.afterProductCode)
|
||||
const append = Boolean(options?.append)
|
||||
const baseIndex = append ? this.rows.length : 0
|
||||
const startedAt = Date.now()
|
||||
console.info('[product-pricing][frontend] request:start', {
|
||||
at: new Date(startedAt).toISOString(),
|
||||
timeout_ms: 180000,
|
||||
limit,
|
||||
offset,
|
||||
after_product_code: afterProductCode || null,
|
||||
append
|
||||
})
|
||||
try {
|
||||
const params = { limit }
|
||||
if (afterProductCode) params.after_product_code = afterProductCode
|
||||
const res = await api.request({
|
||||
method: 'GET',
|
||||
url: '/pricing/products',
|
||||
params: { limit, offset },
|
||||
params,
|
||||
timeout: 180000
|
||||
})
|
||||
const traceId = res?.headers?.['x-trace-id'] || null
|
||||
const hasMoreHeader = String(res?.headers?.['x-has-more'] || '').toLowerCase()
|
||||
const nextCursorHeader = toText(res?.headers?.['x-next-cursor'])
|
||||
const data = Array.isArray(res?.data) ? res.data : []
|
||||
const mapped = data.map((x, i) => mapRow(x, i, offset))
|
||||
const mapped = data.map((x, i) => mapRow(x, i, baseIndex))
|
||||
if (append) {
|
||||
const merged = [...this.rows]
|
||||
const seen = new Set(this.rows.map((x) => x?.productCode))
|
||||
@@ -104,12 +108,14 @@ export const useProductPricingStore = defineStore('product-pricing-store', {
|
||||
duration_ms: Date.now() - startedAt,
|
||||
row_count: this.rows.length,
|
||||
fetched_count: mapped.length,
|
||||
has_more: this.hasMore
|
||||
has_more: this.hasMore,
|
||||
next_cursor: nextCursorHeader || null
|
||||
})
|
||||
return {
|
||||
traceId,
|
||||
fetched: mapped.length,
|
||||
hasMore: this.hasMore
|
||||
hasMore: this.hasMore,
|
||||
nextCursor: nextCursorHeader
|
||||
}
|
||||
} catch (err) {
|
||||
if (!append) this.rows = []
|
||||
|
||||
Reference in New Issue
Block a user