运费逻辑
This commit is contained in:
@@ -23,7 +23,11 @@ from app.models.scrape import (
|
|||||||
SearchResultData, TradeMonitorRequest,
|
SearchResultData, TradeMonitorRequest,
|
||||||
)
|
)
|
||||||
from app.utils.http_utils import generate_signed_headers
|
from app.utils.http_utils import generate_signed_headers
|
||||||
from app.utils.parse_util import get_handle_fee, get_shipping_fee
|
from app.utils.parse_util import (
|
||||||
|
get_franchise_shipping_fee,
|
||||||
|
get_handle_fee,
|
||||||
|
get_shipping_fee,
|
||||||
|
)
|
||||||
|
|
||||||
router = APIRouter(prefix="/api", tags=["scrape"])
|
router = APIRouter(prefix="/api", tags=["scrape"])
|
||||||
|
|
||||||
@@ -139,8 +143,28 @@ async def categories(
|
|||||||
dependencies=[Depends(require_bearer_token)],
|
dependencies=[Depends(require_bearer_token)],
|
||||||
)
|
)
|
||||||
async def order_get_shipping_fee(payload: OrderShippingFeeRequest) -> ApiResponse[OrderShippingFeeData]:
|
async def order_get_shipping_fee(payload: OrderShippingFeeRequest) -> ApiResponse[OrderShippingFeeData]:
|
||||||
goods_amount = sum(item.goods_number * item.goods_price for item in payload.item_list)
|
"""计算订单运费与手续费。
|
||||||
shipping_fee = get_shipping_fee(goods_amount)
|
|
||||||
|
规则:
|
||||||
|
- 官方店铺(supplier 为空)运费: 按官方部分商品总额套阶梯价(440/385/0)
|
||||||
|
* 订单中无任何官方商品时,官方运费 0
|
||||||
|
- 加盟店运费(supplier 非空,同名视为同一家): 每家统一 800 日元
|
||||||
|
- 通贩手续费: 按全部商品(含官方+加盟店)总额套阶梯价(240/0)
|
||||||
|
"""
|
||||||
|
official_amount = 0
|
||||||
|
franchise_stores: set[str] = set()
|
||||||
|
goods_amount = 0
|
||||||
|
for item in payload.item_list:
|
||||||
|
subtotal = item.goods_number * item.goods_price
|
||||||
|
goods_amount += subtotal
|
||||||
|
if item.supplier == "":
|
||||||
|
official_amount += subtotal
|
||||||
|
else:
|
||||||
|
franchise_stores.add(item.supplier)
|
||||||
|
|
||||||
|
official_shipping_fee = get_shipping_fee(official_amount) if official_amount > 0 else 0
|
||||||
|
franchise_shipping_fee = get_franchise_shipping_fee(len(franchise_stores))
|
||||||
|
shipping_fee = official_shipping_fee + franchise_shipping_fee
|
||||||
handle_amount = get_handle_fee(goods_amount)
|
handle_amount = get_handle_fee(goods_amount)
|
||||||
order_amount = goods_amount + shipping_fee + handle_amount
|
order_amount = goods_amount + shipping_fee + handle_amount
|
||||||
|
|
||||||
|
|||||||
@@ -132,6 +132,8 @@ class OrderItem(BaseModel):
|
|||||||
goods_id: str = Field(min_length=1, max_length=40)
|
goods_id: str = Field(min_length=1, max_length=40)
|
||||||
goods_number: int = Field(ge=1, le=9999)
|
goods_number: int = Field(ge=1, le=9999)
|
||||||
goods_price: int = Field(ge=0)
|
goods_price: int = Field(ge=0)
|
||||||
|
# 店铺标识: 空串 = 官方店铺;非空 = 加盟店标识(同名视为同一家加盟店)
|
||||||
|
supplier: str = ""
|
||||||
|
|
||||||
|
|
||||||
class OrderShippingFeeRequest(BaseModel):
|
class OrderShippingFeeRequest(BaseModel):
|
||||||
|
|||||||
+53
-31
@@ -122,42 +122,64 @@ def extract_product_id(url: str | None = None) -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def get_shipping_fee(amount: int) -> int:
|
# --- 官方店铺日本境内运费规则 ---
|
||||||
"""
|
# amount < OFFICIAL_SHIPPING_FEE_LOW_THRESHOLD: OFFICIAL_SHIPPING_FEE_LOW
|
||||||
根据定义的运费规则计算运费
|
# OFFICIAL_SHIPPING_FEE_LOW_THRESHOLD <= amount < OFFICIAL_SHIPPING_FEE_HIGH_THRESHOLD: OFFICIAL_SHIPPING_FEE_MID
|
||||||
|
# amount >= OFFICIAL_SHIPPING_FEE_HIGH_THRESHOLD: 免运费(0)
|
||||||
|
OFFICIAL_SHIPPING_FEE_LOW_THRESHOLD = 1000
|
||||||
|
OFFICIAL_SHIPPING_FEE_HIGH_THRESHOLD = 1500
|
||||||
|
OFFICIAL_SHIPPING_FEE_LOW = 440
|
||||||
|
OFFICIAL_SHIPPING_FEE_MID = 385
|
||||||
|
|
||||||
日本境内运费统一为固定价格(部分地区除外)。
|
# --- 加盟店日本境内运费规则 ---
|
||||||
• 订单金额低于1000日元:运费440日元
|
# 每 1 家加盟店,无论订单数量与金额,统一收 FRANCHISE_SHIPPING_FEE_PER_STORE 日元
|
||||||
• 订单金额低于1500日元:运费385日元
|
FRANCHISE_SHIPPING_FEE_PER_STORE = 800
|
||||||
• 订单金额1500日元及以上:免运费
|
|
||||||
*北海道、冲绳、离岛及其他部分地区可能需额外支付运费。
|
# --- 通贩手续费规则 ---
|
||||||
货到付款需另付550日元货到付款手续费。银行转账付款需由顾客承担转账手续费。信用卡付款无需支付手续费。
|
# amount < HANDLE_FEE_FREE_THRESHOLD: HANDLE_FEE_BELOW
|
||||||
*通过电商平台购买的商品,运费因店铺而异。
|
# amount >= HANDLE_FEE_FREE_THRESHOLD: 免手续费(0)
|
||||||
|
HANDLE_FEE_FREE_THRESHOLD = 5000
|
||||||
|
HANDLE_FEE_BELOW = 240
|
||||||
|
|
||||||
|
|
||||||
|
def get_shipping_fee(amount: int) -> int:
|
||||||
|
"""计算官方店铺日本境内运费(本州/四国/九州统一价)。
|
||||||
|
|
||||||
|
规则:
|
||||||
|
- amount < 1000 日元: 440 日元
|
||||||
|
- 1000 <= amount < 1500 日元: 385 日元
|
||||||
|
- amount >= 1500 日元: 0(免运费)
|
||||||
|
|
||||||
|
备注: 北海道/冲绳/离岛可能需额外支付;此处按本州口径计算。
|
||||||
|
加盟店运费另由 get_franchise_shipping_fee 计算。
|
||||||
"""
|
"""
|
||||||
if amount > 1500:
|
if amount >= OFFICIAL_SHIPPING_FEE_HIGH_THRESHOLD:
|
||||||
return 0
|
return 0
|
||||||
elif amount > 1000:
|
if amount >= OFFICIAL_SHIPPING_FEE_LOW_THRESHOLD:
|
||||||
return 440
|
return OFFICIAL_SHIPPING_FEE_MID
|
||||||
else:
|
return OFFICIAL_SHIPPING_FEE_LOW
|
||||||
return 385
|
|
||||||
|
|
||||||
|
def get_franchise_shipping_fee(franchise_store_count: int) -> int:
|
||||||
|
"""计算加盟店日本境内运费。
|
||||||
|
|
||||||
|
每 1 家加盟店统一收 800 日元,与订单数量/金额无关。
|
||||||
|
franchise_store_count <= 0 时返回 0。
|
||||||
|
"""
|
||||||
|
if franchise_store_count <= 0:
|
||||||
|
return 0
|
||||||
|
return franchise_store_count * FRANCHISE_SHIPPING_FEE_PER_STORE
|
||||||
|
|
||||||
|
|
||||||
def get_handle_fee(amount: int) -> int:
|
def get_handle_fee(amount: int) -> int:
|
||||||
"""
|
"""计算通贩手续费(本州/四国/九州口径)。
|
||||||
根据定义的规则计算手续费
|
|
||||||
https://www.suruga-ya.jp/man/qa/hanbai_qa/shiharai.html#shiharai_1_2
|
|
||||||
A. 运费根据购买金额而定。
|
|
||||||
■本州、四国、九州:
|
|
||||||
购买金额低于5,000日元…240日元;
|
|
||||||
购买金额5,000日元及以上…免运费。
|
|
||||||
■北海道、冲绳:
|
|
||||||
购买金额低于5,000日元…570日元;
|
|
||||||
购买金额5,000日元至低于10,000日元…285日元;
|
|
||||||
购买金额10,000日元及以上…免
|
|
||||||
|
|
||||||
运费。偏远岛屿及其他部分地区可能需额外收费。
|
规则:
|
||||||
这里按照本州计算手续费。
|
- amount < 5000 日元: 240 日元
|
||||||
|
- amount >= 5000 日元: 0(免手续费)
|
||||||
|
|
||||||
|
参考: https://www.suruga-ya.jp/man/qa/hanbai_qa/shiharai.html#shiharai_1_2
|
||||||
"""
|
"""
|
||||||
if amount > 5000:
|
if amount >= HANDLE_FEE_FREE_THRESHOLD:
|
||||||
return 0
|
return 0
|
||||||
else:
|
return HANDLE_FEE_BELOW
|
||||||
return 240
|
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
"""parse_util 运费/手续费计算函数以及订单运费路由的单元测试。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.api.routes.scrape import order_get_shipping_fee
|
||||||
|
from app.models.scrape import OrderItem, OrderShippingFeeRequest
|
||||||
|
from app.utils.parse_util import (
|
||||||
|
FRANCHISE_SHIPPING_FEE_PER_STORE,
|
||||||
|
HANDLE_FEE_BELOW,
|
||||||
|
OFFICIAL_SHIPPING_FEE_LOW,
|
||||||
|
OFFICIAL_SHIPPING_FEE_MID,
|
||||||
|
get_franchise_shipping_fee,
|
||||||
|
get_handle_fee,
|
||||||
|
get_shipping_fee,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --- 官方店铺运费规则: 边界与典型值 ---
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"amount, expected",
|
||||||
|
[
|
||||||
|
(0, OFFICIAL_SHIPPING_FEE_LOW), # < 1000
|
||||||
|
(999, OFFICIAL_SHIPPING_FEE_LOW), # < 1000
|
||||||
|
(1000, OFFICIAL_SHIPPING_FEE_MID), # = 1000 -> 385
|
||||||
|
(1200, OFFICIAL_SHIPPING_FEE_MID), # 1000 <= < 1500
|
||||||
|
(1499, OFFICIAL_SHIPPING_FEE_MID), # < 1500
|
||||||
|
(1500, 0), # = 1500 -> 免运费
|
||||||
|
(2000, 0), # > 1500
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_get_shipping_fee_boundaries(amount: int, expected: int) -> None:
|
||||||
|
assert get_shipping_fee(amount) == expected
|
||||||
|
|
||||||
|
|
||||||
|
# --- 加盟店运费规则: 按家数 ---
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"count, expected",
|
||||||
|
[
|
||||||
|
(-1, 0),
|
||||||
|
(0, 0),
|
||||||
|
(1, FRANCHISE_SHIPPING_FEE_PER_STORE),
|
||||||
|
(3, 3 * FRANCHISE_SHIPPING_FEE_PER_STORE),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_get_franchise_shipping_fee(count: int, expected: int) -> None:
|
||||||
|
assert get_franchise_shipping_fee(count) == expected
|
||||||
|
|
||||||
|
|
||||||
|
# --- 通贩手续费规则: 边界与典型值 ---
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"amount, expected",
|
||||||
|
[
|
||||||
|
(0, HANDLE_FEE_BELOW),
|
||||||
|
(4999, HANDLE_FEE_BELOW),
|
||||||
|
(5000, 0), # = 5000 -> 免手续费
|
||||||
|
(10000, 0),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_get_handle_fee_boundaries(amount: int, expected: int) -> None:
|
||||||
|
assert get_handle_fee(amount) == expected
|
||||||
|
|
||||||
|
|
||||||
|
# ---- 路由 order_get_shipping_fee 端到端 ----
|
||||||
|
# 直接调用 async 路由函数,绕开 FastAPI 应用初始化(浏览器/redis 等)。
|
||||||
|
def _payload(*items: tuple[int, int, str]) -> OrderShippingFeeRequest:
|
||||||
|
"""items: (goods_number, goods_price, supplier) 三元组列表"""
|
||||||
|
return OrderShippingFeeRequest(
|
||||||
|
item_list=[
|
||||||
|
OrderItem(
|
||||||
|
goods_id=f"g{idx}",
|
||||||
|
goods_number=n,
|
||||||
|
goods_price=p,
|
||||||
|
supplier=s,
|
||||||
|
)
|
||||||
|
for idx, (n, p, s) in enumerate(items)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_route_all_official_below_threshold() -> None:
|
||||||
|
# 1 件 999 元官方商品 -> 官方运费 440, 手续费 240
|
||||||
|
resp = await order_get_shipping_fee(_payload((1, 999, "")))
|
||||||
|
data = resp.data
|
||||||
|
assert data.goods_amount == 999
|
||||||
|
assert data.shipping_fee == 440
|
||||||
|
assert data.handle_amount == 240
|
||||||
|
assert data.order_amount == 999 + 440 + 240
|
||||||
|
|
||||||
|
|
||||||
|
async def test_route_all_official_at_free_shipping_boundary() -> None:
|
||||||
|
# 1 件 1500 元官方商品 -> 官方运费 0(>=1500), 手续费 240(<5000)
|
||||||
|
resp = await order_get_shipping_fee(_payload((1, 1500, "")))
|
||||||
|
data = resp.data
|
||||||
|
assert data.goods_amount == 1500
|
||||||
|
assert data.shipping_fee == 0
|
||||||
|
assert data.handle_amount == 240
|
||||||
|
assert data.order_amount == 1500 + 240
|
||||||
|
|
||||||
|
|
||||||
|
async def test_route_only_franchise_no_official_shipping() -> None:
|
||||||
|
# 全部来自 1 家加盟店 -> 官方运费 0(无官方商品), 加盟运费 800
|
||||||
|
resp = await order_get_shipping_fee(_payload((2, 400, "ShopA")))
|
||||||
|
data = resp.data
|
||||||
|
assert data.goods_amount == 800
|
||||||
|
assert data.shipping_fee == 800
|
||||||
|
assert data.handle_amount == 240
|
||||||
|
assert data.order_amount == 800 + 800 + 240
|
||||||
|
|
||||||
|
|
||||||
|
async def test_route_two_franchise_stores_dedup_by_supplier() -> None:
|
||||||
|
# 2 家加盟店各 1 件 + 官方满 1500 -> 官方 0 + 加盟 1600
|
||||||
|
resp = await order_get_shipping_fee(
|
||||||
|
_payload(
|
||||||
|
(1, 1500, ""),
|
||||||
|
(1, 500, "ShopA"),
|
||||||
|
(1, 500, "ShopA"), # 同店,不重复计费
|
||||||
|
(1, 500, "ShopB"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
data = resp.data
|
||||||
|
assert data.goods_amount == 3000
|
||||||
|
assert data.shipping_fee == 0 + 2 * 800 # 官方 0 + 2 家加盟
|
||||||
|
assert data.handle_amount == 240 # 3000 < 5000
|
||||||
|
assert data.order_amount == 3000 + 1600 + 240
|
||||||
|
|
||||||
|
|
||||||
|
async def test_route_total_amount_over_5000_handle_fee_free() -> None:
|
||||||
|
# 全部商品总额(官方+加盟)>= 5000 时手续费免
|
||||||
|
resp = await order_get_shipping_fee(
|
||||||
|
_payload(
|
||||||
|
(1, 3000, ""), # 官方 3000
|
||||||
|
(1, 2000, "ShopA"), # 加盟 2000
|
||||||
|
)
|
||||||
|
)
|
||||||
|
data = resp.data
|
||||||
|
assert data.goods_amount == 5000
|
||||||
|
# 官方 3000 -> 1500<=3000 -> 0 运费; 加盟 1 家 -> 800
|
||||||
|
assert data.shipping_fee == 0 + 800
|
||||||
|
assert data.handle_amount == 0 # 总额 5000 -> 免
|
||||||
|
assert data.order_amount == 5000 + 800
|
||||||
|
|
||||||
|
|
||||||
|
async def test_route_backward_compatible_default_supplier() -> None:
|
||||||
|
# 旧调用方不传 supplier 字段 -> 默认空串 -> 全部按官方
|
||||||
|
payload = OrderShippingFeeRequest.model_validate(
|
||||||
|
{"item_list": [{"goods_id": "x", "goods_number": 1, "goods_price": 800}]}
|
||||||
|
)
|
||||||
|
resp = await order_get_shipping_fee(payload)
|
||||||
|
data = resp.data
|
||||||
|
# 800 < 1000 -> 官方运费 440, 手续费 240
|
||||||
|
assert data.shipping_fee == 440
|
||||||
|
assert data.handle_amount == 240
|
||||||
|
assert data.order_amount == 800 + 440 + 240
|
||||||
Reference in New Issue
Block a user