From b41c89e9dd0d5dcc997899539b66635ad8fda4d9 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Mon, 11 May 2026 11:30:57 +0800 Subject: [PATCH] bugfix --- README.md | 58 ++++- discount/__init__.py | 1 + discount/init.py | 434 ++++++++++++++++++++++++--------- json/discount_detail_list.json | 52 ++++ json/discount_list.json | 41 ++++ models/discount.xlsx | Bin 18316 -> 10661 bytes models/discount_detail.xlsx | Bin 0 -> 9003 bytes pyproject.toml | 5 +- wwwroot/generate_qr.dspy | 101 ++++---- wwwroot/menu.ui | 24 +- wwwroot/promote.ui | 13 +- 11 files changed, 543 insertions(+), 186 deletions(-) create mode 100644 discount/__init__.py create mode 100644 json/discount_detail_list.json create mode 100644 json/discount_list.json create mode 100644 models/discount_detail.xlsx diff --git a/README.md b/README.md index 6eee838..f473449 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,61 @@ # discount ## 功能 -支持商户为客户设置折扣,一旦客户设置折扣,商户的所有商品此客户均享受商户为其设置的折扣 +支持商户为客户设置折扣,可以为不同产品设置不同的折扣值。 -## +## 数据模型 +### discount (折扣表) +主折扣记录表,定义商户-客户关系的折扣。 +- id: 折扣id +- name: 折扣名称 +- resellerid: 商户id +- customerid: 客户id (可为NULL表示商户级默认折扣) +- enabled_date: 启用日期 +- expired_date: 失效日期 + +### discount_detail (折扣产品明细表) +存储每个产品的具体折扣值。 +- id: 明细id +- discountid: 关联折扣id +- resellerid: 商户id +- prodtypeid: 产品类型id (可为NULL表示所有类型) +- productid: 产品id (可为NULL表示该类型下所有产品) +- discount: 折扣值 (0 < discount < 1) + +### discount_qr (折扣二维码表) +促销二维码记录。 +- id: 二维码id +- resellerid: 商户id +- discount: 折扣值 +- valid_term: 有效期 (如 30D, 3M, 1Y) +- expired_date: 促销失效日期 +- qr_webpath: 二维码路径 + +## 折扣查询 + +### 获取产品折扣 +```python +from discount.init import get_product_discount + +discount = await get_product_discount(resellerid, customerid, prodtypeid, productid) +# 返回折扣值 (float),1.0表示无折扣 +``` + +查询优先级: +1. 精确匹配:产品类型 + 产品id +2. 类型级别匹配:产品类型 (productid为NULL) +3. 默认匹配:所有产品 (prodtypeid和productid均为NULL) + +### 获取客户默认折扣 (兼容旧接口) +```python +from discount.init import get_customer_discount + +discount = await get_customer_discount(resellerid, customerid) +# 返回客户默认折扣值 +``` + +## 维护界面 +- 折扣管理: discount_list.ui - 创建/编辑/删除折扣记录 +- 折扣产品明细: discount_detail_list.ui?discountid=xxx - 为指定折扣添加产品明细,设置各产品折扣值 +- 生成促销码: promote.ui - 生成促销二维码 diff --git a/discount/__init__.py b/discount/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/discount/__init__.py @@ -0,0 +1 @@ + diff --git a/discount/init.py b/discount/init.py index 4f6cd8e..96f8309 100644 --- a/discount/init.py +++ b/discount/init.py @@ -1,144 +1,340 @@ +from datetime import datetime, timedelta from appPublic.qr import gen_qr_withlogo from appPublic.uniqueID import getID from appPublic.jsonConfig import getConfig from appPublic.dictObject import DictObject -from sqlor.dbpools import get_sor_context +from sqlor.dbpools import DBPools from ahserver.serverenv import ServerEnv from ahserver.filestorage import FileStorage -async def discount_qrcode(request, params_kw): - """ - discount less then 1 and greate then 0 - valid_term is digit + one of ["D", "M", "Y"] - expired_date is a date after it the promote qrcode invalidable - """ - discount = params_kw.discount - valid_term = params_kw.valid_term - expired_date = params_kw.expired_date - env = request._run_ns - resellerid = await env.get_userorgid() - id = getID() - url = env.entire_url('./promote') + f'?id={id}' - fs = FileStorage() - p = fs._name2path(f'{getID()}.png') - config = getConfig() - gen_qr_withlogo(url, p, logopath=config.logopath, logoloc='cc') - webp = fs.webpath(p) - async with get_sor_context(env, 'discount') as sor: - biz_date = await env.get_business_date(sor) - if biz_date >= expired_date: - raise Exception('Promote QRCODE is out of time') - ret = { - 'id': id, - 'resellerid': resellerid, - 'discount': discount, - 'valid_term': valid_term, - 'expired_date': expired_date, - 'qr_webpath': webp - } - await sor.C('discount_qr', ret.copy()) - return DictObject(**ret) - return None +async def discount_qrcode(request, params_kw): + """ + Generate a promotional QR code for discount + discount less then 1 and greate then 0 + valid_term is digit + one of ["D", "M", "Y"] + expired_date is a date after it the promote qrcode invalidable + """ + discount = params_kw.discount + valid_term = params_kw.valid_term + expired_date = params_kw.expired_date + env = ServerEnv() + dbname = env.get_module_dbname('discount') + config = getConfig() + db = DBPools() + db.databases = config.databases + resellerid = await env.get_userorgid() + qr_id = getID() + url = env.entire_url('./promote') + f'?id={qr_id}' + fs = FileStorage() + p = fs._name2path(f'{getID()}.png') + gen_qr_withlogo(url, p, logopath=config.logopath, logoloc='cc') + webp = fs.webpath(p) + async with db.sqlorContext(dbname) as sor: + biz_date = await env.get_business_date(sor) + if biz_date >= expired_date: + raise Exception('Promote QRCODE is out of time') + + ret = { + 'id': qr_id, + 'resellerid': resellerid, + 'discount': discount, + 'valid_term': valid_term, + 'expired_date': expired_date, + 'qr_webpath': webp + } + await sor.C('discount_qr', ret.copy()) + return DictObject(**ret) + return None + async def set_promote_discount(request, params_kw): - env = request._run_ns - id = params_kw.id - customerid = await env.get_userorgid() - async with get_sor_context(env, 'discount') as sor: - recs = await sor.R('discount_qr', {'id': id}) - if not recs: - raise Exception(f'promote id({id}) not exists') - biz_date = await env.get_business_date(sor) - if recs[0].expired_date <= biz_date: - raise Exception('Promote QRCODE is out of time') - cnt = int(recs[0].valid_term[:-1]) - unit = recs[0].valid_term[-1] - enabled_date = biz_date - expired_date = '' - if unit == 'D': - expired_date = env.strdate_add(enabled_date, days=cnt) - elif unit == 'M': - expired_date = env.strdate_add(enabled_date, months=cnt) - elif unit == 'Y': - expired_date = env.strdate_add(enabled_date, years=cnt) - else: - raise Exception(f'Invalid valid_term({recs[0].valid_term})') - need_new_discount = await disable_old_discount(sor, recs[0].resellerid, - customerid, biz_date, recs[0].discount) - if not need_new_discount: - return await sor_get_customer_discount(sor, recs[0].resellerid, customerid) - ret = { - 'id': getID(), + env = ServerEnv() + dbname = env.get_module_dbname('discount') + config = getConfig() + db = DBPools() + db.databases = config.databases + id = params_kw.id + customerid = await env.get_userorgid() + async with db.sqlorContext(dbname) as sor: + recs = await sor.R('discount_qr', {'id': id}) + if not recs: + raise Exception(f'promote id({id}) not exists') + biz_date = await env.get_business_date(sor) + if recs[0].expired_date <= biz_date: + raise Exception('Promote QRCODE is out of time') + cnt = int(recs[0].valid_term[:-1]) + unit = recs[0].valid_term[-1] + enabled_date = biz_date + expired_date = '' + if unit == 'D': + expired_date = env.strdate_add(enabled_date, days=cnt) + elif unit == 'M': + expired_date = env.strdate_add(enabled_date, months=cnt) + elif unit == 'Y': + expired_date = env.strdate_add(enabled_date, years=cnt) + else: + raise Exception(f'Invalid valid_term({recs[0].valid_term})') + + # Disable old active discount for this customer + await disable_old_discount(sor, recs[0].resellerid, customerid, biz_date) + + # Create new discount record (no longer stores discount value directly) + discountid = getID() + ret = { + 'id': discountid, + 'name': f'促销折扣-{biz_date}', 'resellerid': recs[0].resellerid, 'customerid': customerid, - 'discount': recs[0].discount, 'enabled_date': enabled_date, 'expired_date': expired_date } - await sor.C('discount', ret.copy()) - return recs[0].discount - return None - -async def disable_old_discount(sor, resellerid, customerid, biz_date, new_discount): - sql = """select * from discount -where resellerid = ${resellerid}$ - and customerid=${customerid}$ - and enabled_date <= ${biz_date}$ - and expired_date > ${biz_date}$ for update""" - recs = await sor.sqlExe(sql, {'resellerid': resellerid, 'customerid': customerid, 'biz_date': biz_date}) - if not recs: - return True - if new_discount > recs[0].discount: - return False - await sor.U('discount', {'id': recs[0].id, 'expired_date': biz_date}) - return True + await sor.C('discount', ret.copy()) + + # Create discount_detail record with the discount value from QR code + # prodtypeid=None, productid=None means applies to all products + detail_ret = { + 'id': getID(), + 'discountid': discountid, + 'resellerid': recs[0].resellerid, + 'prodtypeid': None, + 'productid': None, + 'discount': recs[0].discount, + } + await sor.C('discount_detail', detail_ret.copy()) + + return recs[0].discount + return None + + +async def disable_old_discount(sor, resellerid, customerid, biz_date): + """Disable any active discount record for the given reseller+customer pair.""" + # Use sor.R with sort instead of raw SQL + FOR UPDATE (DB-agnostic) + recs = await sor.R('discount', { + 'resellerid': resellerid, + 'customerid': customerid, + 'sort': 'enabled_date desc' + }) + if not recs: + return + # Find the active one in Python (DB-agnostic date comparison) + for rec in recs: + if rec.get('enabled_date', '') <= biz_date and rec.get('expired_date', '') > biz_date: + await sor.U('discount', {'id': rec['id'], 'expired_date': biz_date}) + return + async def sor_get_star_discount(sor, resellerid, biz_date): - env = ServerEnv() - sql = """select * from discount -where resellerid = ${resellerid}$ - and enabled_date <= ${biz_date}$ - and expired_date > ${biz_date}$""" - ns = { - "resellerid": resellerid, - "biz_date": biz_date - } - recs = await sor.sqlExe(sql, ns) - if not recs: - return 1 - - return recs[0].discount + """Get default discount for a reseller (no specific customer).""" + sql = """select d.id from discount d +where d.resellerid = ${resellerid}$ + and d.customerid is NULL + and d.enabled_date <= ${biz_date}$ + and d.expired_date > ${biz_date}$""" + ns = { + "resellerid": resellerid, + "biz_date": biz_date + } + recs = await sor.sqlExe(sql, ns) + if not recs: + return 1 + + # Look up default discount detail (prodtypeid=None, productid=None) + sql2 = """select discount from discount_detail +where discountid = ${discountid}$ + and prodtypeid is NULL + and productid is NULL""" + recs2 = await sor.sqlExe(sql2, {'discountid': recs[0].id}) + if not recs2: + return 1 + return recs2[0].discount async def sor_get_customer_discount(sor, resellerid, customerid): - env = ServerEnv() - biz_date = await env.get_business_date(sor) - sql = """select * from discount + """Get discount record for a customer (legacy, returns record not value). + Use sor_get_product_discount for product-specific discount.""" + env = ServerEnv() + biz_date = await env.get_business_date(sor) + sql = """select * from discount where resellerid = ${resellerid}$ - and customerid = ${customerid}$ - and enabled_date <= ${biz_date}$ - and expired_date > ${biz_date}$""" - ns = { - "resellerid": resellerid, - "customerid": customerid, - "biz_date": biz_date - } - recs = await sor.sqlExe(sql, ns) - if not recs: - return await sor_get_star_discount(sor, resellerid, biz_date) - - return recs[0].discount + and customerid = ${customerid}$ + and enabled_date <= ${biz_date}$ + and expired_date > ${biz_date}$""" + ns = { + "resellerid": resellerid, + "customerid": customerid, + "biz_date": biz_date + } + recs = await sor.sqlExe(sql, ns) + if not recs: + return await sor_get_star_discount(sor, resellerid, biz_date) + + # Return default discount for this customer (prodtypeid=None, productid=None) + sql2 = """select discount from discount_detail +where discountid = ${discountid}$ + and prodtypeid is NULL + and productid is NULL""" + recs2 = await sor.sqlExe(sql2, {'discountid': recs[0].id}) + if not recs2: + return 1 + return recs2[0].discount + + +async def sor_get_product_discount(sor, resellerid, customerid, prodtypeid, productid): + """ + Get product-specific discount. + Lookup priority: + 1. Exact match: discountid -> (prodtypeid, productid) + 2. Type-level match: discountid -> (prodtypeid, NULL) + 3. Default match: discountid -> (NULL, NULL) + + Returns discount value (float), default 1.0 (no discount). + """ + env = ServerEnv() + biz_date = await env.get_business_date(sor) + + # Step 1: Find active discount record for reseller+customer + sql = """select id from discount +where resellerid = ${resellerid}$ + and customerid = ${customerid}$ + and enabled_date <= ${biz_date}$ + and expired_date > ${biz_date}$""" + ns = { + "resellerid": resellerid, + "customerid": customerid, + "biz_date": biz_date + } + recs = await sor.sqlExe(sql, ns) + if not recs: + # Try reseller-level default (customerid is NULL) + sql = """select id from discount +where resellerid = ${resellerid}$ + and customerid is NULL + and enabled_date <= ${biz_date}$ + and expired_date > ${biz_date}$""" + recs = await sor.sqlExe(sql, ns) + if not recs: + return 1.0 + + discountid = recs[0].id + + # Step 2: Try exact product match + sql2 = """select discount from discount_detail +where discountid = ${discountid}$ + and prodtypeid = ${prodtypeid}$ + and productid = ${productid}$""" + recs2 = await sor.sqlExe(sql2, { + 'discountid': discountid, + 'prodtypeid': prodtypeid, + 'productid': productid + }) + if recs2: + return recs2[0].discount + + # Step 3: Try product type level match (productid is NULL) + sql3 = """select discount from discount_detail +where discountid = ${discountid}$ + and prodtypeid = ${prodtypeid}$ + and productid is NULL""" + recs3 = await sor.sqlExe(sql3, { + 'discountid': discountid, + 'prodtypeid': prodtypeid, + }) + if recs3: + return recs3[0].discount + + # Step 4: Try default match (both NULL) + sql4 = """select discount from discount_detail +where discountid = ${discountid}$ + and prodtypeid is NULL + and productid is NULL""" + recs4 = await sor.sqlExe(sql4, {'discountid': discountid}) + if recs4: + return recs4[0].discount + + return 1.0 + async def get_customer_discount(resellerid, customerid): - env = ServerEnv() - async with get_sor_context(env, 'discount') as sor: - return await sor_get_customer_discount(sor, resellerid, customerid) - return 1 + """Legacy: get default discount for a customer (all products).""" + env = ServerEnv() + dbname = env.get_module_dbname('discount') + config = getConfig() + db = DBPools() + db.databases = config.databases + async with db.sqlorContext(dbname) as sor: + return await sor_get_customer_discount(sor, resellerid, customerid) + return 1 + + +async def get_product_discount(resellerid, customerid, prodtypeid, productid): + """ + Get product-specific discount. + Parameters: + resellerid: merchant ID + customerid: customer ID + prodtypeid: product type ID + productid: product ID + Returns: discount value (float), 1.0 means no discount. + """ + env = ServerEnv() + dbname = env.get_module_dbname('discount') + config = getConfig() + db = DBPools() + db.databases = config.databases + async with db.sqlorContext(dbname) as sor: + return await sor_get_product_discount(sor, resellerid, customerid, prodtypeid, productid) + return 1.0 + + +async def get_discount_details(sor, discountid): + """Get all product detail records for a given discount.""" + sql = """select * from discount_detail +where discountid = ${discountid}$ +order by prodtypeid, productid""" + recs = await sor.sqlExe(sql, {'discountid': discountid}) + return recs + + +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') + + ret = { + 'id': getID(), + 'discountid': discountid, + 'resellerid': resellerid, + 'prodtypeid': prodtypeid if prodtypeid else None, + 'productid': productid if productid else None, + 'discount': discount, + } + await sor.C('discount_detail', ret.copy()) + return ret + + +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') + + await sor.U('discount_detail', {'id': detail_id, 'discount': discount}) + + +async def delete_discount_detail(sor, detail_id): + """Delete a discount detail record.""" + await sor.D('discount_detail', {'id': detail_id}) + def load_discount(): - env = ServerEnv() - env.get_customer_discount = get_customer_discount - env.sor_get_customer_discount = sor_get_customer_discount - env.discount_qrcode = discount_qrcode - env.set_promote_discount = set_promote_discount + env = ServerEnv() + env.get_customer_discount = get_customer_discount + env.sor_get_customer_discount = sor_get_customer_discount + env.get_product_discount = get_product_discount + env.sor_get_product_discount = sor_get_product_discount + env.discount_qrcode = discount_qrcode + env.set_promote_discount = set_promote_discount + env.get_discount_details = get_discount_details + env.add_discount_detail = add_discount_detail + env.update_discount_detail = update_discount_detail + env.delete_discount_detail = delete_discount_detail diff --git a/json/discount_detail_list.json b/json/discount_detail_list.json new file mode 100644 index 0000000..75beead --- /dev/null +++ b/json/discount_detail_list.json @@ -0,0 +1,52 @@ +{ + "tblname": "discount_detail", + "alias": "discount_detail_list", + "title": "折扣产品明细", + "params": { + "sortby": ["prodtypeid", "productid"], + "browserfields": { + "exclouded": ["id", "resellerid"], + "alters": { + "discountid": { + "uitype": "code", + "dataurl": "/get_code.dspy", + "datamethod": "GET", + "dataparams": { + "dbname": "sage", + "table": "discount", + "tblvalue": "id", + "tbltext": "name" + } + }, + "prodtypeid": { + "uitype": "code", + "dataurl": "/get_code.dspy", + "datamethod": "GET", + "dataparams": { + "dbname": "sage", + "table": "prodtype", + "tblvalue": "id", + "tbltext": "name" + } + }, + "productid": { + "uitype": "code", + "dataurl": "/get_code.dspy", + "datamethod": "GET", + "dataparams": { + "dbname": "sage", + "table": "product", + "tblvalue": "id", + "tbltext": "name" + } + } + } + }, + "editexclouded": ["id", "discountid", "resellerid"], + "editable": { + "new_data_url": "{{entire_url('../api/discount_detail_create.dspy')}}", + "update_data_url": "{{entire_url('../api/discount_detail_update.dspy')}}", + "delete_data_url": "{{entire_url('../api/discount_detail_delete.dspy')}}" + } + } +} diff --git a/json/discount_list.json b/json/discount_list.json new file mode 100644 index 0000000..8f6ecd5 --- /dev/null +++ b/json/discount_list.json @@ -0,0 +1,41 @@ +{ + "tblname": "discount", + "alias": "discount_list", + "title": "折扣管理", + "params": { + "sortby": ["enabled_date desc"], + "browserfields": { + "exclouded": ["id"], + "alters": { + "resellerid": { + "uitype": "code", + "dataurl": "/get_code.dspy", + "datamethod": "GET", + "dataparams": { + "dbname": "sage", + "table": "organization", + "tblvalue": "id", + "tbltext": "orgname" + } + }, + "customerid": { + "uitype": "code", + "dataurl": "/get_code.dspy", + "datamethod": "GET", + "dataparams": { + "dbname": "sage", + "table": "organization", + "tblvalue": "id", + "tbltext": "orgname" + } + } + } + }, + "editexclouded": ["id"], + "editable": { + "new_data_url": "{{entire_url('../api/discount_create.dspy')}}", + "update_data_url": "{{entire_url('../api/discount_update.dspy')}}", + "delete_data_url": "{{entire_url('../api/discount_delete.dspy')}}" + } + } +} diff --git a/models/discount.xlsx b/models/discount.xlsx index 38aaca9861719a15a6c99310ebf0a49f11e83ce6..769b715c5186e3f153a7bd2c0935f46b6b0c4997 100644 GIT binary patch literal 10661 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
`U1NEYdMw# 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

OIygQHgHQssd&s?6v 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 MrAlK({_F1l0IMbpNB{r; literal 18316 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 xr(Wav8}|2n>Lh$G4{{S%89pL}~ diff --git a/models/discount_detail.xlsx b/models/discount_detail.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..4bb6b19f82173c8497c3cc80e1cb66af2461be95 GIT binary patch literal 9003 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 AK>z>% literal 0 HcmV?d00001 diff --git a/pyproject.toml b/pyproject.toml index 95efe10..ebd79be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,9 +12,8 @@ authors = [ readme = "README.md" requires-python = ">=3.10" dependencies = [ - "apppublic", - "sqlor", - "ahserver" + "sqlor", + "bricks_for_python" ] [tool.setuptools] diff --git a/wwwroot/generate_qr.dspy b/wwwroot/generate_qr.dspy index 6e1b647..e64760d 100644 --- a/wwwroot/generate_qr.dspy +++ b/wwwroot/generate_qr.dspy @@ -1,58 +1,63 @@ +from ahserver.serverenv import ServerEnv + params_kw.discount = float(params_kw.discount) if params_kw.discount <= 0 or params_kw.discount >= 1: - e = Exception(f'discount({params_kw.discount}) invalid') - exception(f'{e}') - raise e + e = Exception(f'discount({params_kw.discount}) invalid') + exception(f'{e}') + raise e if params_kw.valid_term[-1] not in ['D', 'M', 'Y']: - e = Exception(f'valid_term must ends with "D", "M" or "Y"') - exception(f'{e}') - raise e + e = Exception(f'valid_term must ends with "D", "M" or "Y"') + exception(f'{e}') + raise e cnt = int(params_kw.valid_term[:-1]) if cnt < 0: - e = Exception(f'valid_term({params_kw.valid_term}) invalid') - exception(f'{e}') - raise e + e = Exception(f'valid_term({params_kw.valid_term}) invalid') + exception(f'{e}') + raise e if not params_kw.expired_date: - params_kw.expired_date = '9999-12-31' + params_kw.expired_date = '9999-12-31' x = await discount_qrcode(request, params_kw) -return { - "widgettype": "VBox", - "options": { - "width": "100%", - "height": "100%" - }, - "subwidgets": [ - { - "widgettype": "Image", - "options": { - "width": "340px", - "height": "340px", - "url": entire_url('/idfile') + f'?path={x.qr_webpath}' - } - }, { - "widgettype": "HBox", - "options": { - "width": "100%", - "cheight": 2 - }, - "subwidgets": [ - { - "widgettype": "Text", - "options": { - "otext": "折扣:", - "i18n": True, - "cwidth": 3 - } - },{ - "widgettype": "Text", - "options":{ - "text": x.discount - } - } - ] - } - ] -} +env = ServerEnv() +qr_url = env.entire_url('/idfile') + f'?path={x.qr_webpath}' + +return { + "widgettype": "VBox", + "options": { + "width": "100%", + "height": "100%" + }, + "subwidgets": [ + { + "widgettype": "Image", + "options": { + "width": "340px", + "height": "340px", + "url": qr_url + } + }, { + "widgettype": "HBox", + "options": { + "width": "100%", + "cheight": 2 + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "otext": "折扣:", + "i18n": True, + "cwidth": 3 + } + },{ + "widgettype": "Text", + "options":{ + "text": x.discount + } + } + ] + } + ] +} diff --git a/wwwroot/menu.ui b/wwwroot/menu.ui index 39ec706..548c069 100644 --- a/wwwroot/menu.ui +++ b/wwwroot/menu.ui @@ -5,11 +5,21 @@ "target":"root.page_center", "cwidth":10, "items":[ - { - "name":"promotecode", - "label": "生成促销码", - "url": "{{entire_url('promote.ui')}}", - } - ] - } + { + "name":"discount_list", + "label": "折扣管理", + "url": "{{entire_url('discount_list.ui')}}" + }, + { + "name":"discount_detail_list", + "label": "折扣产品明细", + "url": "{{entire_url('discount_detail_list.ui')}}" + }, + { + "name":"promotecode", + "label": "生成促销码", + "url": "{{entire_url('promote.ui')}}" + } + ] + } } diff --git a/wwwroot/promote.ui b/wwwroot/promote.ui index 6d91df8..7c2a0e8 100644 --- a/wwwroot/promote.ui +++ b/wwwroot/promote.ui @@ -3,6 +3,8 @@ "options": { "width": "100%", "height": "100%", + "url": "{{entire_url('generate_qr.dspy')}}", + "method": "POST", "fields": [ { "name": "valid_term", @@ -16,7 +18,7 @@ "tip": "促销二维码需在此日期前使用" },{ "name": "discount", - "lable": "折扣", + "label": "折扣", "uitype": "float", "tip": "折扣>0, < 1, 最终价格=产品价格*折扣, 折扣越小,折扣力度越大" } @@ -25,13 +27,10 @@ "binds":[ { "wid": "self", - "event": "submit", - "actiontype": "urlwidget", + "event": "submited", + "actiontype": "script", "target": "self", - "options": { - "url": "{{entire_url('generate_qr.dspy')}}", - "params": {} - } + "script": "await bricks.show_resp_message_or_error(event.params)" } ] }