feat(discount): 折扣率语义扩展——允许>1加价卖给客户+解析改专属优先/*兜底

用户决策(2026-09-06):
1. 折扣率允许大于1:平台可加价卖产品给客户(如存储缺省1.5);
   也可小于1让利(如百炼模型0.9,供应商有折扣传导给客户)
2. 解析规则:确定客户→查客户专属折扣;查不到→'*'通配兜底;再无→1.0
   旧实现bug:专属与通配OR混查取全局min——专属0.95+通配0.8返回0.8,
   专属记录形同虚设

改动:
- init.py sor_get_min_product_discount: 两步查询(专属层min→通配层min)
- init.py add/update_discount_detail: 上限1→9.9999(DB double(5,4)约束)
- discount_setting/save_discount.dspy: 兜底记录识别'*'+NULL两种形态;
  「客户折扣率不得高于通用折扣率」校验保留(加价语义下=单客户价不贵于通用价)
- marketing_discount_setting/save_discount.dspy: >1上限放开,保留<基准约束
- generate_qr.dspy: 同上放开
- index.ui: placeholder 0~1→'>1加价 <1折扣',dec_len 2→4

本地mock测试5场景全过:专属优先/通配兜底/无记录1.0/exact_match不兜底/层内min
This commit is contained in:
yumoqing 2026-09-06 15:43:01 +08:00
parent a3d7077acc
commit 55b4943170
5 changed files with 57 additions and 26 deletions

View File

@ -277,7 +277,18 @@ where discountid = ${discountid}$
async def sor_get_min_product_discount(sor, product_id, resellerid, customerid, exact_match=False): async def sor_get_min_product_discount(sor, product_id, resellerid, customerid, exact_match=False):
"""Find the minimum discount for a specific product from active discount records. """客户折扣率解析(2026-09-06 语义修订:专属优先,通配兜底)。
解析顺序(用户定义):
1. 客户专属记录优先——discount.customerid 精确匹配,同层级多条取 min
2. 查不到且非 exact_match → '*' 或 NULL 通配兜底记录,多条取 min
3. 仍查不到 → 1.0(无折扣)
旧实现的 bug:把专属与通配记录混在一起 OR 查询后取全局 min——
专属 0.95 + 通配 0.8 会返回 0.8,专属记录形同虚设。
折扣率允许 >1(加价卖给客户)或 <1(让利),上限受
discount_detail.discount double(5,4) 约束 = 9.9999。
Args: Args:
sor: sqlor context sor: sqlor context
@ -290,30 +301,40 @@ async def sor_get_min_product_discount(sor, product_id, resellerid, customerid,
env = ServerEnv() env = ServerEnv()
biz_date = await env.get_business_date(sor) biz_date = await env.get_business_date(sor)
if exact_match: base_sql = """SELECT dd.discount
customer_cond = "d.customerid = ${customerid}$"
else:
customer_cond = "(d.customerid = ${customerid}$ OR d.customerid IS NULL OR d.customerid = '*')"
sql = f"""SELECT dd.discount
FROM discount_detail dd FROM discount_detail dd
JOIN discount d ON dd.discountid = d.id JOIN discount d ON dd.discountid = d.id
WHERE d.resellerid = ${{resellerid}}$ WHERE d.resellerid = ${resellerid}$
AND {customer_cond} AND {customer_cond}
AND d.enabled_date <= ${{biz_date}}$ AND d.enabled_date <= ${biz_date}$
AND d.expired_date > ${{biz_date}}$ AND d.expired_date > ${biz_date}$
AND dd.productid = ${{product_id}}$""" AND dd.productid = ${product_id}$"""
ns = { ns = {
'resellerid': resellerid, 'resellerid': resellerid,
'customerid': customerid, 'customerid': customerid,
'biz_date': biz_date, 'biz_date': biz_date,
'product_id': product_id, 'product_id': product_id,
} }
# 1) 客户专属优先
sql = base_sql.replace('{customer_cond}', "d.customerid = ${customerid}$")
recs = await sor.sqlExe(sql, ns) recs = await sor.sqlExe(sql, ns)
if recs: if recs:
discounts = [r.discount for r in recs if r.discount is not None] discounts = [r.discount for r in recs if r.discount is not None]
if discounts: if discounts:
return min(discounts) return min(discounts)
# 2) '*' / NULL 通配兜底(供应商/分销商 exact_match 不兜底)
if not exact_match:
sql = base_sql.replace(
'{customer_cond}', "(d.customerid = '*' OR d.customerid IS NULL)")
recs = await sor.sqlExe(sql, ns)
if recs:
discounts = [r.discount for r in recs if r.discount is not None]
if discounts:
return min(discounts)
# 3) 无任何记录 → 原价
return 1.0 return 1.0
@ -402,8 +423,11 @@ order by prodtypeid, productid"""
async def add_discount_detail(sor, discountid, resellerid, prodtypeid, productid, discount): async def add_discount_detail(sor, discountid, resellerid, prodtypeid, productid, discount):
"""Add a product-specific discount detail.""" """Add a product-specific discount detail."""
if discount <= 0 or discount > 1: # 2026-09-06: 折扣率允许 >1(加价卖给客户,如存储缺省1.5)或 <1(让利),
raise Exception(f'discount({discount}) invalid, must be between 0 and 1 (inclusive)') # 上限受 discount_detail.discount double(5,4) 约束 = 9.9999
if discount <= 0 or discount > 9.9999:
raise Exception(f'discount({discount}) invalid, must be in (0, 9.9999] — '
f'>1 表示加价、<1 表示折扣')
ret = { ret = {
'id': getID(), 'id': getID(),
@ -419,8 +443,10 @@ async def add_discount_detail(sor, discountid, resellerid, prodtypeid, productid
async def update_discount_detail(sor, detail_id, discount): async def update_discount_detail(sor, detail_id, discount):
"""Update a discount detail record.""" """Update a discount detail record."""
if discount <= 0 or discount > 1: # 2026-09-06: 同 add_discount_detail——允许 >1 加价
raise Exception(f'discount({discount}) invalid, must be between 0 and 1 (inclusive)') if discount <= 0 or discount > 9.9999:
raise Exception(f'discount({discount}) invalid, must be in (0, 9.9999] — '
f'>1 表示加价、<1 表示折扣')
await sor.U('discount_detail', {'id': detail_id, 'discount': discount}) await sor.U('discount_detail', {'id': detail_id, 'discount': discount})

View File

@ -69,8 +69,8 @@
"padding": "0 8px", "padding": "0 8px",
"name": "discount", "name": "discount",
"value": {{json.dumps(p.discount or '1.0')}}, "value": {{json.dumps(p.discount or '1.0')}},
"placeholder": "0~1", "placeholder": ">1加价 <1折扣",
"dec_len": 2, "dec_len": 4,
"fontSize": "12px" "fontSize": "12px"
}, },
"binds": [ "binds": [

View File

@ -51,12 +51,15 @@ try:
disc_sql = "SELECT customerid, resellerid FROM discount WHERE id = ${id}$" disc_sql = "SELECT customerid, resellerid FROM discount WHERE id = ${id}$"
disc_recs = await sor.sqlExe(disc_sql, {'id': discountid}) disc_recs = await sor.sqlExe(disc_sql, {'id': discountid})
if disc_recs and disc_recs[0].customerid: if disc_recs and disc_recs[0].customerid:
# 查找全部用户默认方案的同一产品折扣 # 查找全部用户默认方案的同一产品折扣(兜底记录两种形态:customerid='*' 或 NULL,
# 与 sor_get_min_product_discount 的通配兜底语义一致)
default_sql = """ default_sql = """
SELECT dd.discount FROM discount_detail dd SELECT dd.discount FROM discount_detail dd
JOIN discount d ON dd.discountid = d.id JOIN discount d ON dd.discountid = d.id
WHERE d.resellerid = ${resellerid}$ AND d.customerid IS NULL WHERE d.resellerid = ${resellerid}$
AND (d.customerid IS NULL OR d.customerid = '*')
AND dd.productid = ${productid}$ AND dd.productid = ${productid}$
AND d.enabled_date <= NOW() AND d.expired_date > NOW()
LIMIT 1 LIMIT 1
""" """
default_recs = await sor.sqlExe(default_sql, {'resellerid': disc_recs[0].resellerid, 'productid': productid}) default_recs = await sor.sqlExe(default_sql, {'resellerid': disc_recs[0].resellerid, 'productid': productid})
@ -64,7 +67,7 @@ try:
default_d = float(default_recs[0].discount) default_d = float(default_recs[0].discount)
customer_d = float(discount_val) customer_d = float(discount_val)
if customer_d > default_d: if customer_d > default_d:
result = {'widgettype': 'Error', 'options': {'title': '折扣无效', 'message': f'客户折扣({customer_d})不能高于全部用户折扣({default_d})', 'timeout': 5}} result = {'widgettype': 'Error', 'options': {'title': '折扣无效', 'message': f'客户折扣率({customer_d})不能高于通用折扣率({default_d})——单个客户价格不得贵于通用价', 'timeout': 5}}
return result return result
if detail_id: if detail_id:

View File

@ -1,8 +1,9 @@
from ahserver.serverenv import ServerEnv from ahserver.serverenv import ServerEnv
params_kw.discount = float(params_kw.discount) params_kw.discount = float(params_kw.discount)
if params_kw.discount <= 0 or params_kw.discount >= 1: # 2026-09-06: 折扣率允许 >1(加价促销场景),上限 9.9999(DB double(5,4))
e = Exception(f'discount({params_kw.discount}) invalid') if params_kw.discount <= 0 or params_kw.discount > 9.9999:
e = Exception(f'discount({params_kw.discount}) invalid, must be in (0, 9.9999]')
exception(f'{e}') exception(f'{e}')
raise e raise e

View File

@ -44,12 +44,13 @@ try:
if base_discount: if base_discount:
base_val = float(base_discount) base_val = float(base_discount)
if new_val >= base_val: if new_val >= base_val:
result = {'widgettype': 'Error', 'options': {'title': '折扣无效', 'message': f'营销折扣({new_val})必须小于基准折扣({base_val})', 'timeout': 5}} result = {'widgettype': 'Error', 'options': {'title': '折扣无效', 'message': f'营销折扣率({new_val})必须小于基准折扣率({base_val})', 'timeout': 5}}
return result return result
# 验证: 不能超过100%(1.0) # 2026-09-06: 放开 >1 上限——折扣率>1=加价(基准若是1.5加价,营销可设1.2仍低于基准)。
if new_val > 1.0: # 约束只剩两条:>0(上面已验)、< 基准折扣率(上面已验)、≤9.9999(DB double(5,4))
result = {'widgettype': 'Error', 'options': {'title': '折扣无效', 'message': '折扣不能大于1.0', 'timeout': 5}} if new_val > 9.9999:
result = {'widgettype': 'Error', 'options': {'title': '折扣无效', 'message': '折扣率不能大于9.9999', 'timeout': 5}}
return result return result
async with DBPools().sqlorContext(dbname) as sor: async with DBPools().sqlorContext(dbname) as sor: