feat: 营销方案、促销码、客户归属绑定功能

- 新增3个数据模型: discount_marketing, discount_promo_code, discount_customer_bind
- 新增CRUD定义: marketing/promo_code/customer_bind 的list和api
- 新增init/data.json: appcodes编码(促销码类型、绑定方式)
- init.py: bind_customer支持customerid参数覆盖(注册场景)
- 折扣校验放宽至 0 < discount <= 1
This commit is contained in:
Hermes Agent 2026-06-24 00:56:32 +08:00
parent 8498a10809
commit f5250fb261
19 changed files with 875 additions and 4 deletions

Binary file not shown.

View File

@ -319,8 +319,8 @@ order by prodtypeid, productid"""
async def add_discount_detail(sor, discountid, resellerid, prodtypeid, productid, discount):
"""Add a product-specific discount detail."""
if discount <= 0 or discount >= 1:
raise Exception(f'discount({discount}) invalid, must be between 0 and 1')
if discount <= 0 or discount > 1:
raise Exception(f'discount({discount}) invalid, must be between 0 and 1 (inclusive)')
ret = {
'id': getID(),
@ -336,8 +336,8 @@ async def add_discount_detail(sor, discountid, resellerid, prodtypeid, productid
async def update_discount_detail(sor, detail_id, discount):
"""Update a discount detail record."""
if discount <= 0 or discount >= 1:
raise Exception(f'discount({discount}) invalid, must be between 0 and 1')
if discount <= 0 or discount > 1:
raise Exception(f'discount({discount}) invalid, must be between 0 and 1 (inclusive)')
await sor.U('discount_detail', {'id': detail_id, 'discount': discount})
@ -347,6 +347,366 @@ async def delete_discount_detail(sor, detail_id):
await sor.D('discount_detail', {'id': detail_id})
# ─── Marketing & Promo Code & Customer Bind ───
async def create_marketing(request, params_kw):
"""Create a marketing plan (reseller operator only)."""
env = ServerEnv()
dbname = env.get_module_dbname('discount')
resellerid = await env.get_userorgid()
user_id = await env.get_user()
name = params_kw.get('name')
enabled_date = params_kw.get('enabled_date')
expired_date = params_kw.get('expired_date')
if not name:
return {'status': 'error', 'message': 'Missing name'}
async with DBPools().sqlorContext(dbname) as sor:
marketing_id = getID()
await sor.C('discount_marketing', {
'id': marketing_id,
'resellerid': resellerid,
'name': name,
'enabled_date': enabled_date,
'expired_date': expired_date,
'status': '1',
'created_by': user_id,
})
# Also create a discount record linked to this marketing
discount_id = getID()
await sor.C('discount', {
'id': discount_id,
'name': f'营销方案-{name}',
'resellerid': resellerid,
'customerid': None, # applies to all customers of this reseller
'enabled_date': enabled_date,
'expired_date': expired_date,
})
return {'status': 'success', 'id': marketing_id, 'discount_id': discount_id}
async def update_marketing(request, params_kw):
"""Update a marketing plan."""
env = ServerEnv()
dbname = env.get_module_dbname('discount')
marketing_id = params_kw.get('id')
resellerid = await env.get_userorgid()
if not marketing_id:
return {'status': 'error', 'message': 'Missing id'}
async with DBPools().sqlorContext(dbname) as sor:
# Verify ownership
recs = await sor.R('discount_marketing', {'id': marketing_id, 'resellerid': resellerid})
if not recs:
return {'status': 'error', 'message': 'Marketing not found or no access'}
update_data = {'id': marketing_id}
for field in ['name', 'enabled_date', 'expired_date', 'status']:
if field in params_kw:
update_data[field] = params_kw[field]
await sor.U('discount_marketing', update_data)
return {'status': 'success', 'id': marketing_id}
async def delete_marketing(request, params_kw):
"""Delete a marketing plan and its promo codes."""
env = ServerEnv()
dbname = env.get_module_dbname('discount')
marketing_id = params_kw.get('id')
resellerid = await env.get_userorgid()
if not marketing_id:
return {'status': 'error', 'message': 'Missing id'}
async with DBPools().sqlorContext(dbname) as sor:
recs = await sor.R('discount_marketing', {'id': marketing_id, 'resellerid': resellerid})
if not recs:
return {'status': 'error', 'message': 'Marketing not found or no access'}
# Delete promo codes first
promo_codes = await sor.R('discount_promo_code', {'marketing_id': marketing_id})
for pc in (promo_codes or []):
await sor.D('discount_promo_code', {'id': pc.id})
await sor.D('discount_marketing', {'id': marketing_id})
return {'status': 'success'}
async def get_marketing(request, params_kw):
"""Get marketing plan detail with its discount details."""
env = ServerEnv()
dbname = env.get_module_dbname('discount')
marketing_id = params_kw.get('id')
resellerid = await env.get_userorgid()
if not marketing_id:
return {'status': 'error', 'message': 'Missing id'}
async with DBPools().sqlorContext(dbname) as sor:
recs = await sor.R('discount_marketing', {'id': marketing_id, 'resellerid': resellerid})
if not recs:
return {'status': 'error', 'message': 'Marketing not found'}
marketing = recs[0]
# Get associated discount record
discount_recs = await sor.sqlExe(
"SELECT * FROM discount WHERE name = ${name}$ AND resellerid = ${resellerid}$ AND customerid IS NULL",
{'name': f'营销方案-{marketing.name}', 'resellerid': resellerid}
)
discount_details = []
if discount_recs:
discount_id = discount_recs[0].id
discount_details = await sor.R('discount_detail', {'discountid': discount_id, 'resellerid': resellerid})
return {
'status': 'success',
'data': {
'marketing': marketing,
'discount_details': discount_details or []
}
}
async def list_marketings(request, params_kw):
"""List all marketing plans for current reseller."""
env = ServerEnv()
dbname = env.get_module_dbname('discount')
resellerid = await env.get_userorgid()
async with DBPools().sqlorContext(dbname) as sor:
recs = await sor.R('discount_marketing', {'resellerid': resellerid, 'sort': 'created_at desc'})
return {'status': 'success', 'data': recs or [], 'total': len(recs or [])}
async def generate_promo_code(request, params_kw):
"""Generate a promo code (sale role).
marketing_id can be empty (pure invitation code)."""
env = ServerEnv()
dbname = env.get_module_dbname('discount')
resellerid = await env.get_userorgid()
sale_id = await env.get_user()
marketing_id = params_kw.get('marketing_id') # can be empty
async with DBPools().sqlorContext(dbname) as sor:
# If marketing_id provided, verify it exists and belongs to this reseller
if marketing_id:
mkt = await sor.R('discount_marketing', {'id': marketing_id, 'resellerid': resellerid})
if not mkt:
return {'status': 'error', 'message': 'Marketing not found or no access'}
code_type = '1' # promo code
else:
code_type = '2' # invitation code
code_id = getID()
await sor.C('discount_promo_code', {
'id': code_id, # id IS the promo code
'marketing_id': marketing_id if marketing_id else None,
'resellerid': resellerid,
'sale_id': sale_id,
'code_type': code_type,
'status': '1',
})
return {'status': 'success', 'code': code_id}
async def disable_promo_code(request, params_kw):
"""Disable a promo code."""
env = ServerEnv()
dbname = env.get_module_dbname('discount')
code_id = params_kw.get('id')
resellerid = await env.get_userorgid()
if not code_id:
return {'status': 'error', 'message': 'Missing id'}
async with DBPools().sqlorContext(dbname) as sor:
recs = await sor.R('discount_promo_code', {'id': code_id, 'resellerid': resellerid})
if not recs:
return {'status': 'error', 'message': 'Promo code not found'}
await sor.U('discount_promo_code', {'id': code_id, 'status': '0'})
return {'status': 'success'}
async def bind_customer(request, params_kw):
"""Bind customer to reseller+sale via promo code (called during registration/login).
First-time binding only. Update discounts if new ones are lower.
params_kw may contain 'customerid' to override current user's orgid (for registration flow)."""
env = ServerEnv()
dbname = env.get_module_dbname('discount')
config = getConfig()
db = DBPools()
db.databases = config.databases
code_id = params_kw.get('promo_code_id')
customerid = params_kw.get('customerid') or await env.get_userorgid()
if not code_id:
return {'status': 'error', 'message': 'Missing promo_code_id'}
async with db.sqlorContext(dbname) as sor:
# Check if customer already bound
existing = await sor.R('discount_customer_bind', {'customerid': customerid})
if existing:
return {'status': 'error', 'message': 'Customer already bound'}
# Verify promo code
code_recs = await sor.R('discount_promo_code', {'id': code_id, 'status': '1'})
if not code_recs:
return {'status': 'error', 'message': 'Invalid or disabled promo code'}
code_rec = code_recs[0]
resellerid = code_rec.resellerid
sale_id = code_rec.sale_id
# Create binding
bind_id = getID()
await sor.C('discount_customer_bind', {
'id': bind_id,
'customerid': customerid,
'resellerid': resellerid,
'sale_id': sale_id,
'promo_code_id': code_id,
'bind_type': '1',
})
# If promo code has marketing, copy discount details to customer
if code_rec.marketing_id:
await _apply_marketing_discounts_to_customer(sor, resellerid, customerid, code_rec.marketing_id)
return {'status': 'success', 'resellerid': resellerid, 'sale_id': sale_id}
async def _apply_marketing_discounts_to_customer(sor, resellerid, customerid, marketing_id):
"""Copy marketing's discount details to customer, only if new discount is lower."""
env = ServerEnv()
biz_date = await env.get_business_date(sor)
# Find marketing's discount record
mkt_recs = await sor.R('discount_marketing', {'id': marketing_id, 'resellerid': resellerid})
if not mkt_recs:
return
mkt_name = mkt_recs[0].name
mkt_discount_recs = await sor.sqlExe(
"SELECT * FROM discount WHERE name = ${name}$ AND resellerid = ${resellerid}$ AND customerid IS NULL",
{'name': f'营销方案-{mkt_name}', 'resellerid': resellerid}
)
if not mkt_discount_recs:
return
mkt_discount_id = mkt_discount_recs[0].id
mkt_details = await sor.R('discount_detail', {'discountid': mkt_discount_id})
if not mkt_details:
return
# Find or create customer's discount record
cust_discount = await sor_get_customer_discount(sor, resellerid, customerid)
# Get customer's discount id
cust_sql = """SELECT id FROM discount
WHERE resellerid = ${resellerid}$ AND customerid = ${customerid}$
AND enabled_date <= ${biz_date}$ AND expired_date > ${biz_date}$"""
cust_recs = await sor.sqlExe(cust_sql, {'resellerid': resellerid, 'customerid': customerid, 'biz_date': biz_date})
if not cust_recs:
# Create new discount record for customer
new_discount_id = getID()
await sor.C('discount', {
'id': new_discount_id,
'name': f'客户折扣-{customerid}',
'resellerid': resellerid,
'customerid': customerid,
'enabled_date': biz_date,
'expired_date': '2099-12-31',
})
cust_discount_id = new_discount_id
else:
cust_discount_id = cust_recs[0].id
# Copy details, only if new discount is lower than existing
for mkt_detail in mkt_details:
# Check if customer already has this product discount
existing_detail = await sor.sqlExe(
"""SELECT * FROM discount_detail
WHERE discountid = ${discountid}$ AND prodtypeid = ${prodtypeid}$ AND productid = ${productid}$""",
{
'discountid': cust_discount_id,
'prodtypeid': mkt_detail.prodtypeid,
'productid': mkt_detail.productid
}
)
if existing_detail:
# Only update if new discount is lower (better for customer)
if mkt_detail.discount < existing_detail[0].discount:
await sor.U('discount_detail', {'id': existing_detail[0].id, 'discount': mkt_detail.discount})
else:
# Add new detail
await sor.C('discount_detail', {
'id': getID(),
'discountid': cust_discount_id,
'resellerid': resellerid,
'prodtypeid': mkt_detail.prodtypeid,
'productid': mkt_detail.productid,
'discount': mkt_detail.discount,
})
async def get_customer_bind(request, params_kw):
"""Get customer's binding info."""
env = ServerEnv()
dbname = env.get_module_dbname('discount')
customerid = params_kw.get('customerid') or await env.get_userorgid()
async with DBPools().sqlorContext(dbname) as sor:
recs = await sor.R('discount_customer_bind', {'customerid': customerid})
if not recs:
return {'status': 'success', 'data': None}
return {'status': 'success', 'data': recs[0]}
async def assign_customer_to_sale(request, params_kw):
"""Owner operator manually assigns unbound customer to a sale."""
env = ServerEnv()
dbname = env.get_module_dbname('discount')
customerid = params_kw.get('customerid')
sale_id = params_kw.get('sale_id')
if not customerid or not sale_id:
return {'status': 'error', 'message': 'Missing customerid or sale_id'}
# Verify caller is owner org (orgid='0')
caller_orgid = await env.get_userorgid()
if caller_orgid != '0':
return {'status': 'error', 'message': 'Only owner can assign customers'}
async with DBPools().sqlorContext(dbname) as sor:
# Check if already bound
existing = await sor.R('discount_customer_bind', {'customerid': customerid})
if existing:
return {'status': 'error', 'message': 'Customer already bound'}
bind_id = getID()
await sor.C('discount_customer_bind', {
'id': bind_id,
'customerid': customerid,
'resellerid': '0', # owner org
'sale_id': sale_id,
'promo_code_id': None,
'bind_type': '2', # manual assignment
})
return {'status': 'success', 'id': bind_id}
def load_discount():
env = ServerEnv()
env.get_customer_discount = get_customer_discount
@ -359,3 +719,14 @@ def load_discount():
env.add_discount_detail = add_discount_detail
env.update_discount_detail = update_discount_detail
env.delete_discount_detail = delete_discount_detail
# Marketing & Promo Code
env.create_marketing = create_marketing
env.update_marketing = update_marketing
env.delete_marketing = delete_marketing
env.get_marketing = get_marketing
env.list_marketings = list_marketings
env.generate_promo_code = generate_promo_code
env.disable_promo_code = disable_promo_code
env.bind_customer = bind_customer
env.get_customer_bind = get_customer_bind
env.assign_customer_to_sale = assign_customer_to_sale

40
init/data.json Normal file
View File

@ -0,0 +1,40 @@
{
"appcodes": [
{
"id": "promo_code_type",
"name": "促销码类型",
"hierarchy_flg": "0"
},
{
"id": "bind_type",
"name": "客户绑定方式",
"hierarchy_flg": "0"
}
],
"appcodes_kv": [
{
"id": "promo_code_type_1",
"parentid": "promo_code_type",
"k": "1",
"v": "促销码"
},
{
"id": "promo_code_type_2",
"parentid": "promo_code_type",
"k": "2",
"v": "邀请码"
},
{
"id": "bind_type_1",
"parentid": "bind_type",
"k": "1",
"v": "促销码"
},
{
"id": "bind_type_2",
"parentid": "bind_type",
"k": "2",
"v": "手动分配"
}
]
}

View File

@ -0,0 +1,38 @@
{
"tblname": "discount_customer_bind",
"alias": "discount_customer_bind_list",
"title": "客户归属",
"params": {
"browserfields": {
"customerid": {
"title": "客户用户id",
"width": 200
},
"resellerid": {
"title": "归属商户",
"width": 150
},
"sale_id": {
"title": "归属销售",
"width": 120
},
"promo_code_id": {
"title": "促销码",
"width": 150
},
"bind_type": {
"title": "绑定方式",
"width": 100
},
"created_at": {
"title": "绑定时间",
"width": 160
}
},
"editable": {
"new_data_url": "{{entire_url('../api/customer_bind_create.dspy')}}",
"update_data_url": "{{entire_url('../api/customer_bind_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/customer_bind_delete.dspy')}}"
}
}
}

View File

@ -0,0 +1,38 @@
{
"tblname": "discount_marketing",
"alias": "discount_marketing_list",
"title": "营销方案",
"params": {
"browserfields": {
"name": {
"title": "方案名称"
},
"resellerid": {
"title": "商户机构",
"width": 150
},
"enabled_date": {
"title": "启用日期",
"width": 120
},
"expired_date": {
"title": "失效日期",
"width": 120
},
"status": {
"title": "状态",
"width": 80
},
"created_at": {
"title": "创建时间",
"width": 160
}
},
"editable": {
"new_data_url": "{{entire_url('../api/marketing_create.dspy')}}",
"update_data_url": "{{entire_url('../api/marketing_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/marketing_delete.dspy')}}"
},
"logined_userorgid": "resellerid"
}
}

View File

@ -0,0 +1,43 @@
{
"tblname": "discount_promo_code",
"alias": "discount_promo_code_list",
"title": "促销码",
"params": {
"browserfields": {
"id": {
"title": "促销码",
"width": 200
},
"marketing_id": {
"title": "营销方案",
"width": 150
},
"code_type": {
"title": "类型",
"width": 80
},
"resellerid": {
"title": "商户机构",
"width": 150
},
"sale_id": {
"title": "生成销售",
"width": 120
},
"status": {
"title": "状态",
"width": 80
},
"created_at": {
"title": "生成时间",
"width": 160
}
},
"editable": {
"new_data_url": "{{entire_url('../api/promo_code_create.dspy')}}",
"update_data_url": "{{entire_url('../api/promo_code_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/promo_code_delete.dspy')}}"
},
"logined_userorgid": "resellerid"
}
}

View File

@ -0,0 +1,98 @@
{
"summary": [
{
"name": "discount_customer_bind",
"title": "客户归属关系",
"primary": ["id"],
"catelog": "relation"
}
],
"fields": [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "customerid",
"title": "客户用户id",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "resellerid",
"title": "归属商户机构id",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "sale_id",
"title": "归属销售用户id",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "promo_code_id",
"title": "促销码id(可空=手动分配)",
"type": "str",
"length": 32
},
{
"name": "bind_type",
"title": "绑定方式(1=促销码 2=手动分配)",
"type": "char",
"length": 1,
"default": "1",
"nullable": "no"
},
{
"name": "created_at",
"title": "绑定时间",
"type": "timestamp",
"default": "CURRENT_TIMESTAMP",
"nullable": "no"
}
],
"indexes": [
{
"name": "idx_dcb_customer",
"idxtype": "unique",
"idxfields": ["customerid"]
},
{
"name": "idx_dcb_reseller",
"idxtype": "index",
"idxfields": ["resellerid"]
},
{
"name": "idx_dcb_sale",
"idxtype": "index",
"idxfields": ["sale_id"]
},
{
"name": "idx_dcb_promo",
"idxtype": "index",
"idxfields": ["promo_code_id"]
}
],
"codes": [
{
"field": "resellerid",
"table": "organization",
"valuefield": "id",
"textfield": "orgname"
},
{
"field": "bind_type",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='bind_type'"
}
]
}

View File

@ -0,0 +1,97 @@
{
"summary": [
{
"name": "discount_marketing",
"title": "营销方案",
"primary": ["id"],
"catelog": "entity"
}
],
"fields": [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "resellerid",
"title": "商户机构id",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "name",
"title": "方案名称",
"type": "str",
"length": 255,
"nullable": "no"
},
{
"name": "enabled_date",
"title": "启用日期",
"type": "date"
},
{
"name": "expired_date",
"title": "失效日期",
"type": "date"
},
{
"name": "status",
"title": "状态",
"type": "char",
"length": 1,
"default": "1"
},
{
"name": "created_by",
"title": "创建人",
"type": "str",
"length": 32
},
{
"name": "created_at",
"title": "创建时间",
"type": "timestamp",
"default": "CURRENT_TIMESTAMP",
"nullable": "no"
},
{
"name": "updated_at",
"title": "更新时间",
"type": "timestamp",
"default": "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP",
"nullable": "no"
}
],
"indexes": [
{
"name": "idx_dm_reseller",
"idxtype": "index",
"idxfields": ["resellerid"]
},
{
"name": "idx_dm_status",
"idxtype": "index",
"idxfields": ["status"]
}
],
"codes": [
{
"field": "resellerid",
"table": "organization",
"valuefield": "id",
"textfield": "orgname"
},
{
"field": "status",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='product_status'"
}
]
}

View File

@ -0,0 +1,106 @@
{
"summary": [
{
"name": "discount_promo_code",
"title": "促销码",
"primary": ["id"],
"catelog": "entity"
}
],
"fields": [
{
"name": "id",
"title": "促销码(即id本身)",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "marketing_id",
"title": "营销方案id(可空=纯邀请码)",
"type": "str",
"length": 32
},
{
"name": "resellerid",
"title": "所属商户机构id",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "sale_id",
"title": "生成销售id",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "code_type",
"title": "码类型(1=促销码 2=邀请码)",
"type": "char",
"length": 1,
"default": "1",
"nullable": "no"
},
{
"name": "status",
"title": "状态",
"type": "char",
"length": 1,
"default": "1"
},
{
"name": "created_at",
"title": "生成时间",
"type": "timestamp",
"default": "CURRENT_TIMESTAMP",
"nullable": "no"
}
],
"indexes": [
{
"name": "idx_dpc_reseller",
"idxtype": "index",
"idxfields": ["resellerid"]
},
{
"name": "idx_dpc_sale",
"idxtype": "index",
"idxfields": ["sale_id"]
},
{
"name": "idx_dpc_marketing",
"idxtype": "index",
"idxfields": ["marketing_id"]
}
],
"codes": [
{
"field": "resellerid",
"table": "organization",
"valuefield": "id",
"textfield": "orgname"
},
{
"field": "marketing_id",
"table": "discount_marketing",
"valuefield": "id",
"textfield": "name"
},
{
"field": "code_type",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='promo_code_type'"
},
{
"field": "status",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='product_status'"
}
]
}

View File

@ -0,0 +1,3 @@
# 客户归属 - 绑定客户(通过促销码)
result = await bind_customer(request, params_kw)
return result if isinstance(result, dict) else {'status': 'error', 'message': str(result)}

View File

@ -0,0 +1,2 @@
# 客户归属删除 - 暂无实现(归属关系永久保留)
return {'status': 'error', 'message': 'Customer binding cannot be deleted'}

View File

@ -0,0 +1,2 @@
# 客户归属更新 - 暂无实现(首次绑定后不可变更)
return {'status': 'error', 'message': 'Customer binding cannot be updated after initial binding'}

View File

@ -0,0 +1,3 @@
# 创建营销方案 - 委托给 init.py
result = await create_marketing(request, params_kw)
return result if isinstance(result, dict) else {'status': 'error', 'message': str(result)}

View File

@ -0,0 +1,3 @@
# 删除营销方案
result = await delete_marketing(request, params_kw)
return result if isinstance(result, dict) else {'status': 'error', 'message': str(result)}

View File

@ -0,0 +1,3 @@
# 更新营销方案
result = await update_marketing(request, params_kw)
return result if isinstance(result, dict) else {'status': 'error', 'message': str(result)}

View File

@ -0,0 +1,3 @@
# 生成促销码
result = await generate_promo_code(request, params_kw)
return result if isinstance(result, dict) else {'status': 'error', 'message': str(result)}

View File

@ -0,0 +1,3 @@
# 删除促销码(禁用即可,一般不物理删除)
result = await disable_promo_code(request, params_kw)
return result if isinstance(result, dict) else {'status': 'error', 'message': str(result)}

View File

@ -0,0 +1,3 @@
# 禁用促销码
result = await disable_promo_code(request, params_kw)
return result if isinstance(result, dict) else {'status': 'error', 'message': str(result)}

View File

@ -15,6 +15,21 @@
"label": "折扣产品明细",
"url": "{{entire_url('discount_detail_list.ui')}}"
},
{
"name":"discount_marketing_list",
"label": "营销方案",
"url": "{{entire_url('discount_marketing_list/index.ui')}}"
},
{
"name":"discount_promo_code_list",
"label": "促销码",
"url": "{{entire_url('discount_promo_code_list/index.ui')}}"
},
{
"name":"discount_customer_bind_list",
"label": "客户归属",
"url": "{{entire_url('discount_customer_bind_list/index.ui')}}"
},
{
"name":"promotecode",
"label": "生成促销码",