From f5250fb261589964d4b2d14a20312cb563771d69 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 24 Jun 2026 00:56:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=90=A5=E9=94=80=E6=96=B9=E6=A1=88?= =?UTF-8?q?=E3=80=81=E4=BF=83=E9=94=80=E7=A0=81=E3=80=81=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E5=BD=92=E5=B1=9E=E7=BB=91=E5=AE=9A=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增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 --- discount/__pycache__/init.cpython-310.pyc | Bin 0 -> 8941 bytes discount/init.py | 379 +++++++++++++++++++++- init/data.json | 40 +++ json/discount_customer_bind_list.json | 38 +++ json/discount_marketing_list.json | 38 +++ json/discount_promo_code_list.json | 43 +++ models/discount_customer_bind.json | 98 ++++++ models/discount_marketing.json | 97 ++++++ models/discount_promo_code.json | 106 ++++++ wwwroot/api/customer_bind_create.dspy | 3 + wwwroot/api/customer_bind_delete.dspy | 2 + wwwroot/api/customer_bind_update.dspy | 2 + wwwroot/api/marketing_create.dspy | 3 + wwwroot/api/marketing_delete.dspy | 3 + wwwroot/api/marketing_update.dspy | 3 + wwwroot/api/promo_code_create.dspy | 3 + wwwroot/api/promo_code_delete.dspy | 3 + wwwroot/api/promo_code_update.dspy | 3 + wwwroot/menu.ui | 15 + 19 files changed, 875 insertions(+), 4 deletions(-) create mode 100644 discount/__pycache__/init.cpython-310.pyc create mode 100644 init/data.json create mode 100644 json/discount_customer_bind_list.json create mode 100644 json/discount_marketing_list.json create mode 100644 json/discount_promo_code_list.json create mode 100644 models/discount_customer_bind.json create mode 100644 models/discount_marketing.json create mode 100644 models/discount_promo_code.json create mode 100644 wwwroot/api/customer_bind_create.dspy create mode 100644 wwwroot/api/customer_bind_delete.dspy create mode 100644 wwwroot/api/customer_bind_update.dspy create mode 100644 wwwroot/api/marketing_create.dspy create mode 100644 wwwroot/api/marketing_delete.dspy create mode 100644 wwwroot/api/marketing_update.dspy create mode 100644 wwwroot/api/promo_code_create.dspy create mode 100644 wwwroot/api/promo_code_delete.dspy create mode 100644 wwwroot/api/promo_code_update.dspy diff --git a/discount/__pycache__/init.cpython-310.pyc b/discount/__pycache__/init.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dc259c4a75af0be0acce87836172ac420deedf35 GIT binary patch literal 8941 zcmd5?NsJuVd9JOitE;1}mT>iZX|k=ukt7Lt5b&R;#98%}fn@ z8NR9^Iqm9%3Z2D9oLq7UWC<7|wgKII@i{K}e!)K=5sFY-N?Tb|+UlCB zQdw(hZGBDWyxua}g|!0bjh5Lit`#|7Xj$#jT8Z;!%Wjv~%A7B@D(&i8mGf3>qCL4b ziF`@eKUCJHL|IhuoEB9vf#-~v6jON4ifJ)}=bV@ob9l~+d9i@!f;b?a!t;PQD4xdi zDe*ON2+xD!8F3iTr^U0v!Sie4IdKHfL*l47hUYWl`BkO%!YA0BQqy8fxS<#NZ7(iS zD!f+c*3@`%)9WzO%&{9zZqU#-}agzO2y@G zU+Z>T0rgt-bBC?kg9zh zsn$0_{h`t?M1_4rsE^dX85U43h9*)gDsC4et5HO&^+>sl^LbP0m%tgeP@kVFqVSQ1IjdncDo0h!HX%$={E49|=rb8r(KZoT`%{tm zNIjFz|G~HVlbCxtntY_DZ=8yzBI9;Jif~3)57pmSBReWZTBJutG$BeKntk-%pA~jA zy+8Lzd88%dR3)4jWnYP=f2RJa`i`Pu{f1&F5B02lRXLfyHTbi8;6VD`!cS;8#Y*q* zZ3mw>ypAVvj*jc>$ZoqE`rVG(a^Ak~G`qrcHoDRge$edhc0%r-6`YnA1Wvf+b)1uq z+Y!#D^k^EcIl*1;x-DNcLQl3GKfo-Te&~GD>2^G)yWxEA$nudT=g3=>+&c1o9^l>I z@uepk#8))LbvWm4U=9a^F?YI1&$%OceSU}E;;y$mo&rpnu6CtYs=t_Q$?F-f`Qp`{ zQQzro##*yU%uB4fAZ9d)LfZ^F?e+FYsC| zPx?ZZ!FjPg*k)WA-a%}W5P0kSl5(~zNXExXZGp#1VqrN;2{Cd!4e4mKyJENHHN<+y zZF|zBMvEFtpoP2c23`+i=lTvOy0b{8~7cZD$_qMUA*7)c8DEf^+IekyE}Wbh$(6_aZ!4AcD*2s zOFOP~+d<>@y|Mkoy4SfIo5?0(v&kYKYhqna(<%y}3}~riv9Y!h#Oh9L(86Lp=*rky z_j}12bm@h1ffi56{V(`ryDxn zeLn~T3D!{LB$7`)!zWf^mGqt=+&!@QTgaMi$lMkRw@@hTd9Dn4vzHSXa|T$l_*kv& zlF0tkUp@Tf&ma8y$A9tbAN}R=n#l`@4F(66CA+yBgk3-d1ghe72niDeWnlR@9bjjI z0=!6uVGAPM+wm~0METujm_gb=s)QH1eycXm_#_X|-f~2ePf?NRArDgW43b#AE}x~s zVM++uV%_hA40Lfh2qgh}!xbW)!-8n7jcyCeO);C7#&{+jn&3G~NTzEu@(AURQgV!v z=P7xClCM+3%Xo!y$C1Q_^qPUJQ&w+w!r0gaV8~ago#FO1%DqUv!G7PO~zQ`a=c9Y!5O9HXES z4&iI5RJOFGzcWtA-^MGNgxlGN=uY^{=1zzV%pGMPMrNc&8ezwWIwOYxR4^hVm=VUR z5*f?N_m8pxq>6$tAHl2uPP_yRQbjQ>8z?LL8fGd8AVZ|X=+Lsc$Y`LM?#xkIvwLqY zCkF?mzvsAMn!8?ZAvhp=R|>WoNLg>9v4hG)V}3K^RA zPBS*35WX}c4xaNJeS4qyrA2QY6QG~t< z#0k~C1DPR4^t`9k3cVLDY(3o?pFq<>G}Ku(vK>p#hlq_UX&)$GuFfLbaUb!;VI3ftI&@7o9G&0AqiRqjh=>?G> zY-es?`GhqQC69lQ?Ix$f@GjER&$g2GNjH*h+!sTO7^sfVZ6nttS&Jm7tr_wfEsW%r z`6vPP(vX-YSPia?wNAjI$|C^)cUq_70^#|oF}aQB0S*nag@e$QIXC|?UJ^J+AhDWe zYO4908H2Fc)+RJVT}blkAx-`cI*o8NshXUle~H5TDqDGar0)Z5+3HdWUxfx<2A@X; z;VvvZ;Ad2Tqespe^(aQgU@_Dq1-%S*$xsWPEeZoXYxJ$Kkn%n1<#$8@)*j6PYc8@< z8<6I)BRjY#iWq70NHd?q6e{@syXlOmr(WRuKV-hoPI?HC-JHRl#g@0}Hushw2;r{m z1Q~9V9Rm|&*Q?c;F>eN*lhHUm*__NQN@?ZsT!u1!sju{Yo^stcoQ+|2OZjXdAzv&o zL*hc>Fenc3;#ZQI|3YRy9?~|6nqawE#o;pp%InD0iirWlRto@F>1H0z{*+cP9yh`q=^il*{@n6*}QkAN`D|0GIZ z%;MbTO1FD^cL%-vuJpsbvs`(y?p(YN+sJ9VVRP$jt}pLi z)19oJU`3hrOTl5K_ls1*v915lA}()%>w6#lk1lpxegAi1=${h_{kPZHf$aUs|DMJs zBpoo|5R#^ho+|#l)ISF6n*F8oSfhNMX!*nc)8(bKhqW_UR*u)O{mCv(9MC5j9n<3| zoo_2~EWaGD*(N!i*d(X9a3(378AtM`G9*u_nFIOXVu`_bkO0VS0J%O4t_>%d^kp(TTq)Gp5yp0nt-S{XD3wZv6i8BxVg zibYTaLXysdzGg#spoO2OCeoZqxPaZmF||8SmSnbq{G{NnDZv~D;-s`JmPpaVrO!Zf z7Y!p`I?+3)`R}5T(i|Rn8Ye7poY3Tp3HUgO6BZFCEQ02Rp=*K#SctF$*?xr2{;-e) z^}mGfzx4#V{|96JSI}hFD3CjBW=8R@zwyA*SY*+XO6g`NsQj@M+K`eM73C2KsYcLy=}`doB!E_;DF8C8jHT7%TF7ID*W2`$Cp^ zk2;lxSmrz`80nrEYkCs%%n|d9OZm|u-mwR~GneqrI$km&uN3Xcy$=Tk_NU<=!ykr6 z9jU3|LjEv35|*knbg)Ch0?DF?U24z=saz#-n4CbpcT@ZxGtIxcCx{@AC!O^@KGe^d zGK|YB2s;q|!uMl>F;=r3%qH-d@GI0(6NRRUB>ND?(T`&g z?;(Sz7!1qXYEZ&c!?oIUh}c3@Kt$R|BhuQQ8NWm5+8;+an8+z?^A@lbY?E=q z_#SCL>`{{7oIwHm;X~e}hB0wjz-2q88(;V>S~$Uo_TXtv`p7i#%RgwvHL;>xz%RIj zgo7IhN=8aUr9fOmqmV^ICx5MBP>7;YfErR7W{9v{qZmRVHLMV!ibg54k=lYTNYs^D zIkCE2Ov)Nqu2b$kN={Nj;cS*(W+T# z|0?h;Xk6v{)4EvSp?_LPu4rf6EnLdtKNsrB19zZI7R-}1Y~Y_6a9uTp&9pAs`c#=}D(KMnK;D5PL8oPY?~PSCXsbb9C&%5|SyUf?)38Yw6aa^|Cc> Vm8`?o5$mc|vF4DUwya9wKLPk*r4#@F literal 0 HcmV?d00001 diff --git a/discount/init.py b/discount/init.py index da9cc17..e13f9fd 100644 --- a/discount/init.py +++ b/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 diff --git a/init/data.json b/init/data.json new file mode 100644 index 0000000..a4f834e --- /dev/null +++ b/init/data.json @@ -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": "手动分配" + } + ] +} diff --git a/json/discount_customer_bind_list.json b/json/discount_customer_bind_list.json new file mode 100644 index 0000000..032f933 --- /dev/null +++ b/json/discount_customer_bind_list.json @@ -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')}}" + } + } +} diff --git a/json/discount_marketing_list.json b/json/discount_marketing_list.json new file mode 100644 index 0000000..7cf85d0 --- /dev/null +++ b/json/discount_marketing_list.json @@ -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" + } +} diff --git a/json/discount_promo_code_list.json b/json/discount_promo_code_list.json new file mode 100644 index 0000000..c6e08c3 --- /dev/null +++ b/json/discount_promo_code_list.json @@ -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" + } +} diff --git a/models/discount_customer_bind.json b/models/discount_customer_bind.json new file mode 100644 index 0000000..8b34ce8 --- /dev/null +++ b/models/discount_customer_bind.json @@ -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'" + } + ] +} diff --git a/models/discount_marketing.json b/models/discount_marketing.json new file mode 100644 index 0000000..115203a --- /dev/null +++ b/models/discount_marketing.json @@ -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'" + } + ] +} diff --git a/models/discount_promo_code.json b/models/discount_promo_code.json new file mode 100644 index 0000000..d5a33b0 --- /dev/null +++ b/models/discount_promo_code.json @@ -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'" + } + ] +} diff --git a/wwwroot/api/customer_bind_create.dspy b/wwwroot/api/customer_bind_create.dspy new file mode 100644 index 0000000..8a69b2b --- /dev/null +++ b/wwwroot/api/customer_bind_create.dspy @@ -0,0 +1,3 @@ +# 客户归属 - 绑定客户(通过促销码) +result = await bind_customer(request, params_kw) +return result if isinstance(result, dict) else {'status': 'error', 'message': str(result)} diff --git a/wwwroot/api/customer_bind_delete.dspy b/wwwroot/api/customer_bind_delete.dspy new file mode 100644 index 0000000..e64e632 --- /dev/null +++ b/wwwroot/api/customer_bind_delete.dspy @@ -0,0 +1,2 @@ +# 客户归属删除 - 暂无实现(归属关系永久保留) +return {'status': 'error', 'message': 'Customer binding cannot be deleted'} diff --git a/wwwroot/api/customer_bind_update.dspy b/wwwroot/api/customer_bind_update.dspy new file mode 100644 index 0000000..73b6569 --- /dev/null +++ b/wwwroot/api/customer_bind_update.dspy @@ -0,0 +1,2 @@ +# 客户归属更新 - 暂无实现(首次绑定后不可变更) +return {'status': 'error', 'message': 'Customer binding cannot be updated after initial binding'} diff --git a/wwwroot/api/marketing_create.dspy b/wwwroot/api/marketing_create.dspy new file mode 100644 index 0000000..970ae79 --- /dev/null +++ b/wwwroot/api/marketing_create.dspy @@ -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)} diff --git a/wwwroot/api/marketing_delete.dspy b/wwwroot/api/marketing_delete.dspy new file mode 100644 index 0000000..727ad0a --- /dev/null +++ b/wwwroot/api/marketing_delete.dspy @@ -0,0 +1,3 @@ +# 删除营销方案 +result = await delete_marketing(request, params_kw) +return result if isinstance(result, dict) else {'status': 'error', 'message': str(result)} diff --git a/wwwroot/api/marketing_update.dspy b/wwwroot/api/marketing_update.dspy new file mode 100644 index 0000000..b50c80c --- /dev/null +++ b/wwwroot/api/marketing_update.dspy @@ -0,0 +1,3 @@ +# 更新营销方案 +result = await update_marketing(request, params_kw) +return result if isinstance(result, dict) else {'status': 'error', 'message': str(result)} diff --git a/wwwroot/api/promo_code_create.dspy b/wwwroot/api/promo_code_create.dspy new file mode 100644 index 0000000..fd899c5 --- /dev/null +++ b/wwwroot/api/promo_code_create.dspy @@ -0,0 +1,3 @@ +# 生成促销码 +result = await generate_promo_code(request, params_kw) +return result if isinstance(result, dict) else {'status': 'error', 'message': str(result)} diff --git a/wwwroot/api/promo_code_delete.dspy b/wwwroot/api/promo_code_delete.dspy new file mode 100644 index 0000000..4a83d56 --- /dev/null +++ b/wwwroot/api/promo_code_delete.dspy @@ -0,0 +1,3 @@ +# 删除促销码(禁用即可,一般不物理删除) +result = await disable_promo_code(request, params_kw) +return result if isinstance(result, dict) else {'status': 'error', 'message': str(result)} diff --git a/wwwroot/api/promo_code_update.dspy b/wwwroot/api/promo_code_update.dspy new file mode 100644 index 0000000..4092925 --- /dev/null +++ b/wwwroot/api/promo_code_update.dspy @@ -0,0 +1,3 @@ +# 禁用促销码 +result = await disable_promo_code(request, params_kw) +return result if isinstance(result, dict) else {'status': 'error', 'message': str(result)} diff --git a/wwwroot/menu.ui b/wwwroot/menu.ui index 548c069..d1cfe15 100644 --- a/wwwroot/menu.ui +++ b/wwwroot/menu.ui @@ -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": "生成促销码",