first commit

This commit is contained in:
yumoqing 2025-08-02 00:27:44 +08:00
commit e36c1ec5be
74 changed files with 1544 additions and 0 deletions

123
README.md Normal file
View File

@ -0,0 +1,123 @@
# platformbiz
平台业务模块,包括业务功能和对应的数据表设计
## 依赖
* 记账模块[accounting](https://git.kaiyuancloud.cn/yumoqing/accounting)
* 鉴权模块[rbac](https://git.kaiyuancloud.cn/yumoqing/rbac)
* 支付模块[payment](https://git.kaiyuancloud.cn/yumoqing/payment)
## 功能
### 平台机构功能
#### 供应商管理
##### 供应商协议
##### 协议明细
##### 阶梯折扣管理
##### 分销产品管理
##### 供应商接口测试
#### 机构用户管理
### 客户机构功能
#### 账户管理
##### 充值
##### 提现
##### 账户明细查询
#### 订单管理
##### 订单查询
##### 取消订单
##### 订单支付
##### 重复购买
#### 账单管理
##### 账单查询
#### 资源管理
##### 资源续费
##### 资源退订
##### 重复购买
##### 资源入口
#### 产品浏览
##### 产品比价
##### 产品种类浏览
##### 产品热卖
##### 商户推荐
#### 购买
##### 产品配置
##### 产品询价
##### 加入购物车
##### 立即购买
##### 支付
##### 记账
###### 记账角色
* 客户:产品购买方
* 商户:产品售卖方
* 分销方:产品分销协议分销方
* 供应方:产品分销协议供应方
* 平台:算力平台
###### 分录设置
* 借: 客户 客户资金账户 交易金额
* 贷: 商户 商户交易费支出 交易金额 * 交易费率
* 借: 平台 平台交易费收入 交易金额 * 交易费率
* 借: 商户 商户交易费支出 交易金额 * 交易费率
* 借: 分销方-供应方 商户采购成本 采购金额
* 贷: 分销方 客户资金账户 交易金额 - 采购金额 - (交易金额 * 交易费率)
* 借* 商户-分销方 供应商分销收入 交易金额1
* 贷* 商户-供应方 商户采购成本 采购成本1
* 贷* 商户 客户资金账户 交易金额1 - 采购成本1
#### 参与商户营销
##### 商户获客营销
##### 参与售卖营销
### 商户机构功能
#### 成为商户
在客户用户界面点击【成为商户】,系统自动将此平台客户添加商户角色,成为平台商户
并为其开通商户经营所需的记账账号。
与平台或平台其他商户(供应商)线下协商成为其产品的分销商
#### 添加不同角色的用户
#### 发展分销商户(自己成为分销商户的供应商)
新添一个分销协议,在协议中添加协议明细,设置分销折扣,或阶梯折扣,将自己的产品授权给分销商销售。
##### 供应商添加分销协议
##### 供应商添加协议明细
##### 供应商添加协议明细步骤(只有阶梯折扣协议才需要)
##### 供应商添加分销产品到分销协议
##### 商户设置一个无指定客户的分销协议
#### 自有产品管理
##### 添加自由产品
##### 自有产品区域管理
##### 自有产品数量管理
##### 自有产品计价管理
##### 自有产品计价表管理
#### 设置产品销售折扣或价格
##### 公用产品折扣设置
为所有客户提供一个以产品计价为基础的折扣
##### 为指定客户设置特殊折扣
#### 主推产品管理
#### 大屏功能
##### 展示位置设置
##### 展示指标管理
##### 大屏风格设置
#### 短信管理
##### 开通短信功能
##### 通知场景管理
##### 短信计费
### 产品购买
#### 标的
卖房提供的产品
#### 参与方
* 卖房
* 买房
* 平台
* 供应方
#### 分录设置
借:买方-

23
json/agreedetail.json Normal file
View File

@ -0,0 +1,23 @@
{
"tblname":"agreedetail",
"params":{
"browserfields": {
"exclouded": ["id", "agreeid"],
"alters": {}
},
"editexclouded": [
"id", "agreeid"
],
"subtables":[
{
"field":"agreedetailid",
"title":"协议产品",
"params":{
"agreeid":"${agreeid}",
"prodtypeid":"${prodtypeid}"
},
"subtable":"agreeproduct"
}
]
}
}

12
json/agreedetailstep.json Normal file
View File

@ -0,0 +1,12 @@
{
"tblname":"agreedetailstep",
"params":{
"browserfields": {
"exclouded": ["id"],
"alters": {}
},
"editexclouded": [
"id"
]
}
}

32
json/agreement.json Normal file
View File

@ -0,0 +1,32 @@
{
"tblname":"agreement",
"params":{
"toolbar":{
"tools":[
{
"name":"prodauth",
"selected_row":true,
"label":"授权"
}
]
},
"binds":[
{
"wid":"self",
"event":"prodauth",
"actiontype":"urlwidget",
"target":"PopupWindow",
"options":{
"url":"{{entire_url('../agree_prodclone.dspy')}}"
}
}
],
"browserfields": {
"exclouded": ["id"],
"alters": {}
},
"editexclouded": [
"id"
]
}
}

13
json/agreeproduct.json Normal file
View File

@ -0,0 +1,13 @@
{
"tblname":"agreeproduct",
"params":{
"logined_userorgid":"orgid",
"browserfields": {
"exclouded": ["id", "agreedetailid"],
"alters": {}
},
"editexclouded": [
"id", "agreedetailid", "resellerpid"
]
}
}

12
json/biz_order.json Normal file
View File

@ -0,0 +1,12 @@
{
"tblname":"biz_order",
"params":{
"browserfields": {
"exclouded": ["id"],
"alters": {}
},
"editexclouded": [
"id"
]
}
}

3
json/build.sh Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/bash
xls2ui -m ../models -o ../wwwroot platformbiz *.json

40
json/choose_prodtype.json Normal file
View File

@ -0,0 +1,40 @@
{
"tblname":"prodtype",
"uitype":"tree",
"alias":"choose_prodtype",
"params":{
"toolbar":{
"bar_cwidth":2,
"tools":[
{
"name":"test",
"selected_data":true,
"label":"TEST"
}
]
},
"binds":[
{
"wid":"self",
"event":"prodauth",
"actiontype":"urlwidget",
"target":"PopupWindow",
"options":{
"url":"{{entire_url('platformbiz/ag.dspy')}}",
"params":{
"agreedetailid":"{{params_kw.agreedetailid}}"
}
}
}
],
"idField":"id",
"textField":"name",
"editable":true,
"browserfields":{
"alters":{}
},
"edit_exclouded_fields":["del_flg", "create_at"],
"parentField":"parentid"
}
}

12
json/coupon.json Normal file
View File

@ -0,0 +1,12 @@
{
"tblname":"coupon",
"params":{
"browserfields": {
"exclouded": ["id"],
"alters": {}
},
"editexclouded": [
"id"
]
}
}

12
json/coupon_log.json Normal file
View File

@ -0,0 +1,12 @@
{
"tblname":"coupon_log",
"params":{
"browserfields": {
"exclouded": ["id"],
"alters": {}
},
"editexclouded": [
"id"
]
}
}

12
json/coupontype.json Normal file
View File

@ -0,0 +1,12 @@
{
"tblname":"coupontype",
"params":{
"browserfields": {
"exclouded": ["id"],
"alters": {}
},
"editexclouded": [
"id"
]
}
}

12
json/paychannel.json Normal file
View File

@ -0,0 +1,12 @@
{
"tblname":"paychannel",
"params":{
"browserfields": {
"exclouded": ["id"],
"alters": {}
},
"editexclouded": [
"id"
]
}
}

20
json/prodpricing.json Normal file
View File

@ -0,0 +1,20 @@
{
"tblname":"prodpricing",
"params":{
"browserfields": {
"exclouded": ["id", "prodid" ],
"alters": {
}
},
"editexclouded": [
"id", "prodid"
],
"subtables":[
{
"field":"prodpricingid",
"subtable":"prodpricingtab",
"title":"产品定价表"
}
]
}
}

12
json/prodpricingtab.json Normal file
View File

@ -0,0 +1,12 @@
{
"tblname":"prodpricingtab",
"params":{
"browserfields": {
"exclouded": ["id", "prodid"],
"alters": {}
},
"editexclouded": [
"id", "prodid"
]
}
}

24
json/prodtype.json Normal file
View File

@ -0,0 +1,24 @@
{
"tblname":"prodtype",
"uitype":"tree",
"params":{
"idField":"id",
"textField":"name",
"parentField":"parentid",
"editable":true,
"browserfields": {
"exclouded": ["id", "parentid" ],
"alters": {}
},
"editexclouded": [
"id", "parentid"
],
"subtables":[
{
"field":"prodtypeid",
"title":"产品类型",
"subtable":"prodtypespec"
}
]
}
}

15
json/prodtypespec.json Normal file
View File

@ -0,0 +1,15 @@
{
"tblname":"prodtypespec",
"params":{
"browserfields": {
"exclouded": ["id", "prodtypeid"],
"alters": {}
},
"data_params":{
"prodtypeid":"${params_kw.id}"
},
"editexclouded": [
"id", "prodtypeid"
]
}
}

24
json/product.json Normal file
View File

@ -0,0 +1,24 @@
{
"tblname":"product",
"params":{
"logined_userorgid":"orgid",
"browserfields": {
"exclouded": ["id", "orgid", "providerid", "agreeid", "providerpid" ],
"alters": {
}
},
"editexclouded": [
"id", "orgid", "providerid", "agreeid", "providerpid"
],
"subtables":[
{
"field":"prodid",
"params":{
"prodtypeid":"${prodtypeid}"
},
"subtable":"prodpricing",
"title":"产品定价"
}
]
}
}

30
json/provide_agree.json Normal file
View File

@ -0,0 +1,30 @@
{
"tblname":"agreement",
"alias":"provide_agree",
"params":{
"title":"供应协议",
"logined_userorgid":"resellerid",
"browserfields": {
"exclouded": ["id", "resellerid" ],
"alters": {
"providerid":{
"uitype":"search",
"search_event":"row_selected",
"search_url":"{{entire_url('../select_org')}}",
"valueField":"id",
"textField":"orgname"
}
}
},
"subtables":[
{
"title":"协议明细",
"subtable":"provide_agreedetail",
"field":"agreeid"
}
],
"editexclouded": [
"id", "resellerid"
]
}
}

View File

@ -0,0 +1,24 @@
{
"tblname":"agreedetail",
"alias":"provide_agreedetail",
"params":{
"browserfields": {
"exclouded": ["id", "agreeid"],
"alters": {}
},
"editexclouded": [
"id", "agreeid"
],
"subtables":[
{
"field":"agreedetailid",
"title":"供应协议产品",
"params":{
"agreeid":"${agreeid}",
"prodtypeid":"${prodtypeid}"
},
"subtable":"provide_agreeproduct"
}
]
}
}

View File

@ -0,0 +1,14 @@
{
"tblname":"agreeproduct",
"alias":"provide_agreeproduct",
"params":{
"logined_userorgid":"orgid",
"browserfields": {
"exclouded": ["id", "agreedetailid", "resellerpid"],
"alters": {}
},
"editexclouded": [
"id", "agreedetailid", "resellerpid"
]
}
}

12
json/recgarge_log.json Normal file
View File

@ -0,0 +1,12 @@
{
"tblname":"recharge_log",
"params":{
"browserfields": {
"exclouded": ["id"],
"alters": {}
},
"editexclouded": [
"id"
]
}
}

12
json/resource.json Normal file
View File

@ -0,0 +1,12 @@
{
"tblname":"resource",
"params":{
"browserfields": {
"exclouded": ["id"],
"alters": {}
},
"editexclouded": [
"id"
]
}
}

25
json/retail_agree.json Normal file
View File

@ -0,0 +1,25 @@
{
"tblname":"agreement",
"alias":"retail_agree",
"params":{
"title":"分销协议",
"sortby":"name",
"logined_userorgid":"providerid",
"logined_userid":"provideruid",
"browserfields": {
"exclouded": ["id", "providerid", "provideruid" ],
"alters": {
"resellerid":{
"uitype":"search",
"search_event":"row_selected",
"search_url":"{{entire_url('../select_org')}}",
"valueField":"id",
"textField":"orgname"
}
}
},
"editexclouded": [
"id", "providerid", "provideruid"
]
}
}

BIN
models/agreedetail.xlsx Normal file

Binary file not shown.

BIN
models/agreedetailstep.xlsx Normal file

Binary file not shown.

BIN
models/agreement.xlsx Normal file

Binary file not shown.

BIN
models/agreeproduct.xlsx Normal file

Binary file not shown.

BIN
models/biz_order.xlsx Normal file

Binary file not shown.

BIN
models/biz_orderdetail.xlsx Normal file

Binary file not shown.

BIN
models/coupon.xlsx Normal file

Binary file not shown.

BIN
models/coupon_log.xlsx Normal file

Binary file not shown.

BIN
models/coupontype.xlsx Normal file

Binary file not shown.

Binary file not shown.

BIN
models/paychannel.xlsx Normal file

Binary file not shown.

BIN
models/pr_link.xlsx Normal file

Binary file not shown.

BIN
models/prodpricing.xlsx Normal file

Binary file not shown.

BIN
models/prodpricingtab.xlsx Normal file

Binary file not shown.

BIN
models/prodtype.xlsx Normal file

Binary file not shown.

BIN
models/prodtypespec.xlsx Normal file

Binary file not shown.

BIN
models/product.xlsx Normal file

Binary file not shown.

BIN
models/recharge_log.xlsx Normal file

Binary file not shown.

BIN
models/reseller.xlsx Normal file

Binary file not shown.

BIN
models/resource.xlsx Normal file

Binary file not shown.

10
platformbiz/biz.py Normal file
View File

@ -0,0 +1,10 @@
from platformbiz.pricing import get_price_infos
from biz_order import add_pay_order
async customerpay(sor, sellerid, customerid, userid, order_details):
await add_pay_order(sor, sellerid, customerid, userid, order_details)
price_infos = await get_price_infos(sor, sellerid,
customerid,
priductid,
prod_config)
:

121
platformbiz/biz_order.py Normal file
View File

@ -0,0 +1,121 @@
from time import time
from ahserver.serverenv import get_serverenv
from sqlor.dbpools import DBPools
from appPublic.dictObject import DictObject
from appPublic.log import debug
from appPublic.uniqueID import getID
from platformbiz.const import ORDER_INITIAL, RECHARGE_INITIAL
from platformbiz.pricing import get_biz_date
async def add_recharge_order(sor, customerid, userid, action, recharge_amt):
"""
arguments:
customerid: organization who recharge
userid: user who do the recharge action
recharge_amt: recharge amount
action: business action name
return:
order record
"""
rec = DictObject()
rec.id = getID()
rec.customerid = customerid
rec.userid = userid
get_business_date = get_serverenv('get_business_date')
rec.order_date = await get_business_date()
rec.business_op = action
rec.amount = recharge_amt
rec.order_status = ORDER_INITIAL
await sor.C('biz_order', rec.copy())
return rec
async def get_paychannel_by_name(sor, name):
sql = "select * from paychannel where name=${name}$"
recs = await sor.sqlExe(sql, {'name':name})
if len(recs) > 0:
return recs[0]
debug(f'get paychannel error({name})')
return None
async def add_recharge_log(sor, customerid, userid, action, orderid, transdate, recharge_amt, name):
rec = DictObject()
rec.id = getID()
rec.customerid = customerid
rec.userid = userid
rec.action = action
rec.recharge_amt = recharge_amt
pc = await get_paychannel_by_name(sor, name)
debug(f'{pc=}, {recharge_amt=}')
if pc is None:
raise Exception(f'paychannel({name}) pay channel not found')
rec.fee_amt = recharge_amt * pc.fee_rate
rec.fee_rate = pc.fee_rate
rec.pcid = pc.id
rec.biz_orderid = orderid
rec.recharge_status = RECHARGE_INITIAL
rec.transdate = transdate
await sor.C('recharge_log', rec.copy())
return rec
async def change_recharge_status(sor, rlid, status, tid):
recs = await sor.R('recharge_log', {'id':rlid})
if len(recs) < 1:
return None
recs[0].recharge_status = status
recs[0].channel_tid = tid
await sor.U('recharge_log', recs[0].copy())
return recs[0]
async def add_pay_order(sor, sellerid,
customerid,
userid,
action,
order_details,
origin_orderid=None):
env = globals()
customerid1 = await env.get_userorgid()
userid1 = await env.get_user()
if customerid != customerid1:
e = Exception(f'{custmerid} is not logined orgid')
exception(f'{e}')
raise e
if userid != userid1:
e = Exception(f'{userid} is not logined userid')
exception(f'{e}')
raise e
id = getID()
amount = 0.0
for order_detail in order_details:
ns1 = {
"id": id,
"orderid":ns['id'],
"productid":order_detail.productid,
"product_cnt":order_detail.product_cnt,
"prod_config":order_detail.prod_config,
"list_amount":order_detail.list_amount,
"trans_amount":order_detail.trans_amount
}
await sor.C('biz_orderdetail', ns1)
amount += order_detail.trans_amount
price_infos = await get_price_infos(sor, sellerid,
customerid,
order_detail.productid,
order_detail.prod_config)
ns = {
"id": id,
"customerid":customerid,
"userid":userid,
"resellerid":sellerid,
"order_date":await get_biz_date(sor),
"order_status":"0",
"business_op":action,
"amount":pay_amount,
"ordertype":None,
"pay_date":None,
"origin_orderid":origin_orderid
}
await sor.C('biz_order', ns)

2
platformbiz/const.py Normal file
View File

@ -0,0 +1,2 @@
ORDER_INITIAL = '0'
RECHARGE_INITIAL = '0'

8
platformbiz/getdbname.py Normal file
View File

@ -0,0 +1,8 @@
from ahserver.serverenv import get_serverenv
def get_dbname():
f = get_serverenv('get_module_dbname')
if f is None:
raise Exception('get_module_dbname() not found')
return f('platformbiz')

20
platformbiz/init.py Normal file
View File

@ -0,0 +1,20 @@
from ahserver.serverenv import ServerEnv, get_serverenv
from platformbiz.recharge import Recharge
from platformbiz.pb_acc import PlatformBizAccRecharge, get_owner_orgid, get_balance
from platformbiz.biz_order import change_recharge_status
from platformbiz.pricing import calc_prod_price, get_sell_price, get_price_infos
from platformbiz.product import agree_products_clone, open_agree_account
def load_platformbiz():
g = ServerEnv()
g.Recharge = Recharge
g.PlatformBizAccRecharge = PlatformBizAccRecharge
g.change_recharge_status = change_recharge_status
g.get_owner_orgid = get_owner_orgid
g.get_balance = get_balance
g.calc_prod_price = calc_prod_price
g.get_sell_price = get_sell_price
g.get_price_infos = get_price_infos
g.agree_products_clone = agree_products_clone
g.open_agree_account = open_agree_account

111
platformbiz/pb_acc.py Normal file
View File

@ -0,0 +1,111 @@
import time
import json
from appPublic.timeUtils import timestampstr
from appPublic.registerfunction import rfexe
from sqlor.dbpools import DBPools
from ahserver.serverenv import get_serverenv
from accounting.accounting_config import Accounting
from accounting.bizaccount import BizAccounting
from accounting.bill import write_bill
from platformbiz.getdbname import get_dbname
async def get_owner_orgid(sor, orgid):
return '0'
async def get_balance(orgid):
db = DBPools()
dbname = get_dbname()
async with db.sqlorContext(dbname) as sor:
f = get_serverenv('getCustomerBalance')
if f:
return await f(sor, orgid)
return None
class PlatformBizAcc:
"""
"""
async def build_accountset(self, sor, biz_order, biz_orderdetails):
acconuntset = DictObject()
accountset['action'] = biz_order.business_op
accountset['owner'] = get_owner_orgid(sor, '0')
accountset['reseller'] = biz_order.resellerid
accountset['customer'] = biz_order.customerid
accountset['交易金额'] = biz_order.amount
transfee = await get_transfee(sor, self.resellerid, biz_order.amount, self.curdate)
accountset['交易费用'] = transfee
accountset.subsets = []
for od in biz_orderdetails:
price_infos = await get_price_infos(sor, self.resellerid,
self.customerid,
detail.productid,
detail.prod_config)
if len(price_infos) > 1:
for pi in price_infos[:-1]:
actions = biz_order.business_op.split('_')
actions[0] = actions[0] + '*'
aset = DictObject()
aset['action'] = '_'.join(actions)
aset['owner'] = get_owner_orgid(sor, '0')
aset['reseller'] = pi['buyerid']
aset['provider'] = pi['resellerid']
aset['采购成本'] = pi['sell_price']
accountset.subsets.append(aset)
return accountset
async def accounting(self, sor, biz_orderid):
biz_order = await sor.R('biz_order', {'id':biz_orderid})
details = await sor.R('biz_orderdetail',{'orderid':biz_orderid})
accountset = await self.build_accountset(biz_order, details)
self.curdate = await get_business_date(sor)
transfee = await get_transfee(sor, self.resellerid, biz_order.amount, self.curdate)
a = BizAccounting(self.curdate, biz_order, accountset)
r = await a.do_accounting(sor)
async def get_orgid_by_trans_role(self, sor, orgtype):
if orgtype== 'customer':
return self.customerid
if orgtype== 'reseller':
return self.resellerid
if orgtype== 'provider':
return self.providerid
if orgtype == 'owner':
return '0'
return None
class PlatformBizAccRecharge(PlatformBizAcc):
def __init__(self, rlid):
self.rlid = rlid
async def accounting(self, sor):
sql = """select * from recharge_log where id=${rlid}$"""
recs = await sor.sqlExe(sql, {'rlid':self.rlid})
if len(recs) < 1:
e = Exception(f'get recharge log err by {rlid=}')
exception(f'{e=}')
raise e
self.recharge = recs[0]
self.customerid = self.recharge.customerid
self.orderid = self.recharge.biz_orderid
self.userid = self.recharge.userid
get_business_date = get_serverenv('get_business_date')
self.curdate = await get_business_date(sor)
self.variable = {
"交易金额":self.recharge.recharge_amt,
"充值费率":self.recharge.fee_rate,
"充值费用":self.recharge.feemat
}
bill = await write_bill(sor, self.customerid, self.userid,
self.recharge.orderid,
self.recharge.action,
self.recharge.recharge_amt)
self.billid = bill.id
self.bill = bill
self.providerid = None
self.resellerid = None
self.action = self.recharge.action
self.productid = None
self.timestamp = timestampstr()
a = Accounting(self)
r = await a.do_accounting(sor)

273
platformbiz/pricing.py Normal file
View File

@ -0,0 +1,273 @@
from appPublic.log import debug, exception
from ahserver.serverenv import get_serverenv
from traceback import format_exc
def get_step_dates(use_date, step_type):
if step_type == '0':
# 总金额
return '1000-01-01', '9999-12-31'
y = int(use_date[:4])
m = int(use_date[6:7])
if step_type == '1':
y = int(use_date[:4])
return '%04d-01-01' % y, '%04d-01-01' % (y+1)
if step_type == '2':
ss = [1,4,7,10, 13]
for i, s in enumerate(ss):
if m >=s and m < ss[i+1]:
if i < 3:
return '%04d-%02d-01' % (y, s), '%04d-%02d-01' % (y, ss[i+1])
else:
return '%04d-%02d-01' % (y, s), '%04d-01-01' % (y+1)
if step_type == '3':
if m < 12:
return '%04d-%02d-01' % (y, m), '%04d-%02d-01' % (y, m+1)
else:
return '%04d-%02d-01' % (y, m), '%04d-01-01' % (yi+1)
e = Exception(f'unknown {step_type=}')
exception(f'{e=}')
raise e
async def get_biz_date(sor):
biz_date = ''
f = get_serverenv('get_business_date')
if f:
biz_date = await f(sor)
return biz_date
e = Exception(f'get_serverenv("get_business_date") return None')
exception(f'{e=}')
raise e
async def get_step_discount(sor, agreedetailid, step_type, sellerid, buyerid):
amount = await get_sale_amount(sor, step_type, sellerid, buyerid)
sql = """select * from agreedetailstep
where adid = ${adid}
and minamt <= ${amount}$
and maxamt > ${amount}$
"""
recs = await sor.sqlExe(sql, {'adid':agreedetailid,
'amount':amount
})
if len(recs) < 1:
e = Exception(f'{sql=} not data, {agreedetailid=}, {amount=}')
exception(f'{e=}')
raise e
return rec.discount
async def get_sale_amount(sor, step_type, sellerid, buyerid):
"""
统计销售收入根据step_type分别统计总销售额本年销售累计
本季度销售累计本月销售累计和本周销售累计等
"""
subjectname = '供应商分销收入'
f = get_serverenv('getAccountByName')
if f is None:
e = Exception(f'get_serverenv("getAccountByName") return None')
exception(f'{e=}')
raise e
acc = await f(sor, accounting_orgid, sellerid, subjectname, buyerid)
biz_date = await get_biz_date(sor)
from_date, to_date = get_date_between(biz_date, step_type)
f = get_serverenv('get_account_total_amount')
if f is None:
e = Exception(f'get_serverenv("get_account_total_amount") return None')
exception(f'{e=}')
raise e
amount = await f(sor, acc.id, acc.blance_at, from_date, to_date)
return amount
async def get_product_agreement(sor, sellerid, buyerid, productid, biz_date):
sql = """select a.*,
c.id as agreedetailid,
c.prodtypeid as agree_prodtypeid,
c.discount,
c.step_type
from product as a, agreement as b, agreedetail as c
where c.agreeid = b.id
and ( a.prodtypeid = c.prodtypeid or c.prodtypeid is NULL)
and a.orgid = b.providerid
and b.enable_date <= ${biz_date}$
and b.expire_date > ${biz_date}$
and a.orgid = ${sellerid}$
and b.resellerid = ${buyerid}$
and a.id = ${productid}$
"""
recs = await sor.sqlExe(sql, {'sellerid':sellerid,
'buyerid':buyerid,
'biz_date':biz_date,
'productid':productid
})
if len(recs) == 0:
e = f'there is no agreement for {sellerid=} and {buyerid=}, {productid=} at {biz_date=} {sql=}'
debug(f'{e=}')
return None
rec = None
for r in recs:
if r.prodtypeid == r.agree_prodtypeid:
rec = r
break
if rec is None:
for r in recs:
if r.agree_prodtypeid is None:
rec = r
break
if rec is None:
e = Exception(f'Not agreements for {sellerid=} and {buyerid=}, {productid=} at {biz_date=} ')
exception(f'{e=}')
raise e
return rec
def get_unit_value_price(sc, pricingtab):
for pt in pricingtab:
if pt.specvalue == '':
pt.specvalue = None
if sc.spec_name == pt.specname and \
sc.spec_value == pt.specvalue:
# print(f'found {sc.spec_name=},{sc.spec_value=}')
return pt.unit_value, pt.unit_amt
# print(f'{sc.spec_name=},{sc.spec_value=}:{pt.specname=},{pt.specvalue=}')
return None, None
async def calc_prod_price(sor, productid, spec_config):
"""fact_config:
[
{
spec_name, spec_value, count
}
]
"""
biz_date = await get_biz_date(sor)
sql = """select c.*,
e.name as specname
from product a, prodpricing b,
prodpricingtab c, prodtype d,
prodtypespec e
where a.id=${productid}$
and a.prodtypeid = d.id
and e.prodtypeid = d.id
and a.id = b.prodid
and b.enable_date <= ${biz_date}$
and b.expire_date > ${biz_date}$
and c.prodpricingid = b.id
and c.ptspecid = e.id
"""
recs = await sor.sqlExe(sql, {'productid':productid,
'biz_date':biz_date})
if len(recs) < 1:
e = Exception(f'{sql=}, {productid=} {biz_date=} return not data')
exception(f'{e=}')
raise e
price = 0.0
for sc in spec_config:
# print(f'{sc=}, {recs=}')
uv, up = get_unit_value_price(sc, recs)
if uv is None:
continue
cnt = sc.count / uv
price += up * cnt
return price
async def get_sell_price(sor, sellerid, buyerid, productid, list_price):
"""
获得商品售价
"""
biz_date = await get_biz_date(sor)
agree = await get_product_agreement(sor, sellerid, buyerid, productid, biz_date)
if agree is None:
return list_price
debug(f'{agree=}')
if agree.discount:
return list_price * agree.discount
discount = await get_step_discount(sor, agree.agreedetailid, agree.step_type, selllerid, buyerid)
debug(f'step {discount=}')
return discount * list_price
async def get_price_infos(sor, sellerid, buyerid, productid, prod_config):
"""
获得商品价格
resellerid:商户id
productid:商品id
prod_config:商品配置
返回
[
{ownerid, list_price, price}, ...
]
"""
cost_pricing_infos = []
sql1 = """select a.*,
b.pricing_method,
b.apiid
from product a left join prodpricing b on a.id=b.prodid
where a.id = ${productid}$
and a.orgid = ${sellerid}$"""
prods = await sor.sqlExe(sql1, {'productid':productid, 'sellerid':sellerid})
if len(prods) < 1:
e = Exception(f'{resellerid=} {productid=} product not found')
exception(f'{e=}')
raise e
prod = prods[0]
if prod.agreeid is not None:
cost_pricing_infos = await get_price_infos(sor, prod.providerid, sellerid, prod.providerpid, prod_config)
if prod.pricing_method == '0':
# 按资源因子计费
list_price = await calc_prod_price(sor, productid, prod_config)
sell_price = await get_sell_price(sor, sellerid, buyerid, productid, list_price)
pricing_info = {
'sellerid': sellerid,
'buyerid': buyerid,
'productid': productid,
'list_price': list_price,
'sell_price': sell_price
}
elif prod.pricing_method == '1':
# 外部计费
f = get_serverenv('pricing_api')
list_price = await f(sor, prod.apiid, providerpid, prod_config)
sell_price = await get_sell_price(sor, sellerid, buyerid, productid, list_price)
pricing_info = {
'sellerid': sellerid,
'buyerid': buyerid,
'productid': productid,
'list_price': list_price,
'sell_price': sell_price
}
else:
# 供应商计费
if len(cost_pricing_infos) < 1:
e = Exception(f'{sellerid=}, {buyerid=}, {productid=} has not resell agreement')
exception(e)
raise e
list_price = cost_pricing_infos[-1]['list_price']
sell_price = await get_sell_price(sor, sellerid, buyerid, productid, list_price)
pricing_info = {
'sellerid': sellerid,
'buyerid': buyerid,
'productid': productid,
'list_price': list_price,
'sell_price': sell_price
}
if len(cost_pricing_infos) > 0:
pricing_info['cost_price'] = cost_pricing_infos[-1]['sell_price']
cost_pricing_infos.append(pricing_info)
return cost_pricing_infos
async def get_transfee(sor, resellerid, transamt, biz_date):
sql = """select * from reseller
where orgid=${resellerid}$
and enable_date<=${biz_date}$
and expire_date>${biz_date}$
"""
recs = sor.sqlExe(sql, {'resellerid':resellerid, 'biz_date':biz_date})
if len(recs) < 1:
e = Exception(f'reseller(id={resellerid}) not available at {biz_date=}')
exception(f'{e=}')
raise e
rate = recs[0].transrate
return transamt * rate

59
platformbiz/product.py Normal file
View File

@ -0,0 +1,59 @@
from appPublic.uniqueID import getID
from appPublic.log import debug
from accounting.openaccount import openRetailRelationshipAccounts
async def get_accounting_orgid(sor):
return '0'
async def agree_products_clone(sor, orgid, agreeid):
"""
adid:agreedetailid
sor:
return None
"""
sql = """select a.id,
a.name,
a.prodtypeid,
a.description,
a.prod_state,
a.product_code,
a.spec_note,
b.id as apid,
d.id as agreeid,
d.providerid,
d.resellerid
from product a, agreeproduct b, agreedetail c, agreement d
where a.id = b.providerpid
and b.resellerpid is null
and c.id = b.agreedetailid
and d.id = c.agreeid
and d.id = ${agreeid}$
and a.orgid = ${orgid}$
"""
recs = await sor.sqlExe(sql, {'agreeid':agreeid, 'orgid':orgid})
if len(recs) < 1:
debug(f'{orgid=}, {agreeid=}, {sql=}: found no data')
return
for rec in recs:
pid = getID()
d = {
"id":pid,
"name":rec.name,
"prodtypeid":rec.prodtypeid,
"orgid":rec.resellerid,
"providerid":rec.providerid,
"agreeid":rec.agreeid,
"providerpid":rec.id,
"description":rec.description,
"prod_state":rec.prod_state,
"product_code":rec.product_code,
"spec_note":rec.spec_note
}
await sor.C('product', d)
await sor.U('agreeproduct', {'id':rec.apid, 'resellerpid':pid})
async def open_agree_account(sor, providerid, sellerid):
accounting_orgid = await get_accounting_orgid(sor)
await openRetailRelationshipAccounts(sor, accounting_orgid, providerid, sellerid)

51
platformbiz/recharge.py Normal file
View File

@ -0,0 +1,51 @@
from accounting.accounting_config import Accounting
from appPublic.registerfunction import rfexe
from appPublic.log import exception, debug
from sqlor.dbpools import DBPools
from pf_pay.ali_pay import Zhifubao_Pay
from platformbiz.getdbname import get_dbname
from platformbiz.biz_order import add_recharge_log, add_recharge_order
class Recharge:
def __init__(self, customerid, userid, recharge_amt, pc_name):
self.customerid = customerid
self.userid = userid
self.recharge_amt = recharge_amt
self.pc_name = pc_name
if pc_name not in ['alipay']:
raise Exception(f'{pc_name} pay channel not implemented')
async def start_recharge(self):
return await self.start_recharge_action('RECHARGE')
async def start_recharge_reverse(self):
return await self.start_recharge_action('RECHARGE_REVERSE')
async def start_recharge_action(self, action):
db = DBPools()
dbname = get_dbname()
async with db.sqlorContext(dbname) as sor:
order = await add_recharge_order(sor, self.customerid,
self.userid,
action,
self.recharge_amt)
if order is None:
return None
rl = await add_recharge_log(sor, self.customerid,
self.userid,
action,
order.id,
order.order_date,
self.recharge_amt,
self.pc_name)
if self.pc_name == 'alipay':
z = Zhifubao_Pay()
url = await z.alipay_payment(rl.id, rl.recharge_amt, action)
return url
exception(f'exception ...........{self.pc_name}')
raise Exception(f'{self.pc_name} pay channel not implemented')
exception('Exception happend ....')

1
platformbiz/version.py Normal file
View File

@ -0,0 +1 @@
__version__ = '0.0.1'

4
pyproject.toml Normal file
View File

@ -0,0 +1,4 @@
[build-system]
requires = ["setuptools>=61", "wheel"]
build-backend = "setuptools.build_meta"

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
# git+https://git.kaiyuancloud.cn/yumoqing/rbac
# git+https://git.kaiyuancloud.cn/yumoqing/accounting
# git+https://git.kaiyuancloud.cn/yumoqing/pf_pay

8
script/roleperm.sh Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/bash
python ~/py/rbac/script/roleperm.py sage platformbiz provider sale retail_agree agreedetail agreedetailstep agreeproduct organization
python ~/py/rbac/script/roleperm.py sage platformbiz provider sale product prodpricing prodpricingtab
python ~/py/rbac/script/roleperm.py sage platformbiz reseller sale reseller retail_agree agreedetail agreedetailstep agreeproduct organization
python ~/py/rbac/script/roleperm.py sage platformbiz reseller operator provide_agree coupontype agreedetail agreedetailstep agreeproduct organization product prodpricing prodpricingtab coupontype coupon coupon_log recharge_log resource biz_order
python ~/py/rbac/script/roleperm.py sage platformbiz owner operator paychannel prodtype
python ~/py/rbac/script/roleperm.py sage platformbiz customer customer coupon coupon_log biz_order recharge_log resource

15
setup.cfg Normal file
View File

@ -0,0 +1,15 @@
[metadata]
name=platformbiz
version = 0.0.1
description = a platform business module
author = "yu moqing"
author_email = "yumoqing@gmail.com"
readme = "README.md"
license = "MIT"
[options]
packages = find:
requires_python = ">=3.8"
install_requires =
apppublic
sqlor
ahserver

52
setup.py Executable file
View File

@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
from platformbiz.version import __version__
try:
from setuptools import setup
except ImportError:
from distutils.core import setup
required = []
with open('requirements.txt', 'r') as f:
ls = f.read()
required = ls.split('\n')
with open('platformbiz/version.py', 'r') as f:
x = f.read()
y = x[x.index("'")+1:]
z = y[:y.index("'")]
version = z
with open("README.md", "r") as fh:
long_description = fh.read()
name = "platformbiz"
description = "platformbiz"
author = "yumoqing"
email = "yumoqing@gmail.com"
package_data = {}
setup(
name="platformbiz",
version=version,
# uncomment the following lines if you fill them out in release.py
description=description,
author=author,
author_email=email,
platforms='any',
install_requires=required ,
packages=[
"platformbiz"
],
package_data=package_data,
keywords = [
],
url="https://github.com/yumoqing/platformbiz",
long_description=long_description,
long_description_content_type="text/markdown",
classifiers = [
'Operating System :: OS Independent',
'Programming Language :: Python :: 3',
'License :: OSI Approved :: MIT License',
],
)

BIN
tables.xlsx Normal file

Binary file not shown.

View File

@ -0,0 +1,26 @@
from testenv import runtest
from appPublic.dictObject import DictObject
from platformbiz.pricing import get_price_infos
async def test(sor):
# prodid = 'a--akNeu1Ia-NOZAJAadf'
prodid = '1wleQ7o-RkrSbR-n368bO'
buyerid = 'yyoJwfBDsRiwN8-WFl2q1'
sellerid = '0'
spec_config = [
DictObject(**{
"spec_name":"input_tokens",
"count":12832
}),
DictObject(**{
"spec_name":"output_tokens",
"count":786323
})
]
d = await get_price_infos(sor, sellerid, buyerid, prodid, spec_config)
print(f'{d=}')
if __name__ == '__main__':
runtest(test)

View File

@ -0,0 +1,12 @@
from testenv import runtest
from appPublic.dictObject import DictObject
from platformbiz.product import open_agree_account
async def test(sor):
orgid = 'ac4HnQ0txGqmbAFWpI3wp'
agreeid = 'NUvV1sP9TtAdnlxCISzjo'
await open_agree_account(sor, orgid, '0')
if __name__ == '__main__':
runtest(test)

12
test/test_prodclone.py Normal file
View File

@ -0,0 +1,12 @@
from testenv import runtest
from appPublic.dictObject import DictObject
from platformbiz.product import agree_products_clone
async def test(sor):
orgid = 'ac4HnQ0txGqmbAFWpI3wp'
agreeid = 'NUvV1sP9TtAdnlxCISzjo'
await agree_products_clone(sor, orgid, agreeid)
if __name__ == '__main__':
runtest(test)

23
test/test_prodpricing.py Normal file
View File

@ -0,0 +1,23 @@
from testenv import runtest
from appPublic.dictObject import DictObject
from platformbiz.pricing import calc_prod_price
async def test(sor):
# prodid = 'a--akNeu1Ia-NOZAJAadf'
prodid = 'MT-k34zPUAuWqgov4QCmG'
spec_config = [
DictObject(**{
"spec_name":"input_tokens",
"count":12832
})#,
#DictObject(**{
# "spec_name":"output_tokens",
# "count":786323
#})
]
price = await calc_prod_price(sor, prodid, spec_config)
print(f'{prodid=}, {spec_config=}, {price=}')
if __name__ == '__main__':
runtest(test)

23
test/testenv.py Normal file
View File

@ -0,0 +1,23 @@
import os
import asyncio
from appPublic.jsonConfig import getConfig
from sqlor.dbpools import DBPools
from appbase.init import load_appbase
from accounting.init import load_accounting
from platformbiz.init import load_platformbiz
async def main(asyncfunc):
home = os.environ['HOME']
p = f'{home}/py/sage'
config = getConfig(p, {'workdir':p})
db = DBPools(config.databases)
load_appbase()
load_accounting()
load_platformbiz()
async with db.sqlorContext('sage') as sor:
await asyncfunc(sor)
def runtest(asyncfunc):
asyncio.new_event_loop().run_until_complete(main(asyncfunc))

View File

@ -0,0 +1,9 @@
debug(f'{params_kw=}')
orgid = await get_userorgid()
db = DBPools()
dbname = get_module_dbname('platformbiz')
async with db.sqlorContext(dbname) as sor:
await agree_products_clone(sor, orgid, params_kw.id)
return UiMessage(title='product clone', message='product clone finished')
debug('product clone error')
return UiError(title='product clone', message='product clone error')

View File

@ -0,0 +1,7 @@
debug(f'{params_kw=}')
db = DBPools()
dbname = await get_module_dbname('platformbiz')
async with db.sqlorContext(dbname) as sor:
await agreedetail_products_clone(params_kw.agreedetailid)
return UiMessage(title='clone product', message='OK')
return UiError(title='Product clone', message='Product clone error')

View File

@ -0,0 +1,7 @@
debug(f'{params_kw=}')
db = DBPools()
dbname = await get_module_dbname('platformbiz')
async with db.sqlorContext(dbname) as sor:
await agreedetail_products_clone(sor, params_kw.agreedetailid)
return UiMessage(title='clone product', message='OK')
return UiError(title='Product clone', message='Product clone error')

27
wwwroot/menu.ui Normal file
View File

@ -0,0 +1,27 @@
{% set roles = get_user_roles(get_user()) %}
{
"widgettype":"Menu",
"options":{
"target":"page_center",
"cwidth":10,
"items":[
{% if 'reseller.operator' in roles %}
{
"name":"provider",
"label":"供应商管理"
"url":"{{entire_url('/platformbiz/provider')}}"
}
{% endif %}
{% if 'reseller.sale' in roles %}
{
"name":"reseller",
"label":"分销商管理",
"url":"{{entire_url('/platformbiz/reseller'}}"
}
{% endif %}
{% if 'reseller.accountant' in roles %}
{% endif %}
{}
]
}
}

View File

@ -0,0 +1,18 @@
username = params_kw.username
db = DBPools()
dbname = await rfexe('get_module_dbname', 'platformbiz')
async with db.sqlorContext(dbname) as sor:
if username:
sql = "select * from users where username = ${username}$"
recs = await sor.sqlExe(sql, {'username':username})
if len(recs) > 0:
userorgid = recs[0].orgid
else:
e = Exception(f'{user}:user not found')
exception(f'Eeception:{e}')
raise e
else:
userorgid = await get_userorgid()
await openCustomerAccounts(sor, '0', userorgid)
return "开帐成功"
return "开帐失败"

View File

@ -0,0 +1,6 @@
db = DBPools()
dbname = await rfexe('get_module_dbname', 'accounting')
async with db.sqlorContext(dbname) as sor:
await openOwnerAccounts(sor, '0')
return 'OK'
return 'error'

View File

@ -0,0 +1,33 @@
[
{
"name":"agree",
"label":"供应商管理",
"url":"{{entire_url('/platformbiz/provider')}}"
},
{
"name":"paychannel",
"label":"支付渠道",
"url":"{{entire_url('paychannel')}}"
},
{
"name":"prodtype",
"label":"产品类型",
"url":"{{entire_url('/platformbiz/prodtype')}}"
},
{
"name":"recharge",
"label":"充值",
"items":[
{
"name":"recharge_log",
"label":"充值日志"
"url":"{{entire_url('/platformbiz/recharge_log')}}"
},
{
"name":"recharge_4u",
"label":"充值错账处理",
"url":"{{entire_url('/platformbiz/rechange_accounting.ui')}}"
}
]
}
]

17
wwwroot/recharge.dspy Normal file
View File

@ -0,0 +1,17 @@
debug(f'{params_kw=}')
try:
userid = await get_user()
userorgid = await get_userorgid()
r = Recharge(userorgid, userid, float(params_kw.recharge_amt), params_kw.channel)
url = await r.start_recharge()
return {
"widgettype":"NewWindow",
"options":{
"url":url
}
}
except Exception as e:
es = format_exc()
exception(f'{e=}, {es}')
return UiError(title='Error', message=f'Error:{e},trackback={es}')

38
wwwroot/recharge.ui Normal file
View File

@ -0,0 +1,38 @@
{
"widgettype":"Form",
"options":{
"fields":[
{
"name":"recharge_amt",
"uitype":"float",
"required":true,
"label":"充值金额"
},
{
"name":"channel",
"label":"选择充值方式",
"uitype":"checkbox",
"multicheck":false,
"required":true,
"value":"alipay",
"data":[
{
"value":"alipay",
"text":"支付宝"
}
]
}
]
},
"binds":[
{
"wid":"self",
"event":"submit",
"actiontype":"urlwidget",
"target":"self",
"options":{
"url":"{{entire_url('recharge.dspy')}}"
}
}
]
}

View File

@ -0,0 +1,30 @@
[
{
"name":"product",
"label":"产品管理",
"url":"{{entire_url('/platformbiz/product')}}"
},
{
"name":"coupon",
"label":"代金券",
"items":[
{
"name":"coupontype",
"label":"代金券设置"
"url":"{{entire_url('/platformbiz/coupontype')}}"
},
{
"name":"coupon_issue",
"label":"代金券发放",
"url":"{{entire_url('/platformbiz/coupon_issue.ui')}}"
},
{
"name":"coupon",
"label":"代金券管理",
"url":"{{entire_url('/platformbiz/coupon')}}"
}
]
}
]