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:
parent
8498a10809
commit
f5250fb261
BIN
discount/__pycache__/init.cpython-310.pyc
Normal file
BIN
discount/__pycache__/init.cpython-310.pyc
Normal file
Binary file not shown.
379
discount/init.py
379
discount/init.py
@ -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
40
init/data.json
Normal 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": "手动分配"
|
||||
}
|
||||
]
|
||||
}
|
||||
38
json/discount_customer_bind_list.json
Normal file
38
json/discount_customer_bind_list.json
Normal 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')}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
38
json/discount_marketing_list.json
Normal file
38
json/discount_marketing_list.json
Normal 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"
|
||||
}
|
||||
}
|
||||
43
json/discount_promo_code_list.json
Normal file
43
json/discount_promo_code_list.json
Normal 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"
|
||||
}
|
||||
}
|
||||
98
models/discount_customer_bind.json
Normal file
98
models/discount_customer_bind.json
Normal 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'"
|
||||
}
|
||||
]
|
||||
}
|
||||
97
models/discount_marketing.json
Normal file
97
models/discount_marketing.json
Normal 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'"
|
||||
}
|
||||
]
|
||||
}
|
||||
106
models/discount_promo_code.json
Normal file
106
models/discount_promo_code.json
Normal 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'"
|
||||
}
|
||||
]
|
||||
}
|
||||
3
wwwroot/api/customer_bind_create.dspy
Normal file
3
wwwroot/api/customer_bind_create.dspy
Normal file
@ -0,0 +1,3 @@
|
||||
# 客户归属 - 绑定客户(通过促销码)
|
||||
result = await bind_customer(request, params_kw)
|
||||
return result if isinstance(result, dict) else {'status': 'error', 'message': str(result)}
|
||||
2
wwwroot/api/customer_bind_delete.dspy
Normal file
2
wwwroot/api/customer_bind_delete.dspy
Normal file
@ -0,0 +1,2 @@
|
||||
# 客户归属删除 - 暂无实现(归属关系永久保留)
|
||||
return {'status': 'error', 'message': 'Customer binding cannot be deleted'}
|
||||
2
wwwroot/api/customer_bind_update.dspy
Normal file
2
wwwroot/api/customer_bind_update.dspy
Normal file
@ -0,0 +1,2 @@
|
||||
# 客户归属更新 - 暂无实现(首次绑定后不可变更)
|
||||
return {'status': 'error', 'message': 'Customer binding cannot be updated after initial binding'}
|
||||
3
wwwroot/api/marketing_create.dspy
Normal file
3
wwwroot/api/marketing_create.dspy
Normal 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)}
|
||||
3
wwwroot/api/marketing_delete.dspy
Normal file
3
wwwroot/api/marketing_delete.dspy
Normal file
@ -0,0 +1,3 @@
|
||||
# 删除营销方案
|
||||
result = await delete_marketing(request, params_kw)
|
||||
return result if isinstance(result, dict) else {'status': 'error', 'message': str(result)}
|
||||
3
wwwroot/api/marketing_update.dspy
Normal file
3
wwwroot/api/marketing_update.dspy
Normal file
@ -0,0 +1,3 @@
|
||||
# 更新营销方案
|
||||
result = await update_marketing(request, params_kw)
|
||||
return result if isinstance(result, dict) else {'status': 'error', 'message': str(result)}
|
||||
3
wwwroot/api/promo_code_create.dspy
Normal file
3
wwwroot/api/promo_code_create.dspy
Normal 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)}
|
||||
3
wwwroot/api/promo_code_delete.dspy
Normal file
3
wwwroot/api/promo_code_delete.dspy
Normal 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)}
|
||||
3
wwwroot/api/promo_code_update.dspy
Normal file
3
wwwroot/api/promo_code_update.dspy
Normal 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)}
|
||||
@ -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": "生成促销码",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user