Compare commits
No commits in common. "dff20ae83faa2c095d1226a5929ceb95e425870d" and "6e14684f5ddbee6b70e009451d69e778a9275e49" have entirely different histories.
dff20ae83f
...
6e14684f5d
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(python -c \":*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"database": {
|
||||
"host": "192.168.100.33",
|
||||
"port": 3306,
|
||||
"username": "zhenggantian",
|
||||
"password": "123456",
|
||||
"database": "logistics",
|
||||
"charset": "utf8mb4",
|
||||
"pool_size": 10,
|
||||
"max_overflow": 5,
|
||||
"pool_recycle": 3600
|
||||
}
|
||||
}
|
||||
BIN
data/售价尾端价格.xlsx (Stored with Git LFS)
BIN
data/售价尾端价格.xlsx (Stored with Git LFS)
Binary file not shown.
|
|
@ -1,4 +0,0 @@
|
|||
"""数据访问层模块"""
|
||||
from dataaccess.base_dao import BaseDAO
|
||||
|
||||
__all__ = ["BaseDAO"]
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
"""基础数据访问类"""
|
||||
import pandas as pd
|
||||
from typing import Any, Dict, List, Optional
|
||||
from utils.gtools import MySQLconnect
|
||||
|
||||
|
||||
class BaseDAO:
|
||||
"""基础数据访问对象"""
|
||||
|
||||
_cache: Dict[str, pd.DataFrame] = {}
|
||||
_cache_enabled: bool = True
|
||||
|
||||
def __init__(self, table_name: str):
|
||||
self.table_name = table_name
|
||||
|
||||
def _get_connection(self, dbname: str = None) -> MySQLconnect:
|
||||
"""获取数据库连接"""
|
||||
return MySQLconnect(dbname)
|
||||
|
||||
def get_all(self, dbname: str = None) -> pd.DataFrame:
|
||||
"""获取所有数据"""
|
||||
cache_key = f"{dbname}:{self.table_name}"
|
||||
|
||||
if self._cache_enabled and cache_key in self._cache:
|
||||
return self._cache[cache_key]
|
||||
|
||||
with self._get_connection(dbname) as conn:
|
||||
df = pd.read_sql(f"SELECT * FROM {self.table_name}", conn.con)
|
||||
|
||||
if self._cache_enabled:
|
||||
self._cache[cache_key] = df
|
||||
|
||||
return df
|
||||
|
||||
def get_by_condition(self, conditions: Dict[str, Any], dbname: str = None) -> pd.DataFrame:
|
||||
"""根据条件查询数据"""
|
||||
where_clause = " AND ".join([f"{k} = '{v}'" for k, v in conditions.items()])
|
||||
query = f"SELECT * FROM {self.table_name} WHERE {where_clause}"
|
||||
|
||||
with self._get_connection(dbname) as conn:
|
||||
df = pd.read_sql(query, conn.con)
|
||||
|
||||
return df
|
||||
|
||||
def execute_query(self, query: str, dbname: str = None) -> pd.DataFrame:
|
||||
"""执行自定义查询"""
|
||||
with self._get_connection(dbname) as conn:
|
||||
df = pd.read_sql(query, conn.con)
|
||||
|
||||
return df
|
||||
|
||||
@classmethod
|
||||
def clear_cache(cls):
|
||||
"""清空缓存"""
|
||||
cls._cache.clear()
|
||||
|
||||
@classmethod
|
||||
def disable_cache(cls):
|
||||
"""禁用缓存"""
|
||||
cls._cache_enabled = False
|
||||
|
||||
@classmethod
|
||||
def enable_cache(cls):
|
||||
"""启用缓存"""
|
||||
cls._cache_enabled = True
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
"""物流公司数据访问类"""
|
||||
import pandas as pd
|
||||
from typing import Dict, List, Optional
|
||||
from dataaccess.base_dao import BaseDAO
|
||||
|
||||
|
||||
class CompanyDAO(BaseDAO):
|
||||
"""物流公司数据访问"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("logistics_company")
|
||||
|
||||
def get_all_companies(self, country: str = None, active_only: bool = True) -> List[Dict]:
|
||||
"""获取所有物流公司"""
|
||||
conditions = {}
|
||||
if country:
|
||||
conditions["country"] = country
|
||||
if active_only:
|
||||
conditions["active"] = 1
|
||||
|
||||
if conditions:
|
||||
df = self.get_by_condition(conditions)
|
||||
else:
|
||||
df = self.get_all()
|
||||
|
||||
if df.empty:
|
||||
return []
|
||||
|
||||
return df.to_dict("records")
|
||||
|
||||
def get_company_info(self, company_code: str) -> Optional[Dict]:
|
||||
"""获取物流公司详细信息"""
|
||||
df = self.get_by_condition({"company_code": company_code})
|
||||
if df.empty:
|
||||
return None
|
||||
return df.iloc[0].to_dict()
|
||||
|
||||
def get_companies_by_type(self, country: str, logistics_type: str) -> List[Dict]:
|
||||
"""根据物流类型获取公司列表"""
|
||||
query = f"""
|
||||
SELECT * FROM {self.table_name}
|
||||
WHERE country = '{country}'
|
||||
AND logistics_type = '{logistics_type}'
|
||||
AND active = 1
|
||||
"""
|
||||
df = self.execute_query(query)
|
||||
if df.empty:
|
||||
return []
|
||||
return df.to_dict("records")
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
"""英国物流价格数据访问类"""
|
||||
import pandas as pd
|
||||
from typing import Dict, List, Optional
|
||||
from dataaccess.base_dao import BaseDAO
|
||||
|
||||
|
||||
class UKPriceDAO(BaseDAO):
|
||||
"""英国物流价格数据访问"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("uk_logistics_price")
|
||||
|
||||
def get_all_companies(self) -> List[str]:
|
||||
"""获取所有英国物流公司"""
|
||||
df = self.get_all()
|
||||
if df.empty:
|
||||
return []
|
||||
return df["company"].unique().tolist()
|
||||
|
||||
def get_company_price(self, company: str) -> pd.DataFrame:
|
||||
"""获取指定物流公司的价格数据"""
|
||||
return self.get_by_condition({"company": company})
|
||||
|
||||
def get_company_config(self, company: str) -> Dict:
|
||||
"""获取物流公司配置信息"""
|
||||
df = self.get_by_condition({"company": company})
|
||||
if df.empty:
|
||||
return {}
|
||||
|
||||
config = {}
|
||||
for _, row in df.iterrows():
|
||||
key = row.get("config_key")
|
||||
value = row.get("config_value")
|
||||
if key:
|
||||
config[key] = value
|
||||
|
||||
return config
|
||||
|
||||
|
||||
class UKPostcodeDAO(BaseDAO):
|
||||
"""英国邮编分区数据访问"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("uk_postcode_zone")
|
||||
|
||||
def get_zone(self, postcode: str) -> Optional[str]:
|
||||
"""根据邮编获取分区"""
|
||||
postcode_prefix = postcode.split()[0].upper()
|
||||
df = self.get_by_condition({"postcode_prefix": postcode_prefix})
|
||||
|
||||
if df.empty:
|
||||
return None
|
||||
|
||||
return df.iloc[0].get("zone")
|
||||
|
||||
def is_remote(self, postcode: str) -> bool:
|
||||
"""判断是否偏远"""
|
||||
postcode_prefix = postcode.split()[0].upper()
|
||||
remote_prefixes = ["BT", "IM", "JE", "ZE", "GY", "HS", "PO", "IV", "KA", "KW", "PH", "PA"]
|
||||
|
||||
if not any(postcode_prefix.startswith(p) for p in remote_prefixes):
|
||||
return False
|
||||
|
||||
df = self.get_by_condition({"postcode_prefix": postcode_prefix, "is_remote": "1"})
|
||||
return not df.empty
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
"""美国物流价格数据访问类"""
|
||||
import pandas as pd
|
||||
from typing import Dict, List, Optional
|
||||
from dataaccess.base_dao import BaseDAO
|
||||
|
||||
|
||||
class USPriceDAO(BaseDAO):
|
||||
"""美国物流价格数据访问"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("us_logistics_price")
|
||||
|
||||
def get_all_companies(self) -> List[str]:
|
||||
"""获取所有美国物流公司"""
|
||||
df = self.get_all()
|
||||
if df.empty:
|
||||
return []
|
||||
return df["company"].unique().tolist()
|
||||
|
||||
def get_company_price(self, company: str) -> pd.DataFrame:
|
||||
"""获取指定物流公司的价格数据"""
|
||||
return self.get_by_condition({"company": company})
|
||||
|
||||
def get_company_config(self, company: str) -> Dict:
|
||||
"""获取物流公司配置信息"""
|
||||
df = self.get_by_condition({"company": company})
|
||||
if df.empty:
|
||||
return {}
|
||||
|
||||
config = {}
|
||||
for _, row in df.iterrows():
|
||||
key = row.get("config_key")
|
||||
value = row.get("config_value")
|
||||
if key:
|
||||
config[key] = value
|
||||
|
||||
return config
|
||||
|
||||
|
||||
class USPostcodeDAO(BaseDAO):
|
||||
"""美国邮编分区数据访问"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("us_postcode_zone")
|
||||
|
||||
def get_zone(self, postcode: str, port: str = "west") -> Optional[str]:
|
||||
"""根据邮编获取分区"""
|
||||
postcode_5 = postcode[:5]
|
||||
df = self.get_by_condition({"postcode": postcode_5, "port": port})
|
||||
|
||||
if df.empty:
|
||||
return None
|
||||
|
||||
return df.iloc[0].get("zone")
|
||||
|
||||
def get_remote_type(self, postcode: str) -> int:
|
||||
"""获取偏远类型: 0-非偏远, 1-偏远, 2-超偏远, 3-超超偏远"""
|
||||
postcode_5 = postcode[:5]
|
||||
df = self.get_by_condition({"postcode": postcode_5})
|
||||
|
||||
if df.empty:
|
||||
return 0
|
||||
|
||||
return int(df.iloc[0].get("remote_type", 0))
|
||||
|
||||
def is_contiguous(self, postcode: str) -> bool:
|
||||
"""判断是否在本土范围内"""
|
||||
postcode_5 = postcode[:5]
|
||||
df = self.get_by_condition({"postcode": postcode_5})
|
||||
return not df.empty
|
||||
|
|
@ -5,15 +5,14 @@ import re
|
|||
import pytesseract
|
||||
from PIL import Image
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
from selenium.webdriver.chrome.service import Service
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.remote.file_detector import UselessFileDetector #跳过文件检测 1
|
||||
from selenium import webdriver
|
||||
import redis
|
||||
|
||||
def vercode(cookie=None):
|
||||
urls = 'https://cp.baycheer.com:4433/index.php?main=login&act=vercode'#图片链接(每秒更新,距离当前时间最近的时间戳最近为可使用图片)
|
||||
|
||||
|
||||
|
||||
|
||||
urls = 'https://cp.maso.hk:4433/index.php?main=login&act=vercode'#图片链接(每秒更新,距离当前时间最近的时间戳最近为可使用图片)
|
||||
if cookie is not None:
|
||||
haders = {
|
||||
'Cookie': cookie #使用之前的图片链接Cooike
|
||||
|
|
@ -44,7 +43,7 @@ def Vc(user='honghuayuan',pswd= 'a12345'):
|
|||
try :
|
||||
r = redis.StrictRedis(host='192.168.100.44', port=7379, db=11,password="123456")
|
||||
viewcookie = r.get('cpmasosessid'+user)
|
||||
url="https://cp.baycheer.com/index.php?main=panel"#主页
|
||||
url="https://cp.maso.hk/index.php?main=panel"#主页
|
||||
header={'Cookie':viewcookie.decode('utf8')}
|
||||
resp = requests.get(url=url,headers=header)
|
||||
exists = re.findall(r'欢迎进入本公司后台管理系统',resp.text)
|
||||
|
|
@ -58,7 +57,7 @@ def Vc(user='honghuayuan',pswd= 'a12345'):
|
|||
|
||||
|
||||
while resetcookie ==1:
|
||||
urls = 'https://cp.baycheer.com:4433/index.php?main=login&act=vercode'#图片链接(每秒更新,距离当前时间最近的时间戳最近为可使用图片)
|
||||
urls = 'https://cp.maso.hk:4433/index.php?main=login&act=vercode'#图片链接(每秒更新,距离当前时间最近的时间戳最近为可使用图片)
|
||||
res = requests.post(url=urls)
|
||||
ress = re.findall('Cookie (.*?) for',str(res.cookies))#获取图片链接Cooike,图片Cooike必须和登录请求的Cooike保持一致
|
||||
f1 = NamedTemporaryFile(mode='wb+',suffix='.png')
|
||||
|
|
@ -78,7 +77,7 @@ def Vc(user='honghuayuan',pswd= 'a12345'):
|
|||
bim.save(f2)#处理完成导入
|
||||
f2.seek(0)
|
||||
text = pytesseract.image_to_string(Image.open(f2))#开始识别
|
||||
url = 'https://cp.baycheer.com:4433/index.php?main=login&act=check'
|
||||
url = 'https://cp.maso.hk:4433/index.php?main=login&act=check'
|
||||
haders = {
|
||||
'Cookie': ress[0] #使用之前的图片链接Cooike
|
||||
}
|
||||
|
|
@ -205,9 +204,9 @@ def zenid():
|
|||
options.add_argument('--disable-gpu')
|
||||
options.add_argument(' -port=9222')
|
||||
driver=webdriver.Remote(service.service_url,options=options)
|
||||
mainurl='http://cp.baycheer.com/index.php?main=main'
|
||||
panelUrl = "http://cp.baycheer.com/index.php?main=panel"#地址
|
||||
remoteurl='http://cp.baycheer.com/index.php?main=sys_remote_login&act=remotelogin&id=303'
|
||||
mainurl='http://cp.maso.hk/index.php?main=main'
|
||||
panelUrl = "http://cp.maso.hk/index.php?main=panel"#地址
|
||||
remoteurl='http://cp.maso.hk/index.php?main=sys_remote_login&act=remotelogin&id=303'
|
||||
driver.get(mainurl)
|
||||
driver.delete_all_cookies()
|
||||
driver.add_cookie({'name':'sessid','value':f'{t}'})
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import re
|
|||
|
||||
import pandas
|
||||
from logisticsClass.logisticsBaseClass import LogisticsType, TailLogistics
|
||||
from utils.gtools import MySQLconnect
|
||||
|
||||
|
||||
class ZGDPDLogistics_UK(TailLogistics):
|
||||
|
|
@ -292,75 +291,53 @@ class KPNVlogistics_UK(TailLogistics):
|
|||
country = 'United Kingdom'
|
||||
company = '卡派-NV'
|
||||
currency = 'GBP'
|
||||
logistics_type = LogisticsType.EXPRESS
|
||||
|
||||
# 数据库表数据(内存缓存)
|
||||
_zone_cache = None # 邮编分区缓存
|
||||
_price_cache = None # 价格缓存
|
||||
logistics_type = LogisticsType.LTL
|
||||
|
||||
parent_current_directory = Path(__file__).parent.parent
|
||||
price_path = parent_current_directory.joinpath("data")
|
||||
_price_files = price_path.joinpath("英国卡派.xlsx")
|
||||
ltl_cost = None
|
||||
ltl_zone = None
|
||||
def __new__(cls):
|
||||
"""实现单例模式,从数据库加载数据"""
|
||||
if cls._zone_cache is None or cls._price_cache is None:
|
||||
cls._load_from_db()
|
||||
"""实现单例模式,只加载一次文件"""
|
||||
if cls.ltl_cost is None or cls.ltl_zone is None:
|
||||
cls.ltl_cost = pandas.read_excel(cls._price_files,sheet_name="NV运费")
|
||||
cls.ltl_zone = pandas.read_excel(cls._price_files,sheet_name="NV分区")
|
||||
return super().__new__(cls)
|
||||
|
||||
@classmethod
|
||||
def _load_from_db(cls):
|
||||
"""从数据库加载分区和价格数据"""
|
||||
conn = MySQLconnect('logistics')
|
||||
with conn as c:
|
||||
# 加载分区表
|
||||
c.cur.execute("SELECT `邮编`, `区域` FROM uk_XLCarrier_postcode_partition")
|
||||
cls._zone_cache = {}
|
||||
for row in c.cur.fetchall():
|
||||
postcode = str(row[0]) if row[0] else ""
|
||||
zone = str(row[1]) if row[1] else ""
|
||||
cls._zone_cache[postcode] = zone
|
||||
|
||||
# 加载价格表
|
||||
c.cur.execute("SELECT `分区`, `托盘`, `运费` FROM uk_XLCarrier_postcode_fee")
|
||||
cls._price_cache = {}
|
||||
for row in c.cur.fetchall():
|
||||
zone = str(row[0]) if row[0] else ""
|
||||
tuopan = int(row[1]) if row[1] else 0
|
||||
fee = float(row[2]) if row[2] else 0
|
||||
cls._price_cache[(zone, tuopan)] = fee
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.base_fee = 0
|
||||
self.fuel_rate = 0.1
|
||||
|
||||
def is_remote(self, postcode):
|
||||
def is_remote(self,postcode):
|
||||
"""根据邮编分区,返回分区"""
|
||||
postcode_prefix = postcode.split()[0].upper()
|
||||
zone = self._zone_cache.get(postcode_prefix)
|
||||
if zone:
|
||||
return zone
|
||||
postcode_prefix = str(postcode_prefix)
|
||||
zone_df = self.ltl_zone[self.ltl_zone['邮编']== postcode_prefix]
|
||||
if not zone_df.empty:
|
||||
return zone_df['区域'].values[0]
|
||||
return "不在配送范围内"
|
||||
|
||||
def calculate_fee(self, packages, postcode):
|
||||
detail_amount = {
|
||||
"base": 0.00,
|
||||
"fuel": 0.00,
|
||||
"tail_amount": 0.00
|
||||
"base":0.00,
|
||||
"fuel":0.00,
|
||||
"tail_amount":0.00
|
||||
}
|
||||
zone = self.is_remote(postcode)
|
||||
if zone == "不在配送范围内":
|
||||
detail_amount['tail_amount'] = 99999
|
||||
return detail_amount
|
||||
for package in packages:
|
||||
tuopan = math.ceil(package.fst_size / 120)
|
||||
tuopan = math.ceil(package.fst_size/120)
|
||||
tuopan = min(tuopan, 7)
|
||||
fee = self._price_cache.get((zone, tuopan))
|
||||
if fee is None:
|
||||
base_df = self.ltl_cost[(self.ltl_cost['分区']==zone)&(self.ltl_cost['托盘']==tuopan)]
|
||||
if base_df.empty:
|
||||
detail_amount['tail_amount'] = 99999
|
||||
return detail_amount
|
||||
self.base_fee = fee
|
||||
price = self.base_fee * tuopan / len(packages)
|
||||
self.base_fee = base_df['运费'].values[0]
|
||||
price = self.base_fee * tuopan/len(packages)
|
||||
detail_amount['base'] += price
|
||||
detail_amount['fuel'] = detail_amount['base'] * self.fuel_rate
|
||||
detail_amount['tail_amount'] = detail_amount['base'] + detail_amount['fuel']
|
||||
detail_amount['tail_amount'] = detail_amount['base']+detail_amount['fuel']
|
||||
return detail_amount
|
||||
|
||||
class ZGbigLogistics_UK(TailLogistics):
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import pandas
|
|||
from logisticsClass.logisticsBaseClass import LogisticsType, TailLogistics
|
||||
from data.us_zone import zone_west, zone_east
|
||||
from pathlib import Path
|
||||
from utils.gtools import MySQLconnect
|
||||
from utils.gtools import DBconnect
|
||||
"""
|
||||
port:west(default),east
|
||||
currency:str = 'USD'
|
||||
|
|
@ -83,10 +83,6 @@ class FedexLogistics(WestLogistics_US):
|
|||
"""Fedex"""
|
||||
country = "United States"
|
||||
country_code = "US"
|
||||
|
||||
# 价格缓存(子类可覆盖)
|
||||
_price_cache = None
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.volume_weight_ratio:int # lbs抛重系数
|
||||
|
|
@ -112,16 +108,6 @@ class FedexLogistics(WestLogistics_US):
|
|||
self.bigpackage_2:float # 大包裹费
|
||||
self.bigpackage_3:float
|
||||
self.bigpackage_5:float
|
||||
|
||||
def get_price(self, lbs: int, zone: int) -> float:
|
||||
"""获取价格(子类可覆盖)"""
|
||||
if self._price_cache is not None:
|
||||
return self._price_cache.get(lbs, {}).get(zone, 0)
|
||||
# 默认从DataFrame获取(兼容旧代码)
|
||||
if self.base_price is not None:
|
||||
result = self.base_price[self.base_price['lbs.'] == lbs][zone]
|
||||
return result.values[0] if len(result) > 0 else 0
|
||||
return 0
|
||||
self.bigpackage_7:float
|
||||
self.bigpackage_peak:float # 大包裹旺季附加费
|
||||
self.return_package:float # 超大包裹(不可发)
|
||||
|
|
@ -198,8 +184,8 @@ class FedexLogistics(WestLogistics_US):
|
|||
detail_amount['big_package_peak'] += self.bigpackage_peak if package.girth_inch >130 or package.fst_inch >96 else 0
|
||||
detail_amount['residential_delivery'] += self.residential
|
||||
detail_amount['residential_peak'] += self.residential_peak
|
||||
|
||||
detail_amount['base'] += self.get_price(math.ceil(cal_weight), zone)
|
||||
self.base_price.columns = self.base_price.columns.map(str)
|
||||
detail_amount['base'] +=self.base_price[self.base_price['lbs.']==math.ceil(cal_weight)][str(zone)].values[0]
|
||||
|
||||
for key in detail_amount:
|
||||
if key!= 'tail_amount' and key!= 'fuel':
|
||||
|
|
@ -211,37 +197,18 @@ class FedexLogistics(WestLogistics_US):
|
|||
class FedexPPLogistics_US(FedexLogistics):
|
||||
company="Fedex-彩虹小马"
|
||||
|
||||
# 数据库价格缓存
|
||||
_price_cache = None
|
||||
|
||||
parent_current_directory = Path(__file__).parent.parent
|
||||
price_path = parent_current_directory.joinpath("data")
|
||||
_price_files = price_path.joinpath("美国快递.xlsx")
|
||||
base_price = None
|
||||
def __new__(cls):
|
||||
if cls._price_cache is None:
|
||||
cls._load_from_db()
|
||||
if cls.base_price is None:
|
||||
cls.base_price = pandas.read_excel(cls._price_files,sheet_name='邮差小马')
|
||||
return super().__new__(cls)
|
||||
|
||||
@classmethod
|
||||
def _load_from_db(cls):
|
||||
"""从数据库加载价格数据"""
|
||||
conn = MySQLconnect('logistics')
|
||||
with conn as c:
|
||||
c.cur.execute("SELECT `lbs.`, `2`, `3`, `4`, `5`, `6`, `7`, `8` FROM us_delivery_postpony")
|
||||
cls._price_cache = {}
|
||||
for row in c.cur.fetchall():
|
||||
lbs = int(row[0])
|
||||
cls._price_cache[lbs] = {
|
||||
2: float(row[1]) if row[1] else 0,
|
||||
3: float(row[2]) if row[2] else 0,
|
||||
4: float(row[3]) if row[3] else 0,
|
||||
5: float(row[4]) if row[4] else 0,
|
||||
6: float(row[5]) if row[5] else 0,
|
||||
7: float(row[6]) if row[6] else 0,
|
||||
8: float(row[7]) if row[7] else 0,
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.volume_weight_ratio=250 # lbs抛重系数
|
||||
self.residential = 6.38
|
||||
self.residential = 6.38
|
||||
self.residential_peak = 0 # 0.33 0.6 # 报价表没写,账单有
|
||||
self.oversize_2 = 4.50
|
||||
self.oversize_3 = 4.99
|
||||
|
|
@ -263,7 +230,7 @@ class FedexPPLogistics_US(FedexLogistics):
|
|||
self.bigpackage_3 = 36.16
|
||||
self.bigpackage_5 = 38.57
|
||||
self.bigpackage_7 = 41.78
|
||||
self.bigpackage_peak = 0 # 45.26 53.56# 大包裹旺季附加费
|
||||
self.bigpackage_peak = 0 # 45.26 53.56# 大包裹旺季附加费
|
||||
self.fuel_rate = 0.16 # 燃油费率
|
||||
self.return_package = 1419.34 # 超大包裹(不可发)
|
||||
|
||||
|
|
@ -271,33 +238,14 @@ class FedexKHLogistics_US(FedexLogistics):
|
|||
"""金宏亚"""
|
||||
company = "Fedex-金宏亚"
|
||||
|
||||
# 数据库价格缓存
|
||||
_price_cache = None
|
||||
|
||||
parent_current_directory = Path(__file__).parent.parent
|
||||
price_path = parent_current_directory.joinpath("data")
|
||||
_price_files = price_path.joinpath("美国快递.xlsx")
|
||||
base_price = None
|
||||
def __new__(cls):
|
||||
if cls._price_cache is None:
|
||||
cls._load_from_db()
|
||||
if cls.base_price is None:
|
||||
cls.base_price = pandas.read_excel(cls._price_files,sheet_name='金宏亚')
|
||||
return super().__new__(cls)
|
||||
|
||||
@classmethod
|
||||
def _load_from_db(cls):
|
||||
"""从数据库加载价格数据"""
|
||||
conn = MySQLconnect('logistics')
|
||||
with conn as c:
|
||||
c.cur.execute("SELECT `lbs.`, `2`, `3`, `4`, `5`, `6`, `7`, `8` FROM us_delivery_kinghood")
|
||||
cls._price_cache = {}
|
||||
for row in c.cur.fetchall():
|
||||
lbs = int(row[0])
|
||||
cls._price_cache[lbs] = {
|
||||
2: float(row[1]) if row[1] else 0,
|
||||
3: float(row[2]) if row[2] else 0,
|
||||
4: float(row[3]) if row[3] else 0,
|
||||
5: float(row[4]) if row[4] else 0,
|
||||
6: float(row[5]) if row[5] else 0,
|
||||
7: float(row[6]) if row[6] else 0,
|
||||
8: float(row[7]) if row[7] else 0,
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.volume_weight_ratio = 250 # lbs抛重系数
|
||||
|
|
@ -331,32 +279,15 @@ class FedexHOMELogistics_US(FedexLogistics):
|
|||
"""FEDEX-HOME (1-35%-30%)"""
|
||||
company = "Fedex-HOME"
|
||||
|
||||
# 数据库价格缓存
|
||||
_price_cache = None
|
||||
|
||||
base_price = None
|
||||
_instance = None
|
||||
def __new__(cls):
|
||||
if cls._price_cache is None:
|
||||
cls._load_from_db()
|
||||
return super().__new__(cls)
|
||||
|
||||
@classmethod
|
||||
def _load_from_db(cls):
|
||||
"""从数据库加载价格数据"""
|
||||
conn = MySQLconnect('logistics')
|
||||
with conn as c:
|
||||
c.cur.execute("SELECT `lbs.`, `2`, `3`, `4`, `5`, `6`, `7`, `8` FROM us_fedex_home")
|
||||
cls._price_cache = {}
|
||||
for row in c.cur.fetchall():
|
||||
lbs = int(row[0])
|
||||
cls._price_cache[lbs] = {
|
||||
2: float(row[1]) if row[1] else 0,
|
||||
3: float(row[2]) if row[2] else 0,
|
||||
4: float(row[3]) if row[3] else 0,
|
||||
5: float(row[4]) if row[4] else 0,
|
||||
6: float(row[5]) if row[5] else 0,
|
||||
7: float(row[6]) if row[6] else 0,
|
||||
8: float(row[7]) if row[7] else 0,
|
||||
}
|
||||
if cls.base_price is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
with DBconnect() as db:
|
||||
cls.base_price = pandas.read_sql("SELECT * FROM us_fedex_home", db.engine())
|
||||
return cls._instance
|
||||
return cls._instance
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.volume_weight_ratio = 250 # lbs抛重系数
|
||||
|
|
@ -385,38 +316,18 @@ class FedexHOMELogistics_US(FedexLogistics):
|
|||
self.bigpackage_peak =0 # 42.25# 大包裹旺季附加费
|
||||
self.return_package = 1325 # 超大包裹(不可发)
|
||||
self.fuel_rate = 0.18 # 燃油费率
|
||||
|
||||
class FedexGROUDLogistics_US(FedexLogistics):
|
||||
"""FEDEX-GROUD (1-35%-30%)"""
|
||||
company = "Fedex-GROUD"
|
||||
|
||||
# 数据库价格缓存(与FedexHOMELogistics_US共享us_fedex_home表)
|
||||
_price_cache = None
|
||||
|
||||
parent_current_directory = Path(__file__).parent.parent
|
||||
price_path = parent_current_directory.joinpath("data")
|
||||
_price_files = price_path.joinpath("美国快递.xlsx")
|
||||
base_price = None
|
||||
def __new__(cls):
|
||||
if cls._price_cache is None:
|
||||
cls._load_from_db()
|
||||
if cls.base_price is None:
|
||||
cls.base_price = pandas.read_excel(cls._price_files,sheet_name='FEDEX')
|
||||
return super().__new__(cls)
|
||||
|
||||
@classmethod
|
||||
def _load_from_db(cls):
|
||||
"""从数据库加载价格数据"""
|
||||
conn = MySQLconnect('logistics')
|
||||
with conn as c:
|
||||
c.cur.execute("SELECT `lbs.`, `2`, `3`, `4`, `5`, `6`, `7`, `8` FROM us_fedex_home")
|
||||
cls._price_cache = {}
|
||||
for row in c.cur.fetchall():
|
||||
lbs = int(row[0])
|
||||
cls._price_cache[lbs] = {
|
||||
2: float(row[1]) if row[1] else 0,
|
||||
3: float(row[2]) if row[2] else 0,
|
||||
4: float(row[3]) if row[3] else 0,
|
||||
5: float(row[4]) if row[4] else 0,
|
||||
6: float(row[5]) if row[5] else 0,
|
||||
7: float(row[6]) if row[6] else 0,
|
||||
8: float(row[7]) if row[7] else 0,
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.volume_weight_ratio = 250 # lbs抛重系数
|
||||
|
|
|
|||
|
|
@ -1,165 +0,0 @@
|
|||
"""物流费用计算统一入口服务"""
|
||||
import re
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
from utils.Package import Package, Package_group
|
||||
from utils.countryOperator import OperateCountry
|
||||
from logisticsClass.logisticsBaseClass import PortType
|
||||
|
||||
|
||||
class LogisticsService:
|
||||
"""物流费用计算统一服务"""
|
||||
|
||||
@staticmethod
|
||||
def _detect_country(postcode: str) -> str:
|
||||
"""根据邮编格式自动识别国家"""
|
||||
postcode = postcode.strip().upper()
|
||||
|
||||
# 英国邮编格式:字母+数字(+字母)+空格+数字+字母
|
||||
# 支持格式: SW1A 1AA, M1 1AA, BT1 1AA, AA11 1AA 等
|
||||
if re.match(r'^[A-Z]{1,2}[0-9][A-Z0-9]?\s?[0-9][A-Z]{2}$', postcode):
|
||||
return "UK"
|
||||
|
||||
# 美国邮编格式:5位数字或5位-4位
|
||||
if re.match(r'^\d{5}(-\d{4})?$', postcode):
|
||||
return "US"
|
||||
|
||||
# 澳洲邮编格式:4位数字
|
||||
if re.match(r'^\d{4}$', postcode):
|
||||
return "AU"
|
||||
|
||||
# 欧洲格式(德国、法国等)
|
||||
if re.match(r'^\d{5}$', postcode):
|
||||
return "DE" # 默认德国
|
||||
|
||||
raise ValueError(f"无法识别的邮编格式: {postcode}")
|
||||
|
||||
@staticmethod
|
||||
def _parse_packages(packages_data: List[Dict]) -> Package_group:
|
||||
"""解析包裹数据"""
|
||||
packages = []
|
||||
for i, pkg in enumerate(packages_data):
|
||||
name = pkg.get("name", f"包裹{i+1}")
|
||||
length = pkg.get("length", 0)
|
||||
width = pkg.get("width", 0)
|
||||
height = pkg.get("height", 0)
|
||||
weight = pkg.get("weight", 0)
|
||||
|
||||
package = Package(name, length, width, height, weight)
|
||||
packages.append(package)
|
||||
|
||||
return Package_group(packages)
|
||||
|
||||
@staticmethod
|
||||
def calculate(postcode: str, packages_data: List[Dict],
|
||||
port: PortType = PortType.DEFAULT) -> Dict[str, Any]:
|
||||
"""
|
||||
计算物流费用并返回最优渠道
|
||||
|
||||
Args:
|
||||
postcode: 收件人邮编
|
||||
packages_data: 包裹数据列表,格式为:
|
||||
[
|
||||
{"length": 63, "width": 59, "height": 48, "weight": 8000}, # 单位:cm, g
|
||||
...
|
||||
]
|
||||
port: 港口类型,默认DEFAULT
|
||||
|
||||
Returns:
|
||||
包含最优渠道和所有渠道费用的字典
|
||||
"""
|
||||
# 1. 识别国家
|
||||
country = LogisticsService._detect_country(postcode)
|
||||
|
||||
# 2. 解析包裹
|
||||
packages = LogisticsService._parse_packages(packages_data)
|
||||
|
||||
# 3. 创建国家操作对象
|
||||
op_country = OperateCountry(country, port, packages, postcode)
|
||||
|
||||
# 4. 获取所有渠道费用
|
||||
all_fees = op_country.get_all_tail_info()
|
||||
|
||||
# 5. 找出最优渠道
|
||||
valid_fees = {k: v for k, v in all_fees.items() if v < 99999}
|
||||
if not valid_fees:
|
||||
return {
|
||||
"country": country,
|
||||
"postcode": postcode,
|
||||
"optimal_channel": None,
|
||||
"optimal_fee": None,
|
||||
"currency": None,
|
||||
"all_channels": all_fees,
|
||||
"error": "所有渠道均不可用"
|
||||
}
|
||||
|
||||
optimal_channel = min(valid_fees, key=valid_fees.get)
|
||||
optimal_fee = valid_fees[optimal_channel]
|
||||
currency = op_country.get_tail_currency(optimal_channel)
|
||||
|
||||
# 6. 构建结果
|
||||
result = {
|
||||
"country": country,
|
||||
"postcode": postcode,
|
||||
"optimal_channel": optimal_channel,
|
||||
"optimal_fee": optimal_fee,
|
||||
"currency": currency,
|
||||
"all_channels": {},
|
||||
"package_count": len(packages_data),
|
||||
"total_weight": sum(p.weight for p in packages) / 1000, # kg
|
||||
}
|
||||
|
||||
# 7. 添加所有渠道详情
|
||||
for company, fee in all_fees.items():
|
||||
company_type = op_country.get_logistic_type(company)
|
||||
company_currency = op_country.get_tail_currency(company)
|
||||
result["all_channels"][company] = {
|
||||
"fee": fee if fee < 99999 else None,
|
||||
"currency": company_currency,
|
||||
"type": company_type,
|
||||
"available": fee < 99999
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def calculate_us(postcode: str, packages_data: List[Dict],
|
||||
port: PortType = PortType.DEFAULT) -> Dict[str, Any]:
|
||||
"""计算美国物流费用"""
|
||||
return LogisticsService.calculate(postcode, packages_data, port)
|
||||
|
||||
@staticmethod
|
||||
def calculate_uk(postcode: str, packages_data: List[Dict],
|
||||
port: PortType = PortType.DEFAULT) -> Dict[str, Any]:
|
||||
"""计算英国物流费用"""
|
||||
return LogisticsService.calculate(postcode, packages_data, port)
|
||||
|
||||
@staticmethod
|
||||
def calculate_au(postcode: str, packages_data: List[Dict],
|
||||
port: PortType = PortType.DEFAULT) -> Dict[str, Any]:
|
||||
"""计算澳洲物流费用"""
|
||||
return LogisticsService.calculate(postcode, packages_data, port)
|
||||
|
||||
@staticmethod
|
||||
def calculate_eur(postcode: str, packages_data: List[Dict],
|
||||
port: PortType = PortType.DEFAULT) -> Dict[str, Any]:
|
||||
"""计算欧洲物流费用"""
|
||||
return LogisticsService.calculate(postcode, packages_data, port)
|
||||
|
||||
@staticmethod
|
||||
def get_company_detail(postcode: str, packages_data: List[Dict],
|
||||
company_name: str) -> Dict[str, Any]:
|
||||
"""获取指定物流公司的费用明细"""
|
||||
country = LogisticsService._detect_country(postcode)
|
||||
packages = LogisticsService._parse_packages(packages_data)
|
||||
op_country = OperateCountry(country, PortType.DEFAULT, packages, postcode)
|
||||
|
||||
detail = op_country.get_detail_amount(company_name, packages, postcode)
|
||||
currency = op_country.get_tail_currency(company_name)
|
||||
|
||||
return {
|
||||
"company": company_name,
|
||||
"currency": currency,
|
||||
"detail": detail,
|
||||
"total": detail.get("tail_amount", 0)
|
||||
}
|
||||
|
|
@ -1,469 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
数据导入脚本
|
||||
将XLSX文件中的数据导入到数据库
|
||||
"""
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from utils.gtools import MySQLconnect
|
||||
from utils.config_manager import config
|
||||
|
||||
|
||||
class DataImporter:
|
||||
"""数据导入类"""
|
||||
|
||||
def __init__(self):
|
||||
self.data_dir = Path(__file__).parent.parent / "data"
|
||||
|
||||
def get_connection(self, dbname: str = None):
|
||||
"""获取数据库连接"""
|
||||
return MySQLconnect(dbname)
|
||||
|
||||
def execute_sql(self, sql: str, dbname: str = None):
|
||||
"""执行SQL语句"""
|
||||
with self.get_connection(dbname) as conn:
|
||||
conn.cur.execute(sql)
|
||||
conn.con.commit()
|
||||
|
||||
def execute_many(self, sql: str, data: list, dbname: str = None):
|
||||
"""批量执行SQL"""
|
||||
with self.get_connection(dbname) as conn:
|
||||
conn.cur.executemany(sql, data)
|
||||
conn.con.commit()
|
||||
|
||||
def truncate_table(self, table_name: str, dbname: str = None):
|
||||
"""清空表"""
|
||||
self.execute_sql(f"TRUNCATE TABLE {table_name}", dbname)
|
||||
|
||||
# ==================== 英国数据导入 ====================
|
||||
|
||||
def import_uk_postcode_zone(self):
|
||||
"""导入英国邮编分区"""
|
||||
print("导入英国邮编分区...")
|
||||
df = pd.read_excel(self.data_dir / "英国卡派.xlsx", sheet_name="分区")
|
||||
df.columns = ["postcode_prefix", "zone"]
|
||||
|
||||
data = []
|
||||
for _, row in df.iterrows():
|
||||
postcode = str(row["postcode_prefix"]).strip()
|
||||
zone = str(row["zone"]).strip()
|
||||
is_remote = 0
|
||||
data.append((postcode, zone, is_remote))
|
||||
|
||||
sql = "INSERT IGNORE INTO uk_postcode_zone (postcode_prefix, zone, is_remote) VALUES (%s, %s, %s)"
|
||||
self.execute_many(sql, data)
|
||||
print(f" 已导入 {len(data)} 条记录")
|
||||
|
||||
def import_uk_kp_nv_price(self):
|
||||
"""导入英国卡派NV运费"""
|
||||
print("导入英国卡派NV运费...")
|
||||
df = pd.read_excel(self.data_dir / "英国卡派.xlsx", sheet_name="运费")
|
||||
df.columns = ["zone", "tuopan", "fee"]
|
||||
|
||||
data = []
|
||||
for _, row in df.iterrows():
|
||||
zone = str(row["zone"]).strip()
|
||||
tuopan = int(row["tuopan"])
|
||||
fee = float(row["fee"])
|
||||
data.append((zone, tuopan, fee))
|
||||
|
||||
sql = "INSERT INTO uk_kp_nv_price (zone, tuopan, fee) VALUES (%s, %s, %s)"
|
||||
self.execute_many(sql, data)
|
||||
print(f" 已导入 {len(data)} 条记录")
|
||||
|
||||
# ==================== 美国数据导入 ====================
|
||||
|
||||
def import_us_fedex_pp_price(self):
|
||||
"""导入美国Fedex邮差小马价格"""
|
||||
print("导入美国Fedex邮差小马价格...")
|
||||
df = pd.read_excel(self.data_dir / "美国快递.xlsx", sheet_name="邮差小马")
|
||||
|
||||
# 转换列名
|
||||
cols = ["lbs"] + [str(c) for c in df.columns[1:]]
|
||||
df.columns = cols
|
||||
|
||||
data = []
|
||||
for _, row in df.iterrows():
|
||||
lbs = int(row["lbs"])
|
||||
row_data = [lbs]
|
||||
for i in range(2, 9):
|
||||
val = row.get(str(i), 0)
|
||||
row_data.append(float(val) if pd.notna(val) else 0)
|
||||
data.append(tuple(row_data))
|
||||
|
||||
sql = """INSERT INTO us_fedex_pp_price
|
||||
(lbs, zone_2, zone_3, zone_4, zone_5, zone_6, zone_7, zone_8)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)"""
|
||||
self.execute_many(sql, data)
|
||||
print(f" 已导入 {len(data)} 条记录")
|
||||
|
||||
def import_us_fedex_kh_price(self):
|
||||
"""导入美国Fedex金宏亚价格"""
|
||||
print("导入美国Fedex金宏亚价格...")
|
||||
df = pd.read_excel(self.data_dir / "美国快递.xlsx", sheet_name="金宏亚")
|
||||
|
||||
cols = ["lbs"] + [str(c) for c in df.columns[1:]]
|
||||
df.columns = cols
|
||||
|
||||
data = []
|
||||
for _, row in df.iterrows():
|
||||
lbs = int(row["lbs"])
|
||||
row_data = [lbs]
|
||||
for i in range(2, 9):
|
||||
val = row.get(str(i), 0)
|
||||
row_data.append(float(val) if pd.notna(val) else 0)
|
||||
data.append(tuple(row_data))
|
||||
|
||||
sql = """INSERT INTO us_fedex_kh_price
|
||||
(lbs, zone_2, zone_3, zone_4, zone_5, zone_6, zone_7, zone_8)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)"""
|
||||
self.execute_many(sql, data)
|
||||
print(f" 已导入 {len(data)} 条记录")
|
||||
|
||||
def import_us_fedex_price(self):
|
||||
"""导入美国Fedex价格"""
|
||||
print("导入美国Fedex价格...")
|
||||
|
||||
for sheet_name in ["FEDEX", "FEDEX国内"]:
|
||||
try:
|
||||
df = pd.read_excel(self.data_dir / "美国快递.xlsx", sheet_name=sheet_name)
|
||||
if "lbs." in df.columns:
|
||||
cols = ["lbs"] + [str(c) for c in df.columns[1:8]]
|
||||
df.columns = cols
|
||||
|
||||
data = []
|
||||
for _, row in df.iterrows():
|
||||
lbs = int(row["lbs"])
|
||||
row_data = [lbs]
|
||||
for i in range(2, 9):
|
||||
val = row.get(str(i), 0)
|
||||
row_data.append(float(val) if pd.notna(val) else 0)
|
||||
data.append(tuple(row_data))
|
||||
|
||||
table = "us_fedex_price" if "FEDEX" in sheet_name and "国内" not in sheet_name else "us_fedex_price"
|
||||
sql = f"""INSERT INTO {table}
|
||||
(lbs, zone_2, zone_3, zone_4, zone_5, zone_6, zone_7, zone_8)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)"""
|
||||
self.execute_many(sql, data)
|
||||
print(f" 已导入 {len(data)} 条记录 ({sheet_name})")
|
||||
except Exception as e:
|
||||
print(f" 跳过 {sheet_name}: {e}")
|
||||
|
||||
def import_us_giga_price(self):
|
||||
"""导入美国GIGA价格"""
|
||||
print("导入美国GIGA价格...")
|
||||
df = pd.read_excel(self.data_dir / "GIGA base_fee_20240607223514.xlsx", sheet_name="Local Fee Data")
|
||||
|
||||
data = []
|
||||
for _, row in df.iterrows():
|
||||
zip_code = str(int(row["Zip Code"])) if pd.notna(row["Zip Code"]) else ""
|
||||
delivery_warehouse = str(row["Delivery Warehouse"]) if pd.notna(row["Delivery Warehouse"]) else ""
|
||||
general_area = str(row["General Area"]) if pd.notna(row["General Area"]) else ""
|
||||
fee_type = str(row["Fee Type"]) if pd.notna(row["Fee Type"]) else ""
|
||||
zone = str(row["Zone"]) if pd.notna(row["Zone"]) else ""
|
||||
local_pickup_fee = float(row["Local Pickup Fee"]) if pd.notna(row["Local Pickup Fee"]) else 0
|
||||
warehouse_handling_fee = float(row["Warehouse Handling Fee"]) if pd.notna(row["Warehouse Handling Fee"]) else 0
|
||||
delivery_fee_rate = float(row["Delivery Fee Rate"]) if pd.notna(row["Delivery Fee Rate"]) else 0
|
||||
additional_delivery_fee = float(row["Additional Delivery Fee"]) if pd.notna(row["Additional Delivery Fee"]) else 0
|
||||
assembly_fee = float(row["Assembly Fee"]) if pd.notna(row["Assembly Fee"]) else 0
|
||||
|
||||
data.append((zip_code, delivery_warehouse, general_area, fee_type, zone,
|
||||
local_pickup_fee, warehouse_handling_fee, delivery_fee_rate,
|
||||
additional_delivery_fee, assembly_fee))
|
||||
|
||||
sql = """INSERT INTO us_giga_price
|
||||
(zip_code, delivery_warehouse, general_area, fee_type, zone,
|
||||
local_pickup_fee, warehouse_handling_fee, delivery_fee_rate,
|
||||
additional_delivery_fee, assembly_fee)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)"""
|
||||
self.execute_many(sql, data)
|
||||
print(f" 已导入 {len(data)} 条记录")
|
||||
|
||||
def import_us_ceva_price(self):
|
||||
"""导入美国CEVA价格"""
|
||||
print("导入美国CEVA价格...")
|
||||
|
||||
# CEVA base rate
|
||||
df = pd.read_excel(self.data_dir / "CEVA.xlsx", sheet_name="ceva_base_rate")
|
||||
df.columns = ["ceva_weight"] + list(df.columns[1:])
|
||||
|
||||
data = []
|
||||
for _, row in df.iterrows():
|
||||
ceva_weight = row["ceva_weight"]
|
||||
if pd.isna(ceva_weight):
|
||||
continue
|
||||
row_data = [ceva_weight]
|
||||
for col in df.columns[1:]:
|
||||
val = row[col]
|
||||
row_data.append(float(val) if pd.notna(val) else 0)
|
||||
data.append(tuple(row_data))
|
||||
|
||||
sql = """INSERT INTO us_ceva_price
|
||||
(ceva_weight, zone_ca, zone_wa, zone_or, zone_nv, zone_az,
|
||||
zone_co, zone_ut, zone_nm, remote_area_surcharge)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)"""
|
||||
self.execute_many(sql, data)
|
||||
print(f" 已导入 {len(data)} 条记录 (ceva_base_rate)")
|
||||
|
||||
# CEVA remote zone
|
||||
df = pd.read_excel(self.data_dir / "CEVA.xlsx", sheet_name="remote_zone")
|
||||
df.columns = ["postal_code", "state", "beyond_zone", "remote_type"]
|
||||
|
||||
data = []
|
||||
for _, row in df.iterrows():
|
||||
postal_code = str(int(row["postal_code"])) if pd.notna(row["postal_code"]) else ""
|
||||
state = str(row["state"]) if pd.notna(row["state"]) else ""
|
||||
beyond_zone = str(row["beyond_zone"]) if pd.notna(row["beyond_zone"]) else ""
|
||||
remote_type = str(row["remote_type"]) if pd.notna(row["remote_type"]) else "standard"
|
||||
data.append((postal_code, state, beyond_zone, remote_type))
|
||||
|
||||
sql = "INSERT IGNORE INTO us_ceva_zone (postal_code, state, beyond_zone, remote_type) VALUES (%s, %s, %s, %s)"
|
||||
self.execute_many(sql, data)
|
||||
print(f" 已导入 {len(data)} 条记录 (remote_zone)")
|
||||
|
||||
def import_us_metro_price(self):
|
||||
"""导入美国Metro价格"""
|
||||
print("导入美国Metro价格...")
|
||||
|
||||
for sheet_name in ["cuft_25", "cuft_35", "over35_per_cuft", "over35_min"]:
|
||||
try:
|
||||
df = pd.read_excel(self.data_dir / "Metro.xlsx", sheet_name=sheet_name)
|
||||
if "Origins" not in df.columns:
|
||||
continue
|
||||
|
||||
data = []
|
||||
for _, row in df.iterrows():
|
||||
origins = str(row["Origins"]) if pd.notna(row["Origins"]) else ""
|
||||
row_data = [origins]
|
||||
for col in df.columns[1:]:
|
||||
val = row[col]
|
||||
row_data.append(float(val) if pd.notna(val) else 0)
|
||||
data.append(tuple(row_data))
|
||||
|
||||
cols = ", ".join([f"zone_{i}l" for i in range(1, 10)])
|
||||
sql = f"INSERT INTO us_metro_price (origins, {cols}) VALUES (%s, {cols.replace('zone_', 'zone_')})"
|
||||
# 简化处理
|
||||
sql = "INSERT INTO us_metro_price (origins, zone_1l, zone_2l, zone_3l, zone_4l, zone_5l, zone_6l, zone_7l, zone_8l, zone_9l) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)"
|
||||
self.execute_many(sql, data)
|
||||
print(f" 已导入 {len(data)} 条记录 ({sheet_name})")
|
||||
except Exception as e:
|
||||
print(f" 跳过 {sheet_name}: {e}")
|
||||
|
||||
# Metro zone table
|
||||
try:
|
||||
df = pd.read_excel(self.data_dir / "Metro.xlsx", sheet_name="zone_table")
|
||||
df.columns = ["zip_code", "new_zone_name"]
|
||||
|
||||
data = []
|
||||
for _, row in df.iterrows():
|
||||
zip_code = str(int(row["zip_code"])) if pd.notna(row["zip_code"]) else ""
|
||||
zone = str(row["new_zone_name"]) if pd.notna(row["new_zone_name"]) else ""
|
||||
data.append((zip_code, zone))
|
||||
|
||||
sql = "INSERT IGNORE INTO us_metro_zone (zip_code, new_zone_name) VALUES (%s, %s)"
|
||||
self.execute_many(sql, data)
|
||||
print(f" 已导入 {len(data)} 条记录 (zone_table)")
|
||||
except Exception as e:
|
||||
print(f" zone_table: {e}")
|
||||
|
||||
def import_us_xmiles_zone(self):
|
||||
"""导入美国XMILES邮编"""
|
||||
print("导入美国XMILES邮编...")
|
||||
df = pd.read_excel(self.data_dir / "XMILES.xlsx", sheet_name="postcode_table")
|
||||
|
||||
# 处理可能的列名问题
|
||||
cols = df.columns.tolist()
|
||||
if len(cols) >= 2:
|
||||
df.columns = ["postcode", "area"]
|
||||
|
||||
data = []
|
||||
for _, row in df.iterrows():
|
||||
postcode = str(int(row["postcode"])) if pd.notna(row["postcode"]) else ""
|
||||
area = str(row["area"]) if pd.notna(row["area"]) else ""
|
||||
data.append((postcode, area))
|
||||
|
||||
sql = "INSERT IGNORE INTO us_xmiles_zone (postcode, area) VALUES (%s, %s)"
|
||||
self.execute_many(sql, data)
|
||||
print(f" 已导入 {len(data)} 条记录")
|
||||
|
||||
def import_us_am_price(self):
|
||||
"""导入美国AM卡派价格"""
|
||||
print("导入美国AM卡派价格...")
|
||||
|
||||
# price表
|
||||
df = pd.read_excel(self.data_dir / "美国卡派-AM.xlsx", sheet_name="price")
|
||||
df.columns = ["pu_zone", "dl_zone", "zone_combo", "minimum", "maximum",
|
||||
"fee_without_sc", "shipping_cost", "internalid", "externalid", "surcharge"]
|
||||
|
||||
data = []
|
||||
for _, row in df.iterrows():
|
||||
pu_zone = str(row["pu_zone"]) if pd.notna(row["pu_zone"]) else ""
|
||||
dl_zone = str(row["dl_zone"]) if pd.notna(row["dl_zone"]) else ""
|
||||
zone_combo = str(row["zone_combo"]) if pd.notna(row["zone_combo"]) else ""
|
||||
minimum = float(row["minimum"]) if pd.notna(row["minimum"]) else 0
|
||||
maximum = float(row["maximum"]) if pd.notna(row["maximum"]) else 0
|
||||
fee_without_sc = float(row["fee_without_sc"]) if pd.notna(row["fee_without_sc"]) else 0
|
||||
shipping_cost = float(row["shipping_cost"]) if pd.notna(row["shipping_cost"]) else 0
|
||||
surcharge = float(row["surcharge"]) if pd.notna(row["surcharge"]) else 0
|
||||
|
||||
data.append((pu_zone, dl_zone, zone_combo, minimum, maximum,
|
||||
fee_without_sc, shipping_cost, surcharge))
|
||||
|
||||
sql = """INSERT INTO us_am_price
|
||||
(pu_zone, dl_zone, zone_combo, minimum_weight, maximum_weight,
|
||||
fee_without_sc, shipping_cost, surcharge)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)"""
|
||||
self.execute_many(sql, data)
|
||||
print(f" 已导入 {len(data)} 条记录 (price)")
|
||||
|
||||
# postcode表
|
||||
df = pd.read_excel(self.data_dir / "美国卡派-AM.xlsx", sheet_name="postcode_table")
|
||||
df.columns = ["zip_code", "zone"]
|
||||
|
||||
data = []
|
||||
for _, row in df.iterrows():
|
||||
zip_code = str(int(row["zip_code"])) if pd.notna(row["zip_code"]) else ""
|
||||
zone = str(row["zone"]) if pd.notna(row["zone"]) else ""
|
||||
data.append((zip_code, zone))
|
||||
|
||||
sql = "INSERT IGNORE INTO us_am_postcode (zip_code, zone) VALUES (%s, %s)"
|
||||
self.execute_many(sql, data)
|
||||
print(f" 已导入 {len(data)} 条记录 (postcode_table)")
|
||||
|
||||
# ==================== 澳洲数据导入 ====================
|
||||
|
||||
def import_au_eparcel_price(self):
|
||||
"""导入澳洲eparcel价格"""
|
||||
print("导入澳洲eparcel价格...")
|
||||
df = pd.read_excel(self.data_dir / "澳洲三大渠道.xlsx", sheet_name="eparcel")
|
||||
|
||||
cols = ["post"] + [str(c) for c in df.columns[1:]]
|
||||
df.columns = cols
|
||||
|
||||
data = []
|
||||
for _, row in df.iterrows():
|
||||
post = str(row["post"]) if pd.notna(row["post"]) else ""
|
||||
row_data = [post]
|
||||
for col in cols[1:]:
|
||||
val = row.get(col, 0)
|
||||
row_data.append(float(val) if pd.notna(val) else 0)
|
||||
data.append(tuple(row_data))
|
||||
|
||||
sql = """INSERT INTO au_eparcel_price
|
||||
(post, weight_0_5, weight_1, weight_2, weight_3, weight_4,
|
||||
weight_5, weight_7, weight_10, weight_15)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)"""
|
||||
self.execute_many(sql, data)
|
||||
print(f" 已导入 {len(data)} 条记录")
|
||||
|
||||
def import_au_all(self):
|
||||
"""导入澳洲toll和allied数据"""
|
||||
print("导入澳洲toll和allied数据...")
|
||||
|
||||
# toll
|
||||
try:
|
||||
df = pd.read_excel(self.data_dir / "澳洲三大渠道.xlsx", sheet_name="toll")
|
||||
df.columns = ["post", "zone_1", "zone_2", "zone_3", "zone_4"]
|
||||
|
||||
data = []
|
||||
for _, row in df.iterrows():
|
||||
post = str(row["post"]) if pd.notna(row["post"]) else ""
|
||||
data.append((post,
|
||||
float(row["zone_1"]) if pd.notna(row["zone_1"]) else 0,
|
||||
float(row["zone_2"]) if pd.notna(row["zone_2"]) else 0,
|
||||
float(row["zone_3"]) if pd.notna(row["zone_3"]) else 0,
|
||||
float(row["zone_4"]) if pd.notna(row["zone_4"]) else 0))
|
||||
|
||||
sql = "INSERT INTO au_toll_price (post, zone_1, zone_2, zone_3, zone_4) VALUES (%s, %s, %s, %s, %s)"
|
||||
self.execute_many(sql, data)
|
||||
print(f" 已导入 {len(data)} 条记录 (toll)")
|
||||
except Exception as e:
|
||||
print(f" toll: {e}")
|
||||
|
||||
# allied
|
||||
try:
|
||||
df = pd.read_excel(self.data_dir / "澳洲三大渠道.xlsx", sheet_name="allied")
|
||||
df.columns = ["post", "zone_1", "zone_2", "zone_3", "zone_4"]
|
||||
|
||||
data = []
|
||||
for _, row in df.iterrows():
|
||||
post = str(row["post"]) if pd.notna(row["post"]) else ""
|
||||
data.append((post,
|
||||
float(row["zone_1"]) if pd.notna(row["zone_1"]) else 0,
|
||||
float(row["zone_2"]) if pd.notna(row["zone_2"]) else 0,
|
||||
float(row["zone_3"]) if pd.notna(row["zone_3"]) else 0,
|
||||
float(row["zone_4"]) if pd.notna(row["zone_4"]) else 0))
|
||||
|
||||
sql = "INSERT INTO au_allied_price (post, zone_1, zone_2, zone_3, zone_4) VALUES (%s, %s, %s, %s, %s)"
|
||||
self.execute_many(sql, data)
|
||||
print(f" 已导入 {len(data)} 条记录 (allied)")
|
||||
except Exception as e:
|
||||
print(f" allied: {e}")
|
||||
|
||||
# ==================== 欧洲数据导入 ====================
|
||||
|
||||
def import_eur_dhl_price(self):
|
||||
"""导入欧洲DHL价格"""
|
||||
print("导入欧洲DHL价格...")
|
||||
|
||||
try:
|
||||
df = pd.read_excel(self.data_dir / "欧洲卡派.xlsx", sheet_name="DHL卡派IP报价")
|
||||
df.columns = ["type", "country", "postalcode", "country_postalcode",
|
||||
"ip_1", "ip_2", "ip_3", "ip_4", "ip_5", "ip_6"]
|
||||
|
||||
data = []
|
||||
for _, row in df.iterrows():
|
||||
price_type = str(row["type"]) if pd.notna(row["type"]) else ""
|
||||
country = str(row["country"]) if pd.notna(row["country"]) else ""
|
||||
postalcode = str(row["postalcode"]) if pd.notna(row["postalcode"]) else ""
|
||||
data.append((price_type, country, postalcode,
|
||||
float(row["ip_1"]) if pd.notna(row["ip_1"]) else 0,
|
||||
float(row["ip_2"]) if pd.notna(row["ip_2"]) else 0,
|
||||
float(row["ip_3"]) if pd.notna(row["ip_3"]) else 0,
|
||||
float(row["ip_4"]) if pd.notna(row["ip_4"]) else 0,
|
||||
float(row["ip_5"]) if pd.notna(row["ip_5"]) else 0,
|
||||
float(row["ip_6"]) if pd.notna(row["ip_6"]) else 0))
|
||||
|
||||
sql = """INSERT INTO eur_dhl_price
|
||||
(price_type, country, postalcode, ip_1, ip_2, ip_3, ip_4, ip_5, ip_6)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)"""
|
||||
self.execute_many(sql, data)
|
||||
print(f" 已导入 {len(data)} 条记录")
|
||||
except Exception as e:
|
||||
print(f" 欧洲DHL: {e}")
|
||||
|
||||
# ==================== 主函数 ====================
|
||||
|
||||
def import_all(self):
|
||||
"""导入所有数据"""
|
||||
print("开始导入数据...")
|
||||
|
||||
# 英国
|
||||
self.import_uk_postcode_zone()
|
||||
self.import_uk_kp_nv_price()
|
||||
|
||||
# 美国
|
||||
self.import_us_fedex_pp_price()
|
||||
self.import_us_fedex_kh_price()
|
||||
self.import_us_fedex_price()
|
||||
self.import_us_giga_price()
|
||||
self.import_us_ceva_price()
|
||||
self.import_us_metro_price()
|
||||
self.import_us_xmiles_zone()
|
||||
self.import_us_am_price()
|
||||
|
||||
# 澳洲
|
||||
self.import_au_eparcel_price()
|
||||
self.import_au_all()
|
||||
|
||||
# 欧洲
|
||||
self.import_eur_dhl_price()
|
||||
|
||||
print("\n数据导入完成!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
importer = DataImporter()
|
||||
importer.import_all()
|
||||
|
|
@ -1,37 +1,11 @@
|
|||
# 英国海运订单费用,返回单个sku的订单费用和订单类型
|
||||
import sys
|
||||
sys.path.append(r'D:\workspace\dags\logistics')
|
||||
import pandas as pd
|
||||
|
||||
import math
|
||||
from utils.Package import Package, Package_group
|
||||
import re
|
||||
def uk_ocean_order_price(packages_dict_str,k):
|
||||
def uk_ocean_order_price(packages,k):
|
||||
"""
|
||||
入参:packages的类型是Package_group,里面包含多个Package,这是一个类,这个类今天在群里发过
|
||||
入参:k是物流分摊费,也就是k
|
||||
|
||||
出参: 订单物流费,订单类型
|
||||
"""
|
||||
|
||||
packages = Package_group()
|
||||
def extract_number(value):
|
||||
# 提取字符串中的第一个数字
|
||||
match = re.search(r"[-+]?\d*\.\d+|\d+", str(value))
|
||||
return float(match.group()) if match else 0.0
|
||||
packages_dict = eval(packages_dict_str)
|
||||
if len(packages_dict) == 0:
|
||||
return (0,0)
|
||||
for key, package in packages_dict.items():
|
||||
package['长'] = extract_number(package['长'])
|
||||
package['宽'] = extract_number(package['宽'])
|
||||
package['高'] = extract_number(package['高'])
|
||||
package['重量'] = extract_number(package['重量'])
|
||||
|
||||
if package['长'] == 0 or package['宽'] == 0 or package['高'] == 0 or package['重量'] == 0:
|
||||
return (0,0)
|
||||
packages.add_package(Package(key,package['长'], package['宽'], package['高'], package['重量']))
|
||||
|
||||
# 计算uk经济直达费用
|
||||
order_fee = 0
|
||||
express_fee = 0
|
||||
|
|
@ -70,45 +44,4 @@ def uk_ocean_order_price(packages_dict_str,k):
|
|||
else:
|
||||
order_fee = ltl_fee
|
||||
order_type = order_type2
|
||||
return max(round(order_fee,2),2), order_type
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
sys.path.append(r'D:\workspace\dags\logistics')
|
||||
|
||||
sql = """SELECT
|
||||
t1.*,
|
||||
t2.`物流分摊`
|
||||
FROM
|
||||
`dim_erp_sku_package_vol_info`t1 left join ods.stg_bayshop_litfad_sku t2 on t1.erp_sku = t2.SKU
|
||||
where id >=%s AND id <=%s
|
||||
AND uk_price IS NULL
|
||||
AND `物流分摊` IS NOT NULL
|
||||
"""
|
||||
from utils.gtools import MySQLconnect
|
||||
from tqdm import tqdm
|
||||
import pandas as pd
|
||||
with MySQLconnect('dwd') as db:
|
||||
for i in tqdm(range(0,150)):
|
||||
# count=0
|
||||
print(i,"开始")
|
||||
dfsql = sql % (i*100000, (i+1)*100000-1)
|
||||
df = pd.read_sql(dfsql, db.engine())
|
||||
if len(df) == 0:
|
||||
continue
|
||||
df[['us_price','logitcs_type']] = df.apply(lambda x: uk_ocean_order_price(x['erp_package_vol'],x['物流分摊']), axis=1, result_type='expand')
|
||||
pd.io.sql.to_sql(df, "temp_update",db.eng, if_exists='replace', index=False )
|
||||
#添加主键ID
|
||||
modifysql = """ALTER TABLE `temp_update` ADD PRIMARY KEY (`id`)
|
||||
"""
|
||||
db.cur.execute(modifysql)
|
||||
|
||||
# se = [tuple([round(x['us_price'],2),x['id']]) for y,x in df.iterrows()]
|
||||
update_sql = """
|
||||
UPDATE dim_erp_sku_package_vol_info AS target
|
||||
JOIN temp_update AS src
|
||||
ON target.id = src.id -- 根据主键关联
|
||||
SET target.uk_price = src.us_price;"""
|
||||
# db.cur.executemany(update_sql, se)
|
||||
db.cur.execute(update_sql)
|
||||
db.con.commit()
|
||||
print(i,"结束")
|
||||
return max(round(order_fee,2),2), order_type
|
||||
|
|
@ -1,7 +1,4 @@
|
|||
import sys
|
||||
sys.path.append(r'D:\workspace\dags\logistics')
|
||||
import pandas as pd
|
||||
|
||||
import math
|
||||
from utils.gtools import MySQLconnect,DBconnect
|
||||
# 取计算订单物流费的参数
|
||||
|
|
@ -35,7 +32,6 @@ def ocean_order_price(packages):
|
|||
if package.fst_size>=238 or package.girth>=315:
|
||||
big_fee+=61.6
|
||||
express_type_length ="大包裹"
|
||||
express_fee = 9999999 if express_fee ==0 else express_fee
|
||||
express_fee = express_fee + long_fee + weight_fee + big_fee
|
||||
express_type = express_type_length + express_type_weight
|
||||
|
||||
|
|
@ -131,34 +127,4 @@ def air_order_price(packages):
|
|||
else:
|
||||
express_fee+=(((min(max(package.density,37),337)*0.093+27.7-1.08)/6+0.65-1.06)*package.get_volume_weight(8500))/0.45+price
|
||||
express_type='FEDEX'
|
||||
return express_fee, express_type
|
||||
|
||||
if __name__ == '__main__':
|
||||
sql = "SELECT * FROM `dim_erp_sku_package_vol_info` where id >=%s AND id <=%s AND logis_type IS NULL"
|
||||
from utils.gtools import MySQLconnect
|
||||
from tqdm import tqdm
|
||||
with MySQLconnect('dwd') as db:
|
||||
for i in tqdm(range(1,150)):
|
||||
# count=0
|
||||
print(i,"开始")
|
||||
dfsql = sql % (i*100000, (i+1)*100000-1)
|
||||
df = pd.read_sql(dfsql, db.engine())
|
||||
if len(df) == 0:
|
||||
continue
|
||||
df[['us_price','logitcs_type']] = df.apply(lambda x: ocean_order_price(x['erp_package_vol']), axis=1, result_type='expand')
|
||||
pd.io.sql.to_sql(df, "temp_update",db.eng, if_exists='replace', index=False )
|
||||
#添加主键ID
|
||||
modifysql = """ALTER TABLE `temp_update` ADD PRIMARY KEY (`id`)
|
||||
"""
|
||||
db.cur.execute(modifysql)
|
||||
|
||||
# se = [tuple([round(x['us_price'],2),x['id']]) for y,x in df.iterrows()]
|
||||
update_sql = """
|
||||
UPDATE dim_erp_sku_package_vol_info AS target
|
||||
JOIN temp_update AS src
|
||||
ON target.id = src.id -- 根据主键关联
|
||||
SET target.logis_type = src.logitcs_type;"""
|
||||
# db.cur.executemany(update_sql, se)
|
||||
db.cur.execute(update_sql)
|
||||
db.con.commit()
|
||||
print(i,"结束")
|
||||
return express_fee, express_type
|
||||
|
|
@ -1,517 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
美国售价模型 V2 (shopfad)
|
||||
通过输入多包裹体积和采购价格,快速计算售价
|
||||
规则版本: 2604
|
||||
"""
|
||||
import sys
|
||||
sys.path.append(r'D:\workspace\dags\logistics')
|
||||
|
||||
import math
|
||||
from utils.gtools import MySQLconnect
|
||||
import pandas as pd
|
||||
|
||||
|
||||
class USLogisticsPrice:
|
||||
"""美国物流价格查询"""
|
||||
|
||||
_instance = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance._init_data()
|
||||
return cls._instance
|
||||
|
||||
def _init_data(self):
|
||||
"""从数据库加载价格数据"""
|
||||
conn = MySQLconnect('logistics')
|
||||
with conn as c:
|
||||
self.sell_usps = pd.read_sql("SELECT * FROM sell_usps", c.con)
|
||||
# 2604版本价格表 (表名按需修改)
|
||||
self.sell_uandf_2604 = pd.read_sql("SELECT * FROM sell_uandf_2604", c.con)
|
||||
self.sell_fedex_price_ref_2604 = pd.read_sql("SELECT * FROM sell_fedex_price_ref_2604", c.con)
|
||||
self.sell_fedex_order_ref_2604 = pd.read_sql("SELECT * FROM sell_fedex_order_ref_2604", c.con)
|
||||
# 物流比例限制表 (表名按需修改)
|
||||
self.logistics_ratio_limit = pd.read_sql("SELECT * FROM logistics_ratio_limit", c.con)
|
||||
|
||||
# ========== 包裹计费类型判断 ==========
|
||||
|
||||
def get_package_types(self, packages):
|
||||
"""
|
||||
USPS:默认
|
||||
UandF:最长边>50 或 次长边>40 或 最短边>30 或 实重>2718 或 体积重>4500
|
||||
FEDEX:最长边>264 或 实重>67000 或 体积重>95000 或 围长>391
|
||||
卡派:同FEDEX条件(或该SKU的其他包裹为卡派类型)
|
||||
"""
|
||||
types = []
|
||||
for pkg in packages:
|
||||
l, w, h, wt = pkg['length_cm'], pkg['width_cm'], pkg['height_cm'], pkg['weight_g']
|
||||
sizes = sorted([l, w, h], reverse=True)
|
||||
fst, sed, trd = sizes[0], sizes[1], sizes[2]
|
||||
vol_weight = l * w * h / 6
|
||||
girth = fst + sed * 2 + trd * 2
|
||||
|
||||
pkg_type = 'USPS'
|
||||
if fst > 50 or sed > 40 or trd > 30 or wt > 2718 or vol_weight > 4500:
|
||||
pkg_type = 'UandF'
|
||||
if fst > 264 or wt > 67000 or vol_weight > 95000 or girth > 391:
|
||||
pkg_type = 'FEDEX'
|
||||
types.append(pkg_type)
|
||||
|
||||
# FEDEX升级为卡派(同SKU联动)
|
||||
if 'FEDEX' in types:
|
||||
types = ['卡派' if t == 'FEDEX' else t for t in types]
|
||||
|
||||
return types
|
||||
|
||||
# ========== 快递费计算 ==========
|
||||
|
||||
def _lookup_express_base_fee(self, pkg_type, lbs_weight, oz_weight, profit_rate, fuel_rate):
|
||||
"""
|
||||
查表获取单个包裹的快递基础计费
|
||||
USPS/UandF: 直接查表
|
||||
FEDEX: FEDEX售价参考表 * (1+快递燃油费率) + 2.9
|
||||
"""
|
||||
if pkg_type == 'USPS':
|
||||
row = self.sell_usps[self.sell_usps['oz'] == oz_weight]
|
||||
if len(row) > 0:
|
||||
return row['最终费用_v2'].iloc[0] / profit_rate, 'USPS'
|
||||
else:
|
||||
return self._lookup_express_base_fee('UandF', lbs_weight, oz_weight, profit_rate, fuel_rate)
|
||||
|
||||
if pkg_type == 'UandF':
|
||||
row = self.sell_uandf_2604[self.sell_uandf_2604['lbs'] == lbs_weight]
|
||||
if len(row) > 0:
|
||||
return row['加权价格_v2'].iloc[0] / profit_rate, 'UandF'
|
||||
else:
|
||||
return self._lookup_express_base_fee('FEDEX', lbs_weight, oz_weight, profit_rate, fuel_rate)
|
||||
|
||||
if pkg_type in ('FEDEX', '卡派'):
|
||||
row = self.sell_fedex_price_ref_2604[self.sell_fedex_price_ref_2604['lbs'] == lbs_weight]
|
||||
if len(row) > 0:
|
||||
return row['售价尾端价格_v4'].iloc[0] * (1 + fuel_rate) + 2.9, 'FEDEX'
|
||||
else:
|
||||
return 99999, 'FEDEX'
|
||||
|
||||
return 0, pkg_type
|
||||
|
||||
def _calc_express_fee(self, packages, types, profit_rate, fuel_rate):
|
||||
"""
|
||||
快递费 = Σ(单包裹快递基础计费 + 大包裹费 + 超重费 + 超尺寸费)
|
||||
大包裹费: 最长边>238 或 围长>315 → 109.4
|
||||
超重费: 实重>21000 → 6.1
|
||||
超尺寸费: 最长边>116 或 围长>251 → 7.4
|
||||
"""
|
||||
total_fee = 0
|
||||
for i, pkg in enumerate(packages):
|
||||
l, w, h, wt = pkg['length_cm'], pkg['width_cm'], pkg['height_cm'], pkg['weight_g']
|
||||
sizes = sorted([l, w, h], reverse=True)
|
||||
fst, sed, trd = sizes[0], sizes[1], sizes[2]
|
||||
volume = l * w * h
|
||||
girth = fst + sed * 2 + trd * 2
|
||||
|
||||
lbs_weight = math.ceil(max(volume / 8500 / 0.453, wt / 453))
|
||||
oz_weight = math.ceil(wt / 28)
|
||||
|
||||
pkg_type = types[i]
|
||||
base_fee, actual_type = self._lookup_express_base_fee(
|
||||
pkg_type, lbs_weight, oz_weight, profit_rate, fuel_rate
|
||||
)
|
||||
|
||||
large_pkg_fee = 0
|
||||
overweight_fee = 0
|
||||
oversize_fee = 0
|
||||
|
||||
if actual_type in ('FEDEX', '卡派'):
|
||||
if fst > 238 or girth > 315:
|
||||
large_pkg_fee = 109.4
|
||||
if wt > 21000:
|
||||
overweight_fee = 6.1
|
||||
if fst > 116 or girth > 251:
|
||||
oversize_fee = 7.4
|
||||
|
||||
total_fee += base_fee + large_pkg_fee + overweight_fee + oversize_fee
|
||||
|
||||
return total_fee
|
||||
|
||||
# ========== 卡派费计算 ==========
|
||||
|
||||
def _calc_ltl_fee(self, packages, profit_rate):
|
||||
"""
|
||||
卡派费 = 卡派基础费 + 超长费 + 超重费
|
||||
合并体积 = ceil(长x宽x高/1000000 x 35.3) (立方英尺)
|
||||
"""
|
||||
total_cubic_feet = 0
|
||||
over_length_fee = 0
|
||||
over_weight_fee = 0
|
||||
|
||||
for pkg in packages:
|
||||
l, w, h, wt = pkg['length_cm'], pkg['width_cm'], pkg['height_cm'], pkg['weight_g']
|
||||
sizes = sorted([l, w, h], reverse=True)
|
||||
fst = sizes[0]
|
||||
volume = l * w * h
|
||||
|
||||
cubic_feet = math.ceil(volume / 1000000 * 35.3147)
|
||||
total_cubic_feet += cubic_feet
|
||||
|
||||
# 超长费: 最长边>250
|
||||
if fst > 250:
|
||||
over_length_fee = max(over_length_fee, round(130 / profit_rate - 118, 1))
|
||||
# 超重费
|
||||
if 130000 <= wt < 157000:
|
||||
over_weight_fee = max(over_weight_fee, round(130 / profit_rate - 118, 1))
|
||||
elif 111000 <= wt < 130000:
|
||||
over_weight_fee = max(over_weight_fee, round(80 / profit_rate - 78, 1))
|
||||
|
||||
# 卡派基础费
|
||||
v = total_cubic_feet
|
||||
if v < 25:
|
||||
base_fee = round(198 / profit_rate / 2, 2)
|
||||
elif v < 35:
|
||||
base_fee = round(231 / profit_rate / 2, 2)
|
||||
else:
|
||||
base_fee = round(max(231, 6.6 * v) / profit_rate / 2, 2)
|
||||
|
||||
ltl_fee = base_fee + over_length_fee + over_weight_fee
|
||||
return ltl_fee, total_cubic_feet
|
||||
|
||||
# ========== 初始前端展示物流费 ==========
|
||||
|
||||
def _calc_display_logistics_fee(self, packages, types, total_cubic_feet):
|
||||
"""
|
||||
卡派: v<25→90, v<35→90+(v-25)*3, 其他→99+(v-35)*5
|
||||
快递: FEDEX订单参考表查表
|
||||
"""
|
||||
has_ltl = '卡派' in types
|
||||
|
||||
if has_ltl:
|
||||
v = total_cubic_feet
|
||||
if v < 25:
|
||||
display = 90
|
||||
elif v < 35:
|
||||
display = 90 + (v - 25) * 3
|
||||
else:
|
||||
display = 99 + (v - 35) * 5
|
||||
print(f"[卡派展示物流费] 合并体积={v} cu ft, 展示费={display}")
|
||||
return display
|
||||
else:
|
||||
# 快递: FEDEX订单参考表查表
|
||||
total = 0
|
||||
for pkg in packages:
|
||||
l, w, h, wt = pkg['length_cm'], pkg['width_cm'], pkg['height_cm'], pkg['weight_g']
|
||||
volume = l * w * h
|
||||
lbs_weight = math.ceil(max(volume / 8500 / 0.453, wt / 453))
|
||||
row = self.sell_fedex_order_ref_2604[
|
||||
self.sell_fedex_order_ref_2604['lbs'] == lbs_weight
|
||||
]
|
||||
if len(row) > 0:
|
||||
total += row['售价尾端价格_v3'].iloc[0]
|
||||
return total
|
||||
|
||||
# ========== 物流比例限制查表 ==========
|
||||
|
||||
def _lookup_logistics_limit(self, category, price):
|
||||
"""通过条目和价格查表得到建议物流比例上限和建议物流绝对上限"""
|
||||
try:
|
||||
rows = self.logistics_ratio_limit[
|
||||
(self.logistics_ratio_limit['条目'] == category) &
|
||||
(self.logistics_ratio_limit['价格'] <= price)
|
||||
]
|
||||
if len(rows) > 0:
|
||||
row = rows.iloc[-1]
|
||||
return row['建议物流比例上限'], row['建议物流绝对上限']
|
||||
return 1.0, float('inf')
|
||||
except:
|
||||
return 1.0, float('inf')
|
||||
|
||||
# ========== 统一尾端费用接口 ==========
|
||||
|
||||
def get_tail_fee_info(self, packages, profit_rate=0.359, fuel_rate=0):
|
||||
"""
|
||||
尾端费用 = min(快递费, 卡派费)
|
||||
存在卡派类型包裹则强制使用卡派
|
||||
"""
|
||||
types = self.get_package_types(packages)
|
||||
has_ltl = '卡派' in types
|
||||
|
||||
express_fee = self._calc_express_fee(packages, types, profit_rate, fuel_rate)
|
||||
ltl_fee, total_cubic_feet = self._calc_ltl_fee(packages, profit_rate)
|
||||
|
||||
if has_ltl:
|
||||
tail_fee = ltl_fee
|
||||
else:
|
||||
tail_fee = min(express_fee, ltl_fee)
|
||||
|
||||
display_fee = self._calc_display_logistics_fee(packages, types, total_cubic_feet)
|
||||
|
||||
return {
|
||||
'tail_fee': tail_fee,
|
||||
'display_logistics_fee': display_fee,
|
||||
'express_fee': express_fee,
|
||||
'ltl_fee': ltl_fee,
|
||||
'types': types,
|
||||
'has_ltl': has_ltl,
|
||||
'total_cubic_feet': total_cubic_feet
|
||||
}
|
||||
|
||||
|
||||
class USSellPrice:
|
||||
"""
|
||||
美国售价计算器 V2 (shopfad)
|
||||
规则版本: 2604
|
||||
|
||||
使用示例:
|
||||
calculator = USSellPrice()
|
||||
result = calculator.calculate(
|
||||
packages=[{'length_cm': 30, 'width_cm': 20, 'height_cm': 10, 'weight_g': 500}],
|
||||
purchase_price_cny=100,
|
||||
preset='shopfad'
|
||||
)
|
||||
"""
|
||||
|
||||
PRESETS = {
|
||||
'shopfad': {
|
||||
'ocean_first_cny': 0.5,
|
||||
'ocean_first_usd': 0.7,
|
||||
'air_first_usd': 0.65,
|
||||
'air_cny_type': 0.093,
|
||||
'air_first_fix': 22.7,
|
||||
'exchange_rate': 6.5,
|
||||
'profit_rate': 0.38,
|
||||
'air_rate': 0.7,
|
||||
'tax_rate': 0.1,
|
||||
'fuel_rate': 0,
|
||||
'extra_rate': 0
|
||||
},
|
||||
'shopfad2': {
|
||||
'ocean_first_cny': 1,
|
||||
'ocean_first_usd': 1,
|
||||
'air_first_usd': 0.65,
|
||||
'air_cny_type': 0.093,
|
||||
'air_first_fix': 22.7,
|
||||
'exchange_rate': 6.5,
|
||||
'profit_rate': 0.38,
|
||||
'air_rate': 0.7,
|
||||
'tax_rate': 0.145,
|
||||
'fuel_rate': 0,
|
||||
'extra_rate': 0
|
||||
},
|
||||
'shopfad3': {
|
||||
'ocean_first_cny': 0.67,
|
||||
'ocean_first_usd': 0.7,
|
||||
'air_first_usd': 0.65,
|
||||
'air_cny_type': 0.093,
|
||||
'air_first_fix': 22.7,
|
||||
'exchange_rate': 6.5,
|
||||
'profit_rate': 0.3,
|
||||
'air_rate': 0.7,
|
||||
'tax_rate': 0.1,
|
||||
'fuel_rate': 0,
|
||||
'extra_rate': 0
|
||||
},
|
||||
'shopfad4': {
|
||||
'ocean_first_cny': 0.67,
|
||||
'ocean_first_usd': 0.7,
|
||||
'air_first_usd': 0.65,
|
||||
'air_cny_type': 0.093,
|
||||
'air_first_fix': 22.7,
|
||||
'exchange_rate': 6.5,
|
||||
'profit_rate': 0.3,
|
||||
'air_rate': 0.7,
|
||||
'tax_rate': 0.1,
|
||||
'fuel_rate': 0,
|
||||
'extra_rate': 0
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.logistics = USLogisticsPrice()
|
||||
|
||||
def calculate(self, packages, purchase_price_cny, preset='shopfad', config=None, category=''):
|
||||
"""
|
||||
计算售价
|
||||
|
||||
Args:
|
||||
packages: 包裹列表 [{'length_cm', 'width_cm', 'height_cm', 'weight_g'}]
|
||||
purchase_price_cny: 采购价(CNY)
|
||||
preset: 预设配置名称
|
||||
config: 自定义配置 (优先使用)
|
||||
category: 条目 (用于物流比例限制查表)
|
||||
"""
|
||||
cfg = config or self.PRESETS.get(preset, self.PRESETS['shopfad'])
|
||||
|
||||
# 1. 获取尾端费用
|
||||
fuel_rate = cfg.get('fuel_rate', 0)
|
||||
tail_info = self.logistics.get_tail_fee_info(packages, cfg['profit_rate'], fuel_rate)
|
||||
tail_fee = tail_info['tail_fee']
|
||||
display_fee = tail_info['display_logistics_fee']
|
||||
|
||||
# 2. 判断SPU运输方式
|
||||
has_ltl = tail_info['has_ltl']
|
||||
shipping_type = '海运' if has_ltl else '空运'
|
||||
|
||||
# 3. 汇总体积重 (长x宽x高/6)
|
||||
total_volume_weight = sum(
|
||||
p['length_cm'] * p['width_cm'] * p['height_cm'] / 6 for p in packages
|
||||
)
|
||||
|
||||
tax_amount = purchase_price_cny * cfg['tax_rate'] / cfg['exchange_rate']
|
||||
|
||||
# 4. 根据运输方式计算调整前售价
|
||||
if shipping_type == '海运':
|
||||
pre_adjust_price = self._calc_ocean_price(
|
||||
total_volume_weight, purchase_price_cny, tax_amount, tail_fee, cfg
|
||||
)
|
||||
first_cny = cfg['ocean_first_cny'] / 1000 * total_volume_weight
|
||||
first_usd = cfg['ocean_first_usd'] / 1000 * total_volume_weight
|
||||
else:
|
||||
pre_adjust_price = self._calc_air_price(
|
||||
packages, purchase_price_cny, tax_amount, tail_fee, cfg
|
||||
)
|
||||
first_cny, first_usd = self._calc_air_first_leg(packages, cfg)
|
||||
|
||||
# 5. 最终修正 (仅海运)
|
||||
shared_tail_fee = 0
|
||||
final_price = pre_adjust_price
|
||||
|
||||
if shipping_type == '海运':
|
||||
final_price, shared_tail_fee = self._apply_ocean_adjustment(
|
||||
pre_adjust_price, display_fee, category
|
||||
)
|
||||
|
||||
# 头程费用 / 总物流费用
|
||||
head_fee = first_cny / cfg['exchange_rate'] + first_usd
|
||||
total_logistics_fee = head_fee + tail_fee
|
||||
|
||||
return {
|
||||
'sell_price_usd': round(final_price, 2),
|
||||
'sell_price_cny': round(final_price * cfg['exchange_rate'], 2),
|
||||
'purchase_price_cny': purchase_price_cny,
|
||||
'express_fee': round(tail_fee, 2),
|
||||
'first_cny': round(first_cny, 2),
|
||||
'first_usd': round(first_usd, 2),
|
||||
'tax_amount': round(tax_amount, 2),
|
||||
'shipping_type': shipping_type,
|
||||
'preset': preset,
|
||||
'pre_adjust_price': round(pre_adjust_price, 2),
|
||||
'display_logistics_fee': round(display_fee, 2),
|
||||
'shared_tail_fee': round(shared_tail_fee, 2),
|
||||
'head_fee': round(head_fee, 2),
|
||||
'total_logistics_fee': round(total_logistics_fee, 2),
|
||||
'package_types': tail_info['types'],
|
||||
}
|
||||
|
||||
# ========== 海运计费 ==========
|
||||
|
||||
def _calc_ocean_price(self, total_volume_weight, purchase_price_cny, tax_amount, tail_fee, cfg):
|
||||
"""
|
||||
调整前售价 = (((海运头程CNY总价 + 采购价格) / 汇率 + 海运头程USD总价)
|
||||
+ 采购价格 x 税金系数 / 汇率) / 利润系数 + 尾端费用
|
||||
"""
|
||||
ocean_first_cny_total = cfg['ocean_first_cny'] / 1000 * total_volume_weight
|
||||
ocean_first_usd_total = cfg['ocean_first_usd'] / 1000 * total_volume_weight
|
||||
|
||||
pre_adjust = (
|
||||
((ocean_first_cny_total + purchase_price_cny) / cfg['exchange_rate']
|
||||
+ ocean_first_usd_total + tax_amount) / cfg['profit_rate'] + tail_fee
|
||||
)
|
||||
return pre_adjust
|
||||
|
||||
# ========== 空运计费 ==========
|
||||
|
||||
def _calc_air_price(self, packages, purchase_price_cny, tax_amount, tail_fee, cfg):
|
||||
"""
|
||||
调整前售价 = (采购价格/汇率/利润系数
|
||||
+ ((空运头程CNY总价/汇率 + 空运头程USD总价 + 税金) / 利润系数 + 尾端费用)
|
||||
x 空运分配比例) x (1 + 附加费率)
|
||||
"""
|
||||
air_cny_total = 0
|
||||
air_usd_total = 0
|
||||
|
||||
for pkg in packages:
|
||||
volume = pkg['length_cm'] * pkg['width_cm'] * pkg['height_cm']
|
||||
volume_weight = volume / 6
|
||||
density = pkg['weight_g'] / (volume / 1000) if volume > 0 else 0
|
||||
type_weight = min(337, max(37, math.ceil(density)))
|
||||
|
||||
air_cny_unit = cfg['air_cny_type'] / 1000 * type_weight + cfg['air_first_fix'] / 1000
|
||||
air_cny_total += air_cny_unit * volume_weight
|
||||
air_usd_total += cfg['air_first_usd'] / 1000 * volume_weight
|
||||
|
||||
purchase_usd = purchase_price_cny / cfg['exchange_rate']
|
||||
|
||||
pre_adjust = (
|
||||
purchase_usd / cfg['profit_rate']
|
||||
+ ((air_cny_total / cfg['exchange_rate'] + air_usd_total + tax_amount)
|
||||
/ cfg['profit_rate'] + tail_fee) * cfg['air_rate']
|
||||
) * (1 + cfg.get('extra_rate', 0))
|
||||
|
||||
return pre_adjust
|
||||
|
||||
def _calc_air_first_leg(self, packages, cfg):
|
||||
"""计算空运头程费用 (CNY/USD)"""
|
||||
first_cny = 0
|
||||
first_usd = 0
|
||||
for pkg in packages:
|
||||
volume = pkg['length_cm'] * pkg['width_cm'] * pkg['height_cm']
|
||||
volume_weight = volume / 6
|
||||
density = pkg['weight_g'] / (volume / 1000) if volume > 0 else 0
|
||||
type_weight = min(337, max(37, math.ceil(density)))
|
||||
|
||||
air_cny_unit = cfg['air_cny_type'] / 1000 * type_weight + cfg['air_first_fix'] / 1000
|
||||
first_cny += air_cny_unit * volume_weight
|
||||
first_usd += cfg['air_first_usd'] / 1000 * volume_weight
|
||||
return first_cny, first_usd
|
||||
|
||||
# ========== 最终修正 (海运) ==========
|
||||
|
||||
def _apply_ocean_adjustment(self, pre_adjust_price, display_fee, category):
|
||||
"""
|
||||
前端展示物流费 = min(初始前端展示物流费, 调整前售价×比例上限, 绝对上限)
|
||||
if min == 初始前端展示物流费: 分摊尾端=0, 最终售价=调整前售价
|
||||
else: 分摊尾端=初始前端展示物流费-min, 最终售价=调整前售价+分摊尾端
|
||||
"""
|
||||
total = display_fee + pre_adjust_price
|
||||
if total <= 0:
|
||||
return pre_adjust_price, 0
|
||||
|
||||
ratio_limit, abs_limit = self.logistics._lookup_logistics_limit(category, pre_adjust_price)
|
||||
actual_display_fee = min(display_fee, pre_adjust_price * ratio_limit, abs_limit)
|
||||
|
||||
if actual_display_fee == display_fee:
|
||||
return pre_adjust_price, 0
|
||||
else:
|
||||
shared_tail = display_fee - actual_display_fee
|
||||
final_price = pre_adjust_price + shared_tail
|
||||
return final_price, shared_tail
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
calc = USSellPrice()
|
||||
|
||||
print("=== 单包裹测试 ===")
|
||||
result = calc.calculate(
|
||||
packages=[{'length_cm': 30, 'width_cm': 20, 'height_cm': 10, 'weight_g': 500}],
|
||||
purchase_price_cny=100,
|
||||
preset='shopfad'
|
||||
)
|
||||
print(f" 30x20x10cm 500g => ${result['sell_price_usd']}")
|
||||
print(f" 运输方式: {result['shipping_type']}")
|
||||
print(f" 包裹类型: {result['package_types']}")
|
||||
|
||||
print("\n=== 多包裹测试 ===")
|
||||
result = calc.calculate(
|
||||
packages=[
|
||||
{'length_cm': 30, 'width_cm': 20, 'height_cm': 10, 'weight_g': 500},
|
||||
{'length_cm': 40, 'width_cm': 30, 'height_cm': 15, 'weight_g': 1000},
|
||||
],
|
||||
purchase_price_cny=200,
|
||||
preset='shopfad'
|
||||
)
|
||||
print(f" 两件包裹 => ${result['sell_price_usd']}")
|
||||
print(f" 运输方式: {result['shipping_type']}")
|
||||
print(f" 包裹类型: {result['package_types']}")
|
||||
print(f" 尾端费用: ${result['express_fee']}")
|
||||
print(f" 头程CNY: {result['first_cny']}, 头程USD: ${result['first_usd']}")
|
||||
print(f" 总物流: ${result['total_logistics_fee']}")
|
||||
|
|
@ -1,337 +0,0 @@
|
|||
-- 物流费用计算系统数据库表结构
|
||||
-- 创建数据库: logistics
|
||||
|
||||
-- ============================================
|
||||
-- 物流公司配置表
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS logistics_company (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
company_code VARCHAR(50) NOT NULL COMMENT '公司代码',
|
||||
company_name VARCHAR(100) NOT NULL COMMENT '公司名称',
|
||||
country VARCHAR(10) NOT NULL COMMENT '国家代码: US, UK, AU, DE等',
|
||||
logistics_type VARCHAR(20) NOT NULL COMMENT '物流类型: EXPRESS, COURIER, OCEAN, AIR',
|
||||
port VARCHAR(20) DEFAULT 'DEFAULT' COMMENT '港口: DEFAULT, WEST, EAST等',
|
||||
currency VARCHAR(10) DEFAULT 'USD' COMMENT '货币',
|
||||
active TINYINT DEFAULT 1 COMMENT '是否启用: 0-禁用, 1-启用',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_company_code (company_code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物流公司配置表';
|
||||
|
||||
-- ============================================
|
||||
-- 英国物流价格表
|
||||
-- ============================================
|
||||
|
||||
-- 英国卡派-分区表
|
||||
CREATE TABLE IF NOT EXISTS uk_postcode_zone (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
postcode_prefix VARCHAR(10) NOT NULL COMMENT '邮编前缀',
|
||||
zone VARCHAR(10) NOT NULL COMMENT '分区',
|
||||
is_remote TINYINT DEFAULT 0 COMMENT '是否偏远: 0-否, 1-是',
|
||||
UNIQUE KEY uk_postcode (postcode_prefix)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='英国邮编分区表';
|
||||
|
||||
-- 英国卡派-运费表
|
||||
CREATE TABLE IF NOT EXISTS uk_kp_nv_price (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
zone VARCHAR(10) NOT NULL COMMENT '分区',
|
||||
tuopan INT NOT NULL COMMENT '托盘数',
|
||||
fee DECIMAL(10,2) NOT NULL COMMENT '运费',
|
||||
INDEX idx_zone (zone)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='英国卡派NV运费表';
|
||||
|
||||
-- ============================================
|
||||
-- 美国物流价格表
|
||||
-- ============================================
|
||||
|
||||
-- 美国邮编分区表
|
||||
CREATE TABLE IF NOT EXISTS us_postcode_zone (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
postcode VARCHAR(5) NOT NULL COMMENT '邮编(5位)',
|
||||
port VARCHAR(20) DEFAULT 'west' COMMENT '港口: west, east',
|
||||
zone VARCHAR(10) COMMENT '分区',
|
||||
remote_type INT DEFAULT 0 COMMENT '偏远类型: 0-非偏远, 1-偏远, 2-超偏远, 3-超超偏远',
|
||||
INDEX idx_postcode (postcode)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='美国邮编分区表';
|
||||
|
||||
-- 美国快递-邮差小马 价格表
|
||||
CREATE TABLE IF NOT EXISTS us_fedex_pp_price (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
lbs INT NOT NULL COMMENT '重量(磅)',
|
||||
zone_2 DECIMAL(10,2) COMMENT '2区价格',
|
||||
zone_3 DECIMAL(10,2) COMMENT '3区价格',
|
||||
zone_4 DECIMAL(10,2) COMMENT '4区价格',
|
||||
zone_5 DECIMAL(10,2) COMMENT '5区价格',
|
||||
zone_6 DECIMAL(10,2) COMMENT '6区价格',
|
||||
zone_7 DECIMAL(10,2) COMMENT '7区价格',
|
||||
zone_8 DECIMAL(10,2) COMMENT '8区价格',
|
||||
INDEX idx_lbs (lbs)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='美国Fedex邮差小马价格表';
|
||||
|
||||
-- 美国快递-金宏亚 价格表
|
||||
CREATE TABLE IF NOT EXISTS us_fedex_kh_price (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
lbs INT NOT NULL COMMENT '重量(磅)',
|
||||
zone_2 DECIMAL(10,2) COMMENT '2区价格',
|
||||
zone_3 DECIMAL(10,2) COMMENT '3区价格',
|
||||
zone_4 DECIMAL(10,2) COMMENT '4区价格',
|
||||
zone_5 DECIMAL(10,2) COMMENT '5区价格',
|
||||
zone_6 DECIMAL(10,2) COMMENT '6区价格',
|
||||
zone_7 DECIMAL(10,2) COMMENT '7区价格',
|
||||
zone_8 DECIMAL(10,2) COMMENT '8区价格',
|
||||
INDEX idx_lbs (lbs)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='美国Fedex金宏亚价格表';
|
||||
|
||||
-- 美国快递-FEDEX 价格表
|
||||
CREATE TABLE IF NOT EXISTS us_fedex_price (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
lbs INT NOT NULL COMMENT '重量(磅)',
|
||||
zone_2 DECIMAL(10,2) COMMENT '2区价格',
|
||||
zone_3 DECIMAL(10,2) COMMENT '3区价格',
|
||||
zone_4 DECIMAL(10,2) COMMENT '4区价格',
|
||||
zone_5 DECIMAL(10,2) COMMENT '5区价格',
|
||||
zone_6 DECIMAL(10,2) COMMENT '6区价格',
|
||||
zone_7 DECIMAL(10,2) COMMENT '7区价格',
|
||||
zone_8 DECIMAL(10,2) COMMENT '8区价格',
|
||||
INDEX idx_lbs (lbs)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='美国FEDEX价格表';
|
||||
|
||||
-- 美国卡派-GIGA 价格表
|
||||
CREATE TABLE IF NOT EXISTS us_giga_price (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
zip_code VARCHAR(5) NOT NULL COMMENT '邮编',
|
||||
delivery_warehouse VARCHAR(50) COMMENT '仓库',
|
||||
general_area VARCHAR(50) COMMENT '地区',
|
||||
fee_type VARCHAR(20) COMMENT '费用类型',
|
||||
zone VARCHAR(10) COMMENT '分区',
|
||||
local_pickup_fee DECIMAL(10,2) COMMENT '本地取货费',
|
||||
warehouse_handling_fee DECIMAL(10,2) COMMENT '仓库操作费',
|
||||
delivery_fee_rate DECIMAL(10,2) COMMENT '配送费率',
|
||||
additional_delivery_fee DECIMAL(10,2) COMMENT '额外配送费',
|
||||
assembly_fee DECIMAL(10,2) COMMENT '装配费',
|
||||
INDEX idx_zip (zip_code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='美国GIGA价格表';
|
||||
|
||||
-- 美国卡派-CEVA 价格表
|
||||
CREATE TABLE IF NOT EXISTS us_ceva_price (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
ceva_weight INT NOT NULL COMMENT 'CEVA重量',
|
||||
zone_ca DECIMAL(10,2) COMMENT 'CA区价格',
|
||||
zone_wa DECIMAL(10,2) COMMENT 'WA区价格',
|
||||
zone_or DECIMAL(10,2) COMMENT 'OR区价格',
|
||||
zone_nv DECIMAL(10,2) COMMENT 'NV区价格',
|
||||
zone_az DECIMAL(10,2) COMMENT 'AZ区价格',
|
||||
zone_co DECIMAL(10,2) COMMENT 'CO区价格',
|
||||
zone_ut DECIMAL(10,2) COMMENT 'UT区价格',
|
||||
zone_nm DECIMAL(10,2) COMMENT 'NM区价格',
|
||||
remote_area_surcharge DECIMAL(10,2) COMMENT '偏远地区附加费',
|
||||
INDEX idx_weight (ceva_weight)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='美国CEVA价格表';
|
||||
|
||||
-- 美国卡派-CEVA 邮编分区表
|
||||
CREATE TABLE IF NOT EXISTS us_ceva_zone (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
postal_code VARCHAR(5) NOT NULL COMMENT '邮编',
|
||||
state VARCHAR(20) COMMENT '州',
|
||||
beyond_zone VARCHAR(10) COMMENT '超出分区',
|
||||
remote_type VARCHAR(20) DEFAULT 'standard' COMMENT '偏远类型: standard, remote',
|
||||
INDEX idx_postal (postal_code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='美国CEVA邮编分区表';
|
||||
|
||||
-- 美国卡派-CEVA 分区表
|
||||
CREATE TABLE IF NOT EXISTS us_ceva_zone_grade (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
to_state VARCHAR(20) NOT NULL COMMENT '目的地州',
|
||||
ca DECIMAL(10,2) COMMENT 'CA分区',
|
||||
wa DECIMAL(10,2) COMMENT 'WA分区',
|
||||
INDEX idx_state (to_state)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='美国CEVA分区等级表';
|
||||
|
||||
-- 美国卡派-Metro 价格表
|
||||
CREATE TABLE IF NOT EXISTS us_metro_price (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
origins VARCHAR(20) NOT NULL COMMENT '出发地',
|
||||
zone_1l DECIMAL(10,2) COMMENT 'Zone 1L价格',
|
||||
zone_2l DECIMAL(10,2) COMMENT 'Zone 2L价格',
|
||||
zone_3l DECIMAL(10,2) COMMENT 'Zone 3L价格',
|
||||
zone_4l DECIMAL(10,2) COMMENT 'Zone 4L价格',
|
||||
zone_5l DECIMAL(10,2) COMMENT 'Zone 5L价格',
|
||||
zone_6l DECIMAL(10,2) COMMENT 'Zone 6L价格',
|
||||
zone_7l DECIMAL(10,2) COMMENT 'Zone 7L价格',
|
||||
zone_8l DECIMAL(10,2) COMMENT 'Zone 8L价格',
|
||||
zone_9l DECIMAL(10,2) COMMENT 'Zone 9L价格',
|
||||
INDEX idx_origins (origins)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='美国Metro价格表';
|
||||
|
||||
-- 美国卡派-Metro 邮编分区表
|
||||
CREATE TABLE IF NOT EXISTS us_metro_zone (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
zip_code VARCHAR(5) NOT NULL COMMENT '邮编',
|
||||
new_zone_name VARCHAR(20) COMMENT '新分区名',
|
||||
INDEX idx_zip (zip_code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='美国Metro邮编分区表';
|
||||
|
||||
-- 美国卡派-Metro 偏远表
|
||||
CREATE TABLE IF NOT EXISTS us_metro_remote (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
zip_code VARCHAR(5) NOT NULL COMMENT '邮编',
|
||||
area_type VARCHAR(50) COMMENT '区域类型',
|
||||
INDEX idx_zip (zip_code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='美国Metro偏远表';
|
||||
|
||||
-- 美国卡派-XMILES 邮编表
|
||||
CREATE TABLE IF NOT EXISTS us_xmiles_zone (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
postcode VARCHAR(5) NOT NULL COMMENT '邮编',
|
||||
area VARCHAR(20) COMMENT '地区',
|
||||
INDEX idx_postcode (postcode)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='美国XMILES邮编分区表';
|
||||
|
||||
-- 美国卡派-AM 价格表
|
||||
CREATE TABLE IF NOT EXISTS us_am_price (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
pu_zone VARCHAR(5) NOT NULL COMMENT 'PU区',
|
||||
dl_zone VARCHAR(5) NOT NULL COMMENT 'DL区',
|
||||
zone_combo VARCHAR(10) NOT NULL COMMENT '分区组合',
|
||||
minimum_weight DECIMAL(10,2) NOT NULL COMMENT '最小重量',
|
||||
maximum_weight DECIMAL(10,2) NOT NULL COMMENT '最大重量',
|
||||
fee_without_sc DECIMAL(10,2) COMMENT '无附加费价格',
|
||||
shipping_cost DECIMAL(10,2) COMMENT '运费',
|
||||
surcharge DECIMAL(10,2) DEFAULT 0 COMMENT '附加费',
|
||||
INDEX idx_zone_combo (zone_combo)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='美国AM卡派价格表';
|
||||
|
||||
-- 美国卡派-AM 邮编表
|
||||
CREATE TABLE IF NOT EXISTS us_am_postcode (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
zip_code VARCHAR(5) NOT NULL COMMENT '邮编',
|
||||
zone VARCHAR(5) NOT NULL COMMENT '分区',
|
||||
INDEX idx_zip (zip_code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='美国AM卡派邮编表';
|
||||
|
||||
-- ============================================
|
||||
-- 澳洲物流价格表
|
||||
-- ============================================
|
||||
|
||||
-- 澳洲eparcel价格表
|
||||
CREATE TABLE IF NOT EXISTS au_eparcel_price (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
post VARCHAR(20) NOT NULL COMMENT '邮寄方式',
|
||||
weight_0_5 DECIMAL(10,2) COMMENT '0.5kg价格',
|
||||
weight_1 DECIMAL(10,2) COMMENT '1kg价格',
|
||||
weight_2 DECIMAL(10,2) COMMENT '2kg价格',
|
||||
weight_3 DECIMAL(10,2) COMMENT '3kg价格',
|
||||
weight_4 DECIMAL(10,2) COMMENT '4kg价格',
|
||||
weight_5 DECIMAL(10,2) COMMENT '5kg价格',
|
||||
weight_7 DECIMAL(10,2) COMMENT '7kg价格',
|
||||
weight_10 DECIMAL(10,2) COMMENT '10kg价格',
|
||||
weight_15 DECIMAL(10,2) COMMENT '15kg价格',
|
||||
INDEX idx_post (post)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='澳洲eparcel价格表';
|
||||
|
||||
-- 澳洲eparcel邮编表
|
||||
CREATE TABLE IF NOT EXISTS au_eparcel_zone (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
postcode VARCHAR(10) NOT NULL COMMENT '邮编',
|
||||
zone VARCHAR(10) COMMENT '分区',
|
||||
INDEX idx_postcode (postcode)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='澳洲eparcel邮编表';
|
||||
|
||||
-- 澳洲toll价格表
|
||||
CREATE TABLE IF NOT EXISTS au_toll_price (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
post VARCHAR(20) NOT NULL COMMENT '邮寄方式',
|
||||
zone_1 DECIMAL(10,2) COMMENT '1区价格',
|
||||
zone_2 DECIMAL(10,2) COMMENT '2区价格',
|
||||
zone_3 DECIMAL(10,2) COMMENT '3区价格',
|
||||
zone_4 DECIMAL(10,2) COMMENT '4区价格',
|
||||
INDEX idx_post (post)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='澳洲toll价格表';
|
||||
|
||||
-- 澳洲toll邮编表
|
||||
CREATE TABLE IF NOT EXISTS au_toll_zone (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
postcode VARCHAR(10) NOT NULL COMMENT '邮编',
|
||||
zone VARCHAR(10) COMMENT '分区',
|
||||
INDEX idx_postcode (postcode)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='澳洲toll邮编表';
|
||||
|
||||
-- 澳洲toll偏远表
|
||||
CREATE TABLE IF NOT EXISTS au_toll_remote (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
postcode VARCHAR(10) NOT NULL COMMENT '邮编',
|
||||
remote_type VARCHAR(20) COMMENT '偏远类型',
|
||||
INDEX idx_postcode (postcode)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='澳洲toll偏远表';
|
||||
|
||||
-- 澳洲allied价格表
|
||||
CREATE TABLE IF NOT EXISTS au_allied_price (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
post VARCHAR(20) NOT NULL COMMENT '邮寄方式',
|
||||
zone_1 DECIMAL(10,2) COMMENT '1区价格',
|
||||
zone_2 DECIMAL(10,2) COMMENT '2区价格',
|
||||
zone_3 DECIMAL(10,2) COMMENT '3区价格',
|
||||
zone_4 DECIMAL(10,2) COMMENT '4区价格',
|
||||
INDEX idx_post (post)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='澳洲allied价格表';
|
||||
|
||||
-- 澳洲allied邮编表
|
||||
CREATE TABLE IF NOT EXISTS au_allied_zone (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
postcode VARCHAR(10) NOT NULL COMMENT '邮编',
|
||||
zone VARCHAR(10) COMMENT '分区',
|
||||
remote_zone VARCHAR(10) COMMENT '偏远分区',
|
||||
INDEX idx_postcode (postcode)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='澳洲allied邮编表';
|
||||
|
||||
-- 澳洲allied偏远表
|
||||
CREATE TABLE IF NOT EXISTS au_allied_remote (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
postcode VARCHAR(10) NOT NULL COMMENT '邮编',
|
||||
remote_type VARCHAR(20) COMMENT '偏远类型',
|
||||
INDEX idx_postcode (postcode)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='澳洲allied偏远表';
|
||||
|
||||
-- ============================================
|
||||
-- 欧洲物流价格表
|
||||
-- ============================================
|
||||
|
||||
-- 欧洲卡派-DHL价格表
|
||||
CREATE TABLE IF NOT EXISTS eur_dhl_price (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
price_type VARCHAR(20) NOT NULL COMMENT '价格类型',
|
||||
country VARCHAR(50) NOT NULL COMMENT '国家',
|
||||
postalcode VARCHAR(10) COMMENT '邮编',
|
||||
ip_1 DECIMAL(10,2) COMMENT '1IP价格',
|
||||
ip_2 DECIMAL(10,2) COMMENT '2IP价格',
|
||||
ip_3 DECIMAL(10,2) COMMENT '3IP价格',
|
||||
ip_4 DECIMAL(10,2) COMMENT '4IP价格',
|
||||
ip_5 DECIMAL(10,2) COMMENT '5IP价格',
|
||||
ip_6 DECIMAL(10,2) COMMENT '6IP价格',
|
||||
INDEX idx_country (country)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='欧洲DHL卡派价格表';
|
||||
|
||||
-- ============================================
|
||||
-- 物流公司配置数据
|
||||
-- ============================================
|
||||
INSERT INTO logistics_company (company_code, company_name, country, logistics_type, port, currency) VALUES
|
||||
-- 英国
|
||||
('UK_DPD', '智谷-DPD', 'UK', 'EXPRESS', 'DEFAULT', 'GBP'),
|
||||
('UK_BIG', '智谷-大件', 'UK', 'COURIER', 'DEFAULT', 'GBP'),
|
||||
('UK_KPZG', '海GB-卡派', 'UK', 'COURIER', 'DEFAULT', 'GBP'),
|
||||
('UK_KPNV', '卡派-NV', 'UK', 'COURIER', 'DEFAULT', 'GBP'),
|
||||
-- 美国
|
||||
('US_FEDEX_PP', 'Fedex-邮差小马', 'US', 'EXPRESS', 'WEST', 'USD'),
|
||||
('US_FEDEX_KH', 'Fedex-金宏亚', 'US', 'EXPRESS', 'WEST', 'USD'),
|
||||
('US_FEDEX_HOME', 'Fedex-HOME', 'US', 'EXPRESS', 'WEST', 'USD'),
|
||||
('US_FEDEX_GROUND', 'Fedex-GROUND', 'US', 'EXPRESS', 'WEST', 'USD'),
|
||||
('US_GIGA', '大健-GIGA', 'US', 'COURIER', 'DEFAULT', 'USD'),
|
||||
('US_CEVA', '大健-CEVA', 'US', 'COURIER', 'DEFAULT', 'USD'),
|
||||
('US_METRO', 'Metro-SAIR', 'US', 'COURIER', 'DEFAULT', 'USD'),
|
||||
('US_XMILES', 'XMILES-SAIR', 'US', 'COURIER', 'DEFAULT', 'USD'),
|
||||
('US_AM_WEST', 'AM-美西', 'US', 'COURIER', 'WEST', 'USD'),
|
||||
('US_AM_EAST', 'AM-美东', 'US', 'COURIER', 'EAST', 'USD'),
|
||||
-- 澳洲
|
||||
('AU_EPARCEL', 'AU-eparcel', 'AU', 'EXPRESS', 'DEFAULT', 'AUD'),
|
||||
('AU_TOLL', 'AU-Toll', 'AU', 'COURIER', 'DEFAULT', 'AUD'),
|
||||
('AU_ALLIED', 'AU-Allied', 'AU', 'COURIER', 'DEFAULT', 'AUD');
|
||||
|
|
@ -1,176 +0,0 @@
|
|||
"""物流费用计算服务测试文件"""
|
||||
import pytest
|
||||
from logistics_service import LogisticsService
|
||||
from logisticsClass.logisticsBaseClass import PortType
|
||||
|
||||
|
||||
class TestLogisticsService:
|
||||
"""物流服务测试类"""
|
||||
|
||||
def test_detect_country_uk(self):
|
||||
"""测试英国邮编识别"""
|
||||
assert LogisticsService._detect_country("PA2 9BF") == "UK"
|
||||
assert LogisticsService._detect_country("SW1A 1AA") == "UK"
|
||||
assert LogisticsService._detect_country("BT1 1AA") == "UK"
|
||||
|
||||
def test_detect_country_us(self):
|
||||
"""测试美国邮编识别"""
|
||||
assert LogisticsService._detect_country("10001") == "US"
|
||||
assert LogisticsService._detect_country("10001-1234") == "US"
|
||||
assert LogisticsService._detect_country("90210") == "US"
|
||||
|
||||
def test_detect_country_au(self):
|
||||
"""测试澳洲邮编识别"""
|
||||
assert LogisticsService._detect_country("2000") == "AU"
|
||||
assert LogisticsService._detect_country("3000") == "AU"
|
||||
|
||||
def test_parse_packages(self):
|
||||
"""测试包裹解析"""
|
||||
packages_data = [
|
||||
{"name": "包裹1", "length": 63, "width": 59, "height": 48, "weight": 8000},
|
||||
{"name": "包裹2", "length": 50, "width": 40, "height": 30, "weight": 5000},
|
||||
]
|
||||
packages = LogisticsService._parse_packages(packages_data)
|
||||
|
||||
assert len(packages.packages) == 2
|
||||
assert packages.packages[0].fst_size == 63
|
||||
assert packages.packages[1].fst_size == 50
|
||||
|
||||
def test_parse_packages_default_name(self):
|
||||
"""测试包裹解析-默认名称"""
|
||||
packages_data = [
|
||||
{"length": 63, "width": 59, "height": 48, "weight": 8000},
|
||||
]
|
||||
packages = LogisticsService._parse_packages(packages_data)
|
||||
|
||||
assert len(packages.packages) == 1
|
||||
|
||||
def test_calculate_uk(self):
|
||||
"""测试英国物流费用计算"""
|
||||
packages = [{"length": 63, "width": 59, "height": 48, "weight": 8000}]
|
||||
result = LogisticsService.calculate_uk("PA2 9BF", packages)
|
||||
|
||||
assert result["country"] == "UK"
|
||||
assert result["postcode"] == "PA2 9BF"
|
||||
assert result["optimal_channel"] is not None
|
||||
assert result["optimal_fee"] is not None
|
||||
assert result["currency"] == "GBP"
|
||||
assert len(result["all_channels"]) > 0
|
||||
|
||||
def test_calculate_uk_london(self):
|
||||
"""测试英国伦敦邮编"""
|
||||
packages = [{"length": 30, "width": 20, "height": 10, "weight": 2000}]
|
||||
result = LogisticsService.calculate_uk("SW1A 1AA", packages)
|
||||
|
||||
assert result["country"] == "UK"
|
||||
assert result["optimal_channel"] is not None
|
||||
|
||||
def test_calculate_us(self):
|
||||
"""测试美国物流费用计算"""
|
||||
packages = [{"length": 63, "width": 59, "height": 48, "weight": 8000}]
|
||||
result = LogisticsService.calculate_us("10001", packages)
|
||||
|
||||
assert result["country"] == "US"
|
||||
assert result["postcode"] == "10001"
|
||||
assert result["optimal_channel"] is not None
|
||||
assert result["optimal_fee"] is not None
|
||||
assert result["currency"] == "USD"
|
||||
|
||||
def test_calculate_us_west(self):
|
||||
"""测试美国美西邮编"""
|
||||
packages = [{"length": 50, "width": 40, "height": 30, "weight": 5000}]
|
||||
result = LogisticsService.calculate_us("90001", packages)
|
||||
|
||||
assert result["country"] == "US"
|
||||
assert result["optimal_channel"] is not None
|
||||
|
||||
def test_calculate_auto_detect(self):
|
||||
"""测试自动识别国家"""
|
||||
packages = [{"length": 63, "width": 59, "height": 48, "weight": 8000}]
|
||||
|
||||
# 英国
|
||||
result_uk = LogisticsService.calculate("PA2 9BF", packages)
|
||||
assert result_uk["country"] == "UK"
|
||||
|
||||
# 美国
|
||||
result_us = LogisticsService.calculate("10001", packages)
|
||||
assert result_us["country"] == "US"
|
||||
|
||||
def test_calculate_multiple_packages(self):
|
||||
"""测试多包裹计算"""
|
||||
packages = [
|
||||
{"length": 63, "width": 59, "height": 48, "weight": 8000},
|
||||
{"length": 50, "width": 40, "height": 30, "weight": 5000},
|
||||
{"length": 40, "width": 30, "height": 20, "weight": 3000},
|
||||
]
|
||||
result = LogisticsService.calculate_uk("PA2 9BF", packages)
|
||||
|
||||
assert result["package_count"] == 3
|
||||
assert result["total_weight"] == 16.0 # kg
|
||||
|
||||
def test_get_company_detail(self):
|
||||
"""测试获取指定物流公司详情"""
|
||||
packages = [{"length": 63, "width": 59, "height": 48, "weight": 8000}]
|
||||
detail = LogisticsService.get_company_detail("PA2 9BF", packages, "智谷-DPD")
|
||||
|
||||
assert detail["company"] == "智谷-DPD"
|
||||
assert detail["currency"] == "GBP"
|
||||
assert "detail" in detail
|
||||
assert "total" in detail
|
||||
|
||||
def test_invalid_postcode(self):
|
||||
"""测试无效邮编"""
|
||||
packages = [{"length": 63, "width": 59, "height": 48, "weight": 8000}]
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
LogisticsService.calculate("invalid", packages)
|
||||
|
||||
def test_result_structure(self):
|
||||
"""测试返回结果结构"""
|
||||
packages = [{"length": 63, "width": 59, "height": 48, "weight": 8000}]
|
||||
result = LogisticsService.calculate_uk("PA2 9BF", packages)
|
||||
|
||||
# 验证必需字段
|
||||
required_fields = [
|
||||
"country", "postcode", "optimal_channel", "optimal_fee",
|
||||
"currency", "all_channels", "package_count", "total_weight"
|
||||
]
|
||||
for field in required_fields:
|
||||
assert field in result, f"缺少字段: {field}"
|
||||
|
||||
# 验证渠道详情结构
|
||||
for company, info in result["all_channels"].items():
|
||||
assert "fee" in info
|
||||
assert "currency" in info
|
||||
assert "type" in info
|
||||
assert "available" in info
|
||||
|
||||
|
||||
class TestPackageCalculation:
|
||||
"""包裹计算测试类"""
|
||||
|
||||
def test_small_package(self):
|
||||
"""测试小包裹"""
|
||||
packages = [{"length": 20, "width": 15, "height": 10, "weight": 500}]
|
||||
result = LogisticsService.calculate_uk("SW1A 1AA", packages)
|
||||
|
||||
assert result["optimal_fee"] is not None
|
||||
|
||||
def test_large_package(self):
|
||||
"""测试大包裹"""
|
||||
packages = [{"length": 200, "width": 100, "height": 80, "weight": 50000}]
|
||||
result = LogisticsService.calculate_uk("PA2 9BF", packages)
|
||||
|
||||
# 大包裹可能被某些渠道拒绝
|
||||
assert result is not None
|
||||
|
||||
def test_heavy_package(self):
|
||||
"""测试重包裹"""
|
||||
packages = [{"length": 50, "width": 40, "height": 30, "weight": 80000}]
|
||||
result = LogisticsService.calculate_us("10001", packages)
|
||||
|
||||
assert result is not None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
"""配置管理模块"""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
"""配置管理器"""
|
||||
|
||||
_instance = None
|
||||
_config: Dict[str, Any] = {}
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
if not self._config:
|
||||
self._load_config()
|
||||
|
||||
def _load_config(self):
|
||||
"""加载配置文件"""
|
||||
config_dir = Path(__file__).parent.parent / "config"
|
||||
config_file = config_dir / "database.json"
|
||||
|
||||
if not config_file.exists():
|
||||
raise FileNotFoundError(f"配置文件不存在: {config_file}")
|
||||
|
||||
with open(config_file, "r", encoding="utf-8") as f:
|
||||
self._config = json.load(f)
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
"""获取配置项"""
|
||||
keys = key.split(".")
|
||||
value = self._config
|
||||
for k in keys:
|
||||
if isinstance(value, dict):
|
||||
value = value.get(k)
|
||||
if value is None:
|
||||
return default
|
||||
else:
|
||||
return default
|
||||
return value
|
||||
|
||||
def get_database_config(self) -> Dict[str, Any]:
|
||||
"""获取数据库配置"""
|
||||
return self._config.get("database", {})
|
||||
|
||||
|
||||
# 全局配置实例
|
||||
config = ConfigManager()
|
||||
|
|
@ -1,22 +1,13 @@
|
|||
import pymysql
|
||||
from sqlalchemy import create_engine
|
||||
from utils.config_manager import config
|
||||
|
||||
|
||||
class MySQLconnect():
|
||||
def __init__(self, dbname: str = None):
|
||||
db_config = config.get_database_config()
|
||||
self.host = db_config.get("host", "192.168.100.33")
|
||||
self.port = db_config.get("port", 3306)
|
||||
self.user = db_config.get("username", "zhenggantian")
|
||||
self.password = db_config.get("password", "123456")
|
||||
self.dbname = dbname or db_config.get("database", "logistics")
|
||||
self.charset = db_config.get("charset", "utf8")
|
||||
self.pool_size = db_config.get("pool_size", 10)
|
||||
self.max_overflow = db_config.get("max_overflow", 5)
|
||||
self.pool_recycle = db_config.get("pool_recycle", 3600)
|
||||
|
||||
if not isinstance(self.dbname, str):
|
||||
def __init__(self, dbname: str):
|
||||
if isinstance(dbname, str):
|
||||
self.dbname = dbname
|
||||
self.host = '192.168.100.33'
|
||||
else:
|
||||
raise TypeError("dbname must be a string")
|
||||
|
||||
def __enter__(self):
|
||||
|
|
@ -33,12 +24,8 @@ class MySQLconnect():
|
|||
raise
|
||||
|
||||
def engine(self):
|
||||
return create_engine(
|
||||
f"mysql+pymysql://{self.user}:{self.password}@{self.host}:{self.port}/{self.dbname}",
|
||||
pool_size=self.pool_size,
|
||||
max_overflow=self.max_overflow,
|
||||
pool_recycle=self.pool_recycle
|
||||
)
|
||||
return create_engine("mysql+pymysql://zhenggantian:123456@" + self.host + f":3306/{self.dbname}",
|
||||
pool_size=10, max_overflow=5, pool_recycle=3600)
|
||||
|
||||
def connect(self):
|
||||
return pymysql.connect(host=self.host, port=3306, database=self.dbname, user="zhenggantian", password="123456",
|
||||
|
|
@ -68,4 +55,4 @@ class DBconnect():
|
|||
|
||||
def connect(self):
|
||||
return pymysql.connect(host=self.host, port=3306, database="logistics", user="logistics", password="logistics123",
|
||||
charset="utf8")
|
||||
charset="utf8")
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
5017
自动化跟单11.ipynb
5017
自动化跟单11.ipynb
File diff suppressed because one or more lines are too long
BIN
跟单测试源文件.xlsx (Stored with Git LFS)
BIN
跟单测试源文件.xlsx (Stored with Git LFS)
Binary file not shown.
Loading…
Reference in New Issue