From 6c8d23b9865dbd6db8d6a5e70962efa7e4131d2f Mon Sep 17 00:00:00 2001 From: yumoqing Date: Thu, 9 Jul 2026 13:39:10 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20resellerid=20=E2=86=92=20auto=20fro?= =?UTF-8?q?m=20get=5Fuserorgid(),=20remove=20from=20codes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- discount/init.py | 2 +- discount/init.py.bak | 1022 +++++++++++++++++++++++++ discount/init.py.new | 11 + json/discount_customer_bind_list.json | 66 +- json/discount_list.json | 78 +- json/discount_marketing_list.json | 124 +-- json/discount_promo_code_list.json | 126 +-- json/reseller_discount_tier.json | 63 +- models/discount.json | 118 ++- models/discount_customer_bind.json | 197 ++--- models/discount_detail.json | 184 ++--- models/discount_marketing.json | 188 ++--- models/discount_promo_code.json | 208 ++--- models/reseller_discount_tier.json | 226 +++--- 14 files changed, 1834 insertions(+), 779 deletions(-) create mode 100644 discount/init.py.bak create mode 100644 discount/init.py.new diff --git a/discount/init.py b/discount/init.py index 37a6270..55a1bdf 100644 --- a/discount/init.py +++ b/discount/init.py @@ -49,7 +49,7 @@ async def discount_qrcode(request, params_kw): async def set_promote_discount(request, params_kw): - env = ServerEnv() + env = request._run_ns dbname = env.get_module_dbname('discount') config = getConfig() db = DBPools() diff --git a/discount/init.py.bak b/discount/init.py.bak new file mode 100644 index 0000000..37a6270 --- /dev/null +++ b/discount/init.py.bak @@ -0,0 +1,1022 @@ +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 DBPools +from ahserver.serverenv import ServerEnv +from ahserver.filestorage import FileStorage + + +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 = request._run_ns + 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 = 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, + 'enabled_date': enabled_date, + 'expired_date': expired_date + } + 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 _discount_detail_exists(sor): + """Check if discount_detail table exists.""" + try: + await sor.sqlExe("SELECT 1 FROM discount_detail LIMIT 0", {}) + return True + except Exception: + return False + + +async def sor_get_star_discount(sor, resellerid, biz_date): + """Get default discount for a reseller (no specific customer).""" + sql = """select d.id, d.discount 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 + + discountid = recs[0].id + # If discount_detail table exists, look up default detail + if await _discount_detail_exists(sor): + sql2 = """select discount from discount_detail +where discountid = ${discountid}$ + and prodtypeid is NULL + and productid is NULL""" + recs2 = await sor.sqlExe(sql2, {'discountid': discountid}) + if not recs2: + return 1 + return recs2[0].discount + # Fallback: read discount value directly from discount table + return recs[0].discount if recs[0].discount is not None else 1 + + +async def sor_get_customer_discount(sor, resellerid, customerid): + """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 default discount for this customer (prodtypeid=None, productid=None) + discountid = recs[0].id + if await _discount_detail_exists(sor): + sql2 = """select discount from discount_detail +where discountid = ${discountid}$ + and prodtypeid is NULL + and productid is NULL""" + recs2 = await sor.sqlExe(sql2, {'discountid': discountid}) + if not recs2: + return 1 + return recs2[0].discount + # Fallback: read discount value directly from discount table + return recs[0].discount if recs[0].discount is not None else 1 + + +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 + + # If discount_detail table doesn't exist, fall back to discount.discount + if not await _discount_detail_exists(sor): + return recs[0].discount if recs[0].discount is not None else 1.0 + + # 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 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. + + Args: + sor: sqlor context + product_id: product.id + resellerid: discount.resellerid + customerid: discount.customerid (user/supplier/distributor org ID) + exact_match: if True, only match exact customerid (no NULL/'*' fallback). + Use True for suppliers/distributors, False (default) for customers. + """ + env = ServerEnv() + biz_date = await env.get_business_date(sor) + + if exact_match: + 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 +JOIN discount d ON dd.discountid = d.id +WHERE d.resellerid = ${{resellerid}}$ + AND {customer_cond} + AND d.enabled_date <= ${{biz_date}}$ + AND d.expired_date > ${{biz_date}}$ + AND dd.productid = ${{product_id}}$""" + ns = { + 'resellerid': resellerid, + 'customerid': customerid, + 'biz_date': biz_date, + 'product_id': product_id, + } + 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) + return 1.0 + + +async def get_min_product_discount(product_id, resellerid, customerid): + """Get the minimum discount for a specific product (convenience wrapper).""" + 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_min_product_discount(sor, product_id, resellerid, customerid) + return 1.0 + + +async def get_min_supplier_discount(product_id, resellerid, supplierid): + """Get the minimum discount for a supplier on a specific product. + + No wildcard/NULL fallback — exact supplier match only. + """ + 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_min_product_discount(sor, product_id, resellerid, supplierid, exact_match=True) + return 1.0 + + +async def get_min_distributor_discount(product_id, resellerid, distributorid): + """Get the minimum discount for a distributor on a specific product. + + No wildcard/NULL fallback — exact distributor match only. + """ + 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_min_product_discount(sor, product_id, resellerid, distributorid, exact_match=True) + return 1.0 + + +async def get_customer_discount(resellerid, customerid): + """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 (inclusive)') + + 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 (inclusive)') + + 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}) + + +# ─── 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 = request._run_ns + 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 generate_promo_qr(request, params_kw): + """Generate QR code for a promo code.""" + env = request._run_ns + dbname = env.get_module_dbname('discount') + promo_id = params_kw.get('id', '') + + if not promo_id: + return {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Missing promo code id', 'type': 'error'}} + + from appPublic.qr import gen_qr_withlogo + from appPublic.jsonConfig import getConfig + from ahserver.filestorage import FileStorage + from appPublic.uniqueID import getID as _getID + + async with DBPools().sqlorContext(dbname) as sor: + recs = await sor.R('discount_promo_code', {'id': promo_id}) + if not recs: + return {'widgettype': 'Message', 'options': {'title': 'Error', 'message': f'Promo code {promo_id} not found', 'type': 'error'}} + + config = getConfig() + qr_url = env.entire_url('./promote/index.ui') + f'?code={promo_id}' + fs = FileStorage() + p = fs._name2path(f'{_getID()}.png') + gen_qr_withlogo(qr_url, p, logopath=config.logopath, logoloc='cc') + webp = fs.webpath(p) + qr_file_url = env.entire_url('/idfile') + f'?path={webp}' + + return { + "widgettype": "VBox", + "options": {"width": "100%", "height": "100%", "spacing": 10}, + "subwidgets": [ + {"widgettype": "Image", "options": {"width": "340px", "height": "340px", "url": qr_file_url}}, + {"widgettype": "HBox", "options": {"width": "100%", "cheight": 2}, + "subwidgets": [ + {"widgettype": "Text", "options": {"otext": "促销码:", "i18n": True, "cwidth": 5}}, + {"widgettype": "Text", "options": {"text": promo_id}} + ]}, + {"widgettype": "HBox", "options": {"width": "100%", "cheight": 2}, + "subwidgets": [ + {"widgettype": "Text", "options": {"otext": "链接:", "i18n": True, "cwidth": 5}}, + {"widgettype": "Text", "options": {"text": qr_url, "css": "text-muted"}} + ]} + ] + } + + +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 = request._run_ns + 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} + + +async def sor_before_insert_discount_promo_code(env, sor, ns): + """CRUD hook: auto-set sale_id from logged-in user on insert.""" + from appPublic.uniqueID import getID as _getID + ns['id'] = ns.get('id') or _getID() + ns['sale_id'] = ns.get('sale_id') or await env.get_userid() + ns['resellerid'] = ns.get('resellerid') or await env.get_userorgid() + + +def load_discount(): + env = ServerEnv() + # before_insert hooks + env.sor_before_insert_discount_promo_code = sor_before_insert_discount_promo_code + # existing registrations... + 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.sor_get_min_product_discount = sor_get_min_product_discount + env.get_min_product_discount = get_min_product_discount + env.get_min_supplier_discount = get_min_supplier_discount + env.get_min_distributor_discount = get_min_distributor_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 + # 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.generate_promo_qr = generate_promo_qr + env.bind_customer = bind_customer + env.get_customer_bind = get_customer_bind + env.assign_customer_to_sale = assign_customer_to_sale + # Discount setting products list + env.get_discount_setting_products = get_discount_setting_products + env.get_discount_info = get_discount_info + # Marketing plan discount setting + env.get_marketing_discount_products = get_marketing_discount_products + env.get_marketing_info = get_marketing_info + + +async def get_discount_setting_products(request): + """Get products with their discount details for discount_setting page. + Used in Jinja2 template: {% set products = get_discount_setting_products(request) %} + """ + env = request._run_ns + discountid = (request._run_ns.params_kw or {}).get('discountid', '') + user_orgid = await env.get_userorgid() + + if not discountid or not user_orgid: + return [] + + dbname = env.get_module_dbname('discount') + db = DBPools() + config = getConfig() + db.databases = config.databases + + # 查询基准折扣方案ID (customerid='*', 全部用户适用) + base_sql = "SELECT id FROM discount WHERE customerid='*' AND resellerid=${org_id}$ LIMIT 1" + base_ns = {'org_id': user_orgid} + + sql = """ + SELECT + p.id as productid, + p.product_code, + p.product_name, + p.category_id, + pc.name as category_name, + dd.id as detail_id, + COALESCE(dd.discount, base_dd.discount) as discount, + dd.discountid, + CASE WHEN dd.id IS NOT NULL THEN 1 ELSE 0 END as in_plan + FROM product p + LEFT JOIN product_category pc ON p.category_id = pc.id + LEFT JOIN discount_detail dd ON dd.productid = p.id + AND dd.discountid = ${discountid}$ + LEFT JOIN discount_detail base_dd ON base_dd.productid = p.id + AND base_dd.discountid = ${base_discountid}$ + WHERE p.org_id = ${org_id}$ AND p.status = '1' + """ + ns = {'discountid': discountid, 'org_id': user_orgid} + + async with db.sqlorContext(dbname) as sor: + # 查询基准折扣方案ID, 若不存在则降级(没有基准折扣) + base_recs = await sor.sqlExe(base_sql, base_ns) + base_discountid = base_recs[0].id if base_recs else None + if base_discountid: + ns['base_discountid'] = base_discountid + else: + # 无基准方案时, 用discountid自身代替, 保证COALESCE正常(只会取dd.discount, base_dd为NULL) + ns['base_discountid'] = discountid + + recs = await sor.sqlExe(sql, ns) + # 为每个产品附加 in_plan 标记 + return recs if recs else [] + + +async def get_discount_info(request): + """Get discount name and customer name for display header.""" + env = request._run_ns + discountid = (request._run_ns.params_kw or {}).get('discountid', '') + if not discountid: + return {} + dbname = env.get_module_dbname('discount') + db = DBPools() + config = getConfig() + db.databases = config.databases + sql = """ + SELECT dm.name, o.orgname + FROM discount_marketing dm + LEFT JOIN organization o ON dm.resellerid = o.id + WHERE dm.id = ${discountid}$ + """ + async with db.sqlorContext(dbname) as sor: + recs = await sor.sqlExe(sql, {'discountid': discountid}) + if recs: + return {'name': recs[0].name or '', 'customer': recs[0].orgname or ''} + return {} + + +async def get_marketing_discount_products(request): + """Get products with base (all-user) discount and marketing-specific discount. + Used in Jinja2 template: {% set products = get_marketing_discount_products(request) %} + """ + env = request._run_ns + discountid = (request._run_ns.params_kw or {}).get('discountid', '') + user_orgid = await env.get_userorgid() + if not discountid or not user_orgid: + return [] + dbname = env.get_module_dbname('discount') + db = DBPools() + config = getConfig() + db.databases = config.databases + sql = """ + SELECT + p.id as productid, + p.product_code, + p.product_name, + p.category_id, + pc.name as category_name, + base.discount as base_discount, + mkt.id as detail_id, + mkt.discount as marketing_discount, + CASE WHEN mkt.id IS NOT NULL THEN '1' ELSE '0' END as in_marketing + FROM product p + LEFT JOIN product_category pc ON p.category_id = pc.id + LEFT JOIN ( + SELECT dd.productid, dd.discount + FROM discount_detail dd + JOIN discount d ON dd.discountid = d.id + WHERE d.resellerid = ${org_id}$ AND (d.customerid IS NULL OR d.customerid = '*') + ) base ON base.productid = p.id + LEFT JOIN discount_detail mkt ON mkt.productid = p.id AND mkt.discountid = ${discountid}$ + WHERE p.org_id = ${org_id}$ AND p.status = '1' + ORDER BY pc.name, p.product_name + """ + ns = {'discountid': discountid, 'org_id': user_orgid} + async with db.sqlorContext(dbname) as sor: + recs = await sor.sqlExe(sql, ns) + return recs if recs else [] + + +async def get_marketing_info(request): + """Get marketing plan name for display header.""" + env = request._run_ns + discountid = (request._run_ns.params_kw or {}).get('discountid', '') + if not discountid: + return {} + dbname = env.get_module_dbname('discount') + db = DBPools() + config = getConfig() + db.databases = config.databases + sql = "SELECT name FROM discount_marketing WHERE id = ${discountid}$" + async with db.sqlorContext(dbname) as sor: + recs = await sor.sqlExe(sql, {'discountid': discountid}) + if recs: + return {'name': recs[0].name or ''} + return {} diff --git a/discount/init.py.new b/discount/init.py.new new file mode 100644 index 0000000..f0e4f9e --- /dev/null +++ b/discount/init.py.new @@ -0,0 +1,11 @@ +import asyncio, json, os, re, traceback, uuid +from datetime import datetime, timedelta +import yaml +from appPublic.log import exception, debug, info, critical +from appPublic.Singleton import SingletonDecorator +from appPublic.uniqueID import getID +from appPublic.dictObject import DictObject +from appPublic.jsonConfig import getConfig +from appPublic.timeUtils import timestampstr +from sqlor.dbpools import DBPools, get_sor_context +from ahserver.serverenv import ServerEnv diff --git a/json/discount_customer_bind_list.json b/json/discount_customer_bind_list.json index 07931fc..b50b149 100644 --- a/json/discount_customer_bind_list.json +++ b/json/discount_customer_bind_list.json @@ -1,34 +1,34 @@ { - "tblname": "discount_customer_bind", - "alias": "discount_customer_bind_list", - "title": "客户归属", - "params": { - "browserfields": { - "sale_id": { - "title": "归属销售", - "width": 120 - }, - "promo_code_id": { - "title": "促销码", - "width": 150 - }, - "bind_type": { - "title": "绑定方式", - "width": 100 - }, - "created_at": { - "title": "绑定时间", - "width": 160 - } - }, - "editexclouded": [ - "customerid", - "resellerid" - ], - "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')}}" - } - } -} + "tblname": "discount_customer_bind", + "alias": "discount_customer_bind_list", + "title": "客户归属", + "params": { + "browserfields": { + "sale_id": { + "title": "归属销售", + "width": 120 + }, + "promo_code_id": { + "title": "促销码", + "width": 150 + }, + "bind_type": { + "title": "绑定方式", + "width": 100 + }, + "created_at": { + "title": "绑定时间", + "width": 160 + } + }, + "editexclouded": [ + "customerid", + "resellerid" + ], + "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')}}" + } + } +} \ No newline at end of file diff --git a/json/discount_list.json b/json/discount_list.json index 3c69fe1..21012d4 100644 --- a/json/discount_list.json +++ b/json/discount_list.json @@ -1,40 +1,40 @@ { - "tblname": "discount", - "alias": "discount_list", - "title": "折扣管理", - "params": { - "sortby": [ - "enabled_date desc" - ], - "browserfields": { - "exclouded": [ - "id", - "resellerid" - ], - "alters": { - "customerid": { - "uitype": "code", - "dataurl": "{{entire_url('../api/get_customer_list.dspy')}}", - "datamethod": "GET" - } - } - }, - "editexclouded": [ - "id", - "resellerid" - ], - "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')}}" - }, - "logined_userorgid": "resellerid", - "subtables": [ - { - "field": "discountid", - "title": "设置产品折扣", - "subtable": "discount_setting" - } - ] - } -} + "tblname": "discount", + "alias": "discount_list", + "title": "折扣管理", + "params": { + "sortby": [ + "enabled_date desc" + ], + "browserfields": { + "exclouded": [ + "id", + "resellerid" + ], + "alters": { + "customerid": { + "uitype": "code", + "dataurl": "{{entire_url('../api/get_customer_list.dspy')}}", + "datamethod": "GET" + } + } + }, + "editexclouded": [ + "id", + "resellerid" + ], + "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')}}" + }, + "logined_userorgid": "resellerid", + "subtables": [ + { + "field": "discountid", + "title": "设置产品折扣", + "subtable": "discount_setting" + } + ] + } +} \ No newline at end of file diff --git a/json/discount_marketing_list.json b/json/discount_marketing_list.json index ce382ef..f9c989b 100644 --- a/json/discount_marketing_list.json +++ b/json/discount_marketing_list.json @@ -1,64 +1,64 @@ { - "tblname": "discount_marketing", - "alias": "discount_marketing_list", - "title": "营销方案", - "params": { - "browserfields": { - "name": { - "title": "方案名称" - }, - "enabled_date": { - "title": "启用日期", - "width": 120 - }, - "expired_date": { - "title": "失效日期", - "width": 120 - }, - "status": { - "title": "状态", - "width": 80 - } - }, - "editexclouded": [ - "id", - "resellerid", - "created_by", - "created_at", - "updated_at" - ], - "toolbar": { - "tools": [ - { - "name": "detail", - "selected_row": true, - "label": "产品明细", - "icon": "{{entire_url('/bricks/imgs/list.svg')}}" - } - ] - }, - "binds": [ - { - "wid": "self", - "event": "detail", - "actiontype": "urlwidget", - "target": "app.sage_main_content", - "params_mapping": { - "mapping": { - "id": "discountid" - }, - "need_other": false - }, - "options": { - "url": "{{entire_url('../marketing_discount_setting')}}" - } - } - ], - "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" - } + "tblname": "discount_marketing", + "alias": "discount_marketing_list", + "title": "营销方案", + "params": { + "browserfields": { + "name": { + "title": "方案名称" + }, + "enabled_date": { + "title": "启用日期", + "width": 120 + }, + "expired_date": { + "title": "失效日期", + "width": 120 + }, + "status": { + "title": "状态", + "width": 80 + } + }, + "editexclouded": [ + "id", + "resellerid", + "created_by", + "created_at", + "updated_at" + ], + "toolbar": { + "tools": [ + { + "name": "detail", + "selected_row": true, + "label": "产品明细", + "icon": "{{entire_url('/bricks/imgs/list.svg')}}" + } + ] + }, + "binds": [ + { + "wid": "self", + "event": "detail", + "actiontype": "urlwidget", + "target": "app.sage_main_content", + "params_mapping": { + "mapping": { + "id": "discountid" + }, + "need_other": false + }, + "options": { + "url": "{{entire_url('../marketing_discount_setting')}}" + } + } + ], + "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" + } } \ No newline at end of file diff --git a/json/discount_promo_code_list.json b/json/discount_promo_code_list.json index 09d2e91..eab9cbd 100644 --- a/json/discount_promo_code_list.json +++ b/json/discount_promo_code_list.json @@ -1,63 +1,65 @@ { - "tblname": "discount_promo_code", - "alias": "discount_promo_code_list", - "title": "促销码", - "params": { - "browserfields": { - "exclouded": ["id"], - "marketing_id": { - "title": "营销方案", - "width": 150 - }, - "code_type": { - "title": "类型", - "width": 80 - }, - "status": { - "title": "状态", - "width": 80 - }, - "created_at": { - "title": "生成时间", - "width": 160 - } - }, - "editexclouded": [ - "id", - "resellerid", - "sale_id", - "created_at" - ], - "toolbar": { - "tools": [ - { - "name": "generate_qr", - "label": "生成二维码", - "selected_row": true - } - ] - }, - "binds": [ - { - "wid": "self", - "event": "generate_qr", - "actiontype": "urlwidget", - "target": "PopupWindow", - "popup_options": { - "title": "促销码二维码", - "cwidth": 18, - "cheight": 22 - }, - "options": { - "url": "{{entire_url('../api/promo_code_qr.dspy')}}?id=${id}$" - } - } - ], - "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" - } -} + "tblname": "discount_promo_code", + "alias": "discount_promo_code_list", + "title": "促销码", + "params": { + "browserfields": { + "exclouded": [ + "id" + ], + "marketing_id": { + "title": "营销方案", + "width": 150 + }, + "code_type": { + "title": "类型", + "width": 80 + }, + "status": { + "title": "状态", + "width": 80 + }, + "created_at": { + "title": "生成时间", + "width": 160 + } + }, + "editexclouded": [ + "id", + "resellerid", + "sale_id", + "created_at" + ], + "toolbar": { + "tools": [ + { + "name": "generate_qr", + "label": "生成二维码", + "selected_row": true + } + ] + }, + "binds": [ + { + "wid": "self", + "event": "generate_qr", + "actiontype": "urlwidget", + "target": "PopupWindow", + "popup_options": { + "title": "促销码二维码", + "cwidth": 18, + "cheight": 22 + }, + "options": { + "url": "{{entire_url('../api/promo_code_qr.dspy')}}?id=${id}$" + } + } + ], + "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" + } +} \ No newline at end of file diff --git a/json/reseller_discount_tier.json b/json/reseller_discount_tier.json index a33880f..bbd0a74 100644 --- a/json/reseller_discount_tier.json +++ b/json/reseller_discount_tier.json @@ -1,26 +1,39 @@ { - "tblname": "reseller_discount_tier", - "title": "阶梯折扣管理", - "params": { - "sortby": "sort_order asc", - "logined_userorgid": "resellerid", - "browserfields": { - "exclouded": ["id"], - "alters": { - "status": { - "uitype": "code", - "data": [ - {"value": "active", "text": "启用"}, - {"value": "inactive", "text": "停用"} - ] - } - } - }, - "editexclouded": ["id", "created_at", "updated_at"], - "editable": { - "new_data_url": "{{entire_url('../api/reseller_discount_tier_create.dspy')}}", - "update_data_url": "{{entire_url('../api/reseller_discount_tier_update.dspy')}}", - "delete_data_url": "{{entire_url('../api/reseller_discount_tier_delete.dspy')}}" - } - } -} + "tblname": "reseller_discount_tier", + "title": "阶梯折扣管理", + "params": { + "sortby": "sort_order asc", + "logined_userorgid": "resellerid", + "browserfields": { + "exclouded": [ + "id" + ], + "alters": { + "status": { + "uitype": "code", + "data": [ + { + "value": "active", + "text": "启用" + }, + { + "value": "inactive", + "text": "停用" + } + ] + } + } + }, + "editexclouded": [ + "id", + "created_at", + "updated_at", + "resellerid" + ], + "editable": { + "new_data_url": "{{entire_url('../api/reseller_discount_tier_create.dspy')}}", + "update_data_url": "{{entire_url('../api/reseller_discount_tier_update.dspy')}}", + "delete_data_url": "{{entire_url('../api/reseller_discount_tier_delete.dspy')}}" + } + } +} \ No newline at end of file diff --git a/models/discount.json b/models/discount.json index ecd6a2f..749602b 100644 --- a/models/discount.json +++ b/models/discount.json @@ -1,62 +1,58 @@ { - "summary": [ - { - "name": "discount", - "title": "折扣表", - "primary": ["id"], - "catelog": "entity" - } - ], - "fields": [ - { - "name": "id", - "title": "id", - "type": "str", - "length": 32 - }, - { - "name": "name", - "title": "折扣名称", - "type": "str", - "length": 100 - }, - { - "name": "resellerid", - "title": "商户id", - "type": "str", - "length": 32, - "default": "*" - }, - { - "name": "customerid", - "title": "客户id(*表示全部客户)", - "type": "str", - "length": 32, - "default": "*" - }, - { - "name": "enabled_date", - "title": "启用日期", - "type": "date" - }, - { - "name": "expired_date", - "title": "失效日期", - "type": "date" - } - ], - "codes": [ - { - "field": "resellerid", - "table": "organization", - "valuefield": "id", - "textfield": "orgname" - }, - { - "field": "customerid", - "table": "organization", - "valuefield": "id", - "textfield": "orgname" - } - ] -} + "summary": [ + { + "name": "discount", + "title": "折扣表", + "primary": [ + "id" + ], + "catelog": "entity" + } + ], + "fields": [ + { + "name": "id", + "title": "id", + "type": "str", + "length": 32 + }, + { + "name": "name", + "title": "折扣名称", + "type": "str", + "length": 100 + }, + { + "name": "resellerid", + "title": "商户id", + "type": "str", + "length": 32, + "default": "*" + }, + { + "name": "customerid", + "title": "客户id(*表示全部客户)", + "type": "str", + "length": 32, + "default": "*" + }, + { + "name": "enabled_date", + "title": "启用日期", + "type": "date" + }, + { + "name": "expired_date", + "title": "失效日期", + "type": "date" + } + ], + "codes": [ + { + "field": "customerid", + "table": "organization", + "valuefield": "id", + "textfield": "orgname" + } + ] +} \ No newline at end of file diff --git a/models/discount_customer_bind.json b/models/discount_customer_bind.json index c841102..7a94cb7 100644 --- a/models/discount_customer_bind.json +++ b/models/discount_customer_bind.json @@ -1,97 +1,102 @@ { - "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=手动 3=域名注册)", - "type": "char", - "length": 1, - "default": "1", - "nullable": "no" - }, - { - "name": "created_at", - "title": "绑定时间", - "type": "timestamp", - "nullable": "no" - } - ], - "indexes": [ - { - "name": "idx_dcb_customer_reseller", - "idxtype": "unique", - "idxfields": ["customerid", "resellerid"] - }, - { - "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'" - } - ] -} + "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=手动 3=域名注册)", + "type": "char", + "length": 1, + "default": "1", + "nullable": "no" + }, + { + "name": "created_at", + "title": "绑定时间", + "type": "timestamp", + "nullable": "no" + } + ], + "indexes": [ + { + "name": "idx_dcb_customer_reseller", + "idxtype": "unique", + "idxfields": [ + "customerid", + "resellerid" + ] + }, + { + "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": "bind_type", + "table": "appcodes_kv", + "valuefield": "k", + "textfield": "v", + "cond": "parentid='bind_type'" + } + ] +} \ No newline at end of file diff --git a/models/discount_detail.json b/models/discount_detail.json index b6d5141..4c2d233 100644 --- a/models/discount_detail.json +++ b/models/discount_detail.json @@ -1,92 +1,94 @@ { - "summary": [ - { - "name": "discount_detail", - "title": "折扣明细表", - "primary": ["id"], - "catelog": "entity" - } - ], - "fields": [ - { - "name": "id", - "title": "id", - "type": "str", - "length": 32, - "nullable": "no" - }, - { - "name": "discountid", - "title": "折扣方案id", - "type": "str", - "length": 32, - "nullable": "no" - }, - { - "name": "resellerid", - "title": "商户id", - "type": "str", - "length": 32, - "nullable": "no" - }, - { - "name": "prodtypeid", - "title": "产品种类id", - "type": "str", - "length": 32 - }, - { - "name": "productid", - "title": "产品id", - "type": "str", - "length": 32 - }, - { - "name": "discount", - "title": "折扣", - "type": "double", - "length": 5, - "dec": 4, - "nullable": "no" - } - ], - "indexes": [ - { - "name": "idx_dd_discountid", - "idxtype": "index", - "idxfields": ["discountid"] - }, - { - "name": "idx_dd_product", - "idxtype": "index", - "idxfields": ["discountid", "prodtypeid", "productid"] - } - ], - "codes": [ - { - "field": "discountid", - "table": "discount", - "valuefield": "id", - "textfield": "name" - }, - { - "field": "resellerid", - "table": "organization", - "valuefield": "id", - "textfield": "orgname" - }, - { - "field": "prodtypeid", - "table": "product_category", - "valuefield": "id", - "textfield": "name" - }, - { - "field": "productid", - "table": "product", - "valuefield": "id", - "textfield": "product_name", - "cond": "category_id=[[prodtypeid]]" - } - ] -} + "summary": [ + { + "name": "discount_detail", + "title": "折扣明细表", + "primary": [ + "id" + ], + "catelog": "entity" + } + ], + "fields": [ + { + "name": "id", + "title": "id", + "type": "str", + "length": 32, + "nullable": "no" + }, + { + "name": "discountid", + "title": "折扣方案id", + "type": "str", + "length": 32, + "nullable": "no" + }, + { + "name": "resellerid", + "title": "商户id", + "type": "str", + "length": 32, + "nullable": "no" + }, + { + "name": "prodtypeid", + "title": "产品种类id", + "type": "str", + "length": 32 + }, + { + "name": "productid", + "title": "产品id", + "type": "str", + "length": 32 + }, + { + "name": "discount", + "title": "折扣", + "type": "double", + "length": 5, + "dec": 4, + "nullable": "no" + } + ], + "indexes": [ + { + "name": "idx_dd_discountid", + "idxtype": "index", + "idxfields": [ + "discountid" + ] + }, + { + "name": "idx_dd_product", + "idxtype": "index", + "idxfields": [ + "discountid", + "prodtypeid", + "productid" + ] + } + ], + "codes": [ + { + "field": "discountid", + "table": "discount", + "valuefield": "id", + "textfield": "name" + }, + { + "field": "prodtypeid", + "table": "product_category", + "valuefield": "id", + "textfield": "name" + }, + { + "field": "productid", + "table": "product", + "valuefield": "id", + "textfield": "product_name", + "cond": "category_id=[[prodtypeid]]" + } + ] +} \ No newline at end of file diff --git a/models/discount_marketing.json b/models/discount_marketing.json index 8da00f9..3f860c9 100644 --- a/models/discount_marketing.json +++ b/models/discount_marketing.json @@ -1,95 +1,95 @@ { - "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", - "nullable": "no" - }, - { - "name": "updated_at", - "title": "更新时间", - "type": "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'" - } - ] -} + "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", + "nullable": "no" + }, + { + "name": "updated_at", + "title": "更新时间", + "type": "timestamp", + "nullable": "no" + } + ], + "indexes": [ + { + "name": "idx_dm_reseller", + "idxtype": "index", + "idxfields": [ + "resellerid" + ] + }, + { + "name": "idx_dm_status", + "idxtype": "index", + "idxfields": [ + "status" + ] + } + ], + "codes": [ + { + "field": "status", + "table": "appcodes_kv", + "valuefield": "k", + "textfield": "v", + "cond": "parentid='product_status'" + } + ] +} \ No newline at end of file diff --git a/models/discount_promo_code.json b/models/discount_promo_code.json index d7c6b15..6968bdd 100644 --- a/models/discount_promo_code.json +++ b/models/discount_promo_code.json @@ -1,104 +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 - }, - { - "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", - "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'" - } - ] -} + "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 + }, + { + "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", + "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": "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'" + } + ] +} \ No newline at end of file diff --git a/models/reseller_discount_tier.json b/models/reseller_discount_tier.json index aad259b..373469f 100644 --- a/models/reseller_discount_tier.json +++ b/models/reseller_discount_tier.json @@ -1,113 +1,115 @@ { - "summary": [ - { - "name": "reseller_discount_tier", - "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": 50, - "nullable": "no" - }, - { - "name": "min_amount", - "title": "最低消费金额", - "type": "float", - "length": 18, - "dec": 2, - "nullable": "no", - "default": "0.00" - }, - { - "name": "max_amount", - "title": "最高消费金额(NULL=无上限)", - "type": "float", - "length": 18, - "dec": 2, - "nullable": "yes" - }, - { - "name": "discount_pct", - "title": "折扣百分比", - "type": "float", - "length": 5, - "dec": 2, - "nullable": "no" - }, - { - "name": "sort_order", - "title": "排序", - "type": "int", - "length": 4, - "nullable": "no", - "default": "0" - }, - { - "name": "status", - "title": "状态", - "type": "str", - "length": 10, - "nullable": "no", - "default": "active" - }, - { - "name": "created_at", - "title": "创建时间", - "type": "timestamp", - "nullable": "no" - }, - { - "name": "updated_at", - "title": "更新时间", - "type": "timestamp", - "nullable": "no" - } - ], - "indexes": [ - { - "name": "idx_rdt_reseller_order", - "idxtype": "index", - "idxfields": ["resellerid", "sort_order"] - }, - { - "name": "idx_rdt_amount", - "idxtype": "index", - "idxfields": ["resellerid", "min_amount"] - } - ], - "codes": [ - { - "field": "resellerid", - "table": "organization", - "valuefield": "id", - "textfield": "orgname" - }, - { - "field": "status", - "table": "appcodes_kv", - "valuefield": "k", - "textfield": "v", - "cond": "parentid='discount_tier_status'" - } - ] -} + "summary": [ + { + "name": "reseller_discount_tier", + "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": 50, + "nullable": "no" + }, + { + "name": "min_amount", + "title": "最低消费金额", + "type": "float", + "length": 18, + "dec": 2, + "nullable": "no", + "default": "0.00" + }, + { + "name": "max_amount", + "title": "最高消费金额(NULL=无上限)", + "type": "float", + "length": 18, + "dec": 2, + "nullable": "yes" + }, + { + "name": "discount_pct", + "title": "折扣百分比", + "type": "float", + "length": 5, + "dec": 2, + "nullable": "no" + }, + { + "name": "sort_order", + "title": "排序", + "type": "int", + "length": 4, + "nullable": "no", + "default": "0" + }, + { + "name": "status", + "title": "状态", + "type": "str", + "length": 10, + "nullable": "no", + "default": "active" + }, + { + "name": "created_at", + "title": "创建时间", + "type": "timestamp", + "nullable": "no" + }, + { + "name": "updated_at", + "title": "更新时间", + "type": "timestamp", + "nullable": "no" + } + ], + "indexes": [ + { + "name": "idx_rdt_reseller_order", + "idxtype": "index", + "idxfields": [ + "resellerid", + "sort_order" + ] + }, + { + "name": "idx_rdt_amount", + "idxtype": "index", + "idxfields": [ + "resellerid", + "min_amount" + ] + } + ], + "codes": [ + { + "field": "status", + "table": "appcodes_kv", + "valuefield": "k", + "textfield": "v", + "cond": "parentid='discount_tier_status'" + } + ] +} \ No newline at end of file