├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── docs ├── 1、注册登录用户 │ ├── README.md │ ├── 找回密码.png │ ├── 注册用户.png │ └── 登录用户.png ├── 2、业务管理 │ ├── README.md │ └── 新建业务 │ │ ├── 业务列表.png │ │ ├── 创建业务-1.png │ │ ├── 创建业务-2.png │ │ ├── 创建业务-3.png │ │ ├── 创建业务-4.png │ │ ├── 创建业务-5.png │ │ └── 创建业务-6.png ├── 3、业务权限 │ ├── README.md │ ├── 业务用户管理 │ │ ├── 保存业务角色.png │ │ ├── 分配业务角色.png │ │ ├── 完成添加业务.png │ │ ├── 完成角色分配.png │ │ ├── 新增业务用户.png │ │ └── 选中业务用户.png │ ├── 菜单管理 │ │ ├── 保存成功.png │ │ ├── 保存菜单.png │ │ ├── 保存菜单2.png │ │ ├── 创建二级菜单.png │ │ └── 创建菜单.png │ ├── 角色管理 │ │ ├── 修改角色.png │ │ ├── 删除角色.png │ │ ├── 新建角色.png │ │ └── 角色命名.png │ └── 角色菜单管理 │ │ ├── 修改角色菜单.png │ │ ├── 修改角色菜单2.png │ │ └── 角色菜单页面.png ├── 4、业务数据 │ ├── README.md │ └── 图表编辑 │ │ ├── 全局过滤组件拖拽.png │ │ ├── 报表组件保存成功.png │ │ ├── 报表组件拖拽.png │ │ ├── 报表组件拖拽成功.png │ │ ├── 报表组件设置编辑.png │ │ ├── 编辑全局过滤组件.png │ │ ├── 编辑新页面.png │ │ ├── 过滤组件保存成功.png │ │ ├── 过滤组建联动.png │ │ └── 进入编辑页面.png ├── README.md └── 其他图片 │ ├── 默认业务图.png │ └── 默认首页.png ├── requirements-base.txt ├── setup.py └── src └── seed ├── __init__.py ├── api ├── __init__.py ├── _base.py ├── common.py ├── endpoints │ ├── __init__.py │ ├── account.py │ ├── buser.py │ ├── buserrole.py │ ├── buserselect.py │ ├── bussiness.py │ ├── databases.py │ ├── filters.py │ ├── menu.py │ ├── pages.py │ ├── panels.py │ ├── queries.py │ ├── role.py │ ├── rolemenu.py │ └── user.py ├── front.py ├── urls.py ├── users │ ├── __init__.py │ ├── active_account.py │ ├── login.py │ ├── register.py │ └── reset_password.py └── utils │ ├── __init__.py │ ├── dbtest.py │ ├── dbtypes.py │ ├── files.py │ └── sql_analyze.py ├── cache ├── __init__.py ├── active_account.py ├── auth.py ├── base.py ├── redis.py ├── reset_password.py ├── session.py └── user_bussiness.py ├── conf ├── __init__.py └── server.py ├── data └── config │ ├── config.yaml.default │ └── seed_conf.py.default ├── drives ├── __init__.py ├── base.py ├── email.py ├── impala.py ├── mysql.py └── postgresql.py ├── libs ├── __init__.py ├── data_access │ ├── __init__.py │ ├── app.py │ ├── dataware │ │ ├── __init__.py │ │ └── base.py │ ├── formatters │ │ ├── __init__.py │ │ ├── bar.py │ │ ├── base.py │ │ ├── funnel.py │ │ ├── line.py │ │ ├── linestack.py │ │ ├── map.py │ │ ├── pie.py │ │ ├── sankey.py │ │ └── table.py │ ├── middledata │ │ ├── __init__.py │ │ └── base.py │ └── utils │ │ ├── __init__.py │ │ └── auto_register.py └── filter_access │ ├── __init__.py │ └── app.py ├── logging ├── README.md ├── __init__.py └── handlers.py ├── models ├── __init__.py ├── _base.py ├── account.py ├── analogdatas.py ├── bmanager.py ├── buser.py ├── buserrole.py ├── bussiness.py ├── databases.py ├── filters.py ├── init.py ├── menu.py ├── panels.py ├── role.py └── rolemenu.py ├── runner ├── __init__.py ├── commands │ ├── __init__.py │ ├── init.py │ ├── run.py │ └── upgrade.py ├── initializer.py └── setting.py ├── schema ├── __init__.py └── base.py ├── services ├── __init__.py ├── app.py └── wsgi.py ├── static ├── 3rdpartylicenses.txt ├── OpenSans-Regular.629a55a7e793da068dc5.ttf ├── assets │ ├── compConfig │ │ ├── compConfig.js │ │ └── reportConfig.js │ ├── fonts │ │ ├── LICENSE.txt │ │ ├── OpenSans-Bold.ttf │ │ ├── OpenSans-BoldItalic.ttf │ │ ├── OpenSans-ExtraBold.ttf │ │ ├── OpenSans-ExtraBoldItalic.ttf │ │ ├── OpenSans-Italic.ttf │ │ ├── OpenSans-Light.ttf │ │ ├── OpenSans-LightItalic.ttf │ │ ├── OpenSans-Regular.ttf │ │ ├── OpenSans-Semibold.ttf │ │ └── OpenSans-SemiboldItalic.ttf │ ├── images │ │ ├── arrow.png │ │ ├── avatar.jpg │ │ ├── bgt.jpg │ │ ├── bt.jpg │ │ ├── businessIcon.png │ │ ├── businessNo.png │ │ ├── cloudPlane.png │ │ ├── dbqst.jpg │ │ ├── dbt.jpg │ │ ├── drq.jpg │ │ ├── giphy1.gif │ │ ├── ldt.jpg │ │ ├── line.gif │ │ ├── loading-bars.svg │ │ ├── loading-spinning-bubbles.svg │ │ ├── loading.gif │ │ ├── loginIcon.png │ │ ├── logo.png │ │ ├── logo1.png │ │ ├── mans.png │ │ ├── map.jpg │ │ ├── password-meter.png │ │ ├── sankey.jpg │ │ ├── srq.jpg │ │ ├── superman.png │ │ ├── userFace.png │ │ ├── xldx.jpg │ │ ├── xlfx.jpg │ │ └── zxt.jpg │ ├── js │ │ ├── bowser.min.js │ │ └── jquery.min.js │ └── theme │ │ └── fonts │ │ ├── roboto-v15-latin-regular.eot │ │ ├── roboto-v15-latin-regular.svg │ │ ├── roboto-v15-latin-regular.ttf │ │ ├── roboto-v15-latin-regular.woff │ │ └── roboto-v15-latin-regular.woff2 ├── favicon.ico ├── fontawesome-webfont.674f50d287a8c48dc19b.eot ├── fontawesome-webfont.912ec66d7572ff821749.svg ├── fontawesome-webfont.af7ae505a9eed503f8b8.woff2 ├── fontawesome-webfont.b06871f281fee6b241d6.ttf ├── fontawesome-webfont.fee66e712a8a08eef580.woff ├── index.html ├── inline.07bd3aeef3ad0ebf96d7.bundle.js ├── line.567f57385ea3dde2c9ae.gif ├── loading-bars.0e7c657c2cbc1629a789.svg ├── main.04553bc590534d61b1af.bundle.js ├── mans.1302759b8be148f12915.png ├── polyfills.71faae03c9efad617caa.bundle.js ├── scripts.3905038f780eac8a5c4d.bundle.js ├── styles.2bd39d9150bfd833564f.bundle.css └── vendor.effac60f981278e0c4fd.bundle.js └── utils ├── __init__.py ├── auth.py ├── database.py ├── distutils ├── __init__.py ├── base.py └── build_assets.py ├── file.py ├── helper.py ├── imports.py ├── mail.py ├── permissions.py ├── response.py └── time.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled python files. 2 | *.py[cod] 3 | build 4 | dist 5 | 6 | 7 | # Packages. 8 | __pycache__ 9 | *.egg-info 10 | migrations 11 | 12 | 13 | # Temporary files. 14 | *~ 15 | *.swp 16 | 17 | # Hidden files. 18 | .* 19 | !.gitmodules 20 | !.gitignore 21 | 22 | *.log 23 | config.py 24 | 25 | sftp-config.json 26 | self_script 27 | *.rdb 28 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "seed_static"] 2 | path = seed_static 3 | url = https://github.com/BoyaaDataCenter/seed_static.git 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # 项目概述 3 | Seed自助数据展示系统只是一款简易的BI系统,它方便快捷,可以通过简易的拖拽并配置报表,使只会SQL的统计人员都能快速搭建出属于自己的数据可视化报表。 4 |
5 | 首页 6 | 默认业务 7 |
8 | 9 | 10 | ## 测试网址 11 | http://seed.boyaa.com 12 | 13 | 测试账号: admin 密码: admin123 14 | 15 | ## 系统操作手册 16 | [如何操作Seed自助数据展示系统](docs/README.md) 17 | 18 | ## 如何安装 19 | 1. 需要环境 20 | ``` 21 | 系统环境: Linux, Mac和Windows 22 | 运行环境: Python3.5+ 23 | 其他软件: Redis, MySQL(Postgresql) 24 | 25 | 注: 数据库一定要使用空库。 26 | ``` 27 | 2. 安装 28 | - pip安装 29 | ``` 30 | pip install boyaa-seed 31 | ``` 32 | 33 | - 编译安装 34 | ``` 35 | 获取代码 git clone git@github.com:BoyaaDataCenter/seed.git 36 | 进入文件夹 cd seed 37 | 编译代码 python setup.py install 38 | ``` 39 | 3. 初始化seed的config文件 40 | ``` 41 | 执行seed init 42 | ``` 43 | 4. config文件设置 44 | ``` 45 | 打开用户根目录下的.seed/seed_conf.py文件 46 | 进行数据库等相关的配置 47 | 如: vim ~/.seed/seed_conf.py 48 | ``` 49 | 5. 初始化数据库 50 | ``` 51 | 进行数据库初始化, 执行 52 | seed upgrade 53 | 即可 54 | ``` 55 | 6. 运行web程序 56 | ``` 57 | 执行 seed run web运行web系统 58 | 注:因uwsgi模块不支持Windows,故只能以开发模式运行:seed run web --debug=True 59 | ``` 60 | 7. 访问 61 | ``` 62 | 127.0.0.1:5000 可访问系统 63 | ``` 64 | 65 | ## 如何升级 66 | 1. 获取到最新代码 67 | 2. 打包seed 68 | ``` 69 | 进入seed项目根目录 70 | 运行 python setup.py install 71 | ``` 72 | 3. 运行web程序 73 | ``` 74 | 执行 seed run web运行web系统 75 | ``` 76 | 4. 访问 77 | ``` 78 | 127.0.0.1:5000 可访问系统 79 | ``` 80 | 81 | ## 开发模式 82 | 1. 安装seed的pip运行文件到第三方库中 83 | ``` 84 | python setup.py develop 85 | ``` 86 | 2. 运行seed数据 87 | ``` 88 | seed init 89 | ``` 90 | 3. 设置数据库 91 | ``` 92 | vim ~/.seed/seed_config.py 93 | ``` 94 | 4. 运行web 95 | ``` 96 | seed run web --debug=True 97 | ``` 98 | 99 | 100 | ## 图形说明 101 | 102 | ### 1、桑基图使用规范 103 | 桑基图源数据需可以将数据按照以下形式组合: 104 | ``` 105 | selet source, target, value from table 106 | union all 107 | selet source, target, value from table 108 | ``` 109 | (上面一条sql的target需要和下一条sql的source相同,否则就不能形成桑基图形式 110 | source, target 这两个字段别名已固定) 111 | 112 | 113 | ##### 数据示例说明(以mysql为例): 114 | ``` 115 | CREATE TABLE `sankey_testdata` ( 116 | `state` varchar(20) , 117 | `address` varchar(20) , 118 | `province` varchar(20) , 119 | `value` int 120 | ) ENGINE=innodb DEFAULT CHARSET=utf8; 121 | ``` 122 | ``` 123 | insert into sankey_testdata values 124 | ('东部地区', '东三省', '黑龙江',246), 125 | ('东部地区', '东三省', '吉林', 319), 126 | ('东部地区', '东三省', '辽宁', 871), 127 | ('东部地区', '华南', '广东', 323), 128 | ('东部地区', '华南', '广西', 250), 129 | ('东部地区', '华南', '海南', 431), 130 | ('东部地区', '华南', '福建', 236), 131 | ('东部地区', '华南', '香港', 334), 132 | ('东部地区', '华南', '澳门', 544), 133 | ('东部地区', '华南', '台湾', 915), 134 | ('东部地区', '环渤海', '北京', 687), 135 | ('东部地区', '环渤海', '天津', 340), 136 | ('东部地区', '环渤海', '内蒙古',234), 137 | ('东部地区', '环渤海', '河北', 282), 138 | ('东部地区', '环渤海', '山东', 102), 139 | ('东部地区', '长三角', '上海', 201), 140 | ('东部地区', '长三角', '江苏', 717), 141 | ('东部地区', '长三角', '浙江', 669), 142 | ('西部地区', '西北', '青海', 335), 143 | ('西部地区', '西北', '甘肃', 357), 144 | ('西部地区', '西北', '宁夏', 456), 145 | ('西部地区', '西北', '山西', 119), 146 | ('西部地区', '西北', '新疆', 984), 147 | ('西部地区', '西南', '云南', 611); 148 | ``` 149 | ##### 桑基图查询SQL示例: 150 | ``` 151 | SELECT 152 | state as source, 153 | address as target, 154 | sum(value) as value 155 | FROM sankey_testdata 156 | GROUP BY state,address 157 | union all 158 | SELECT 159 | address as source, 160 | province as target , 161 | sum(value) as value 162 | FROM sankey_testdata 163 | GROUP BY address,province 164 | ``` 165 | 166 | 167 | ### 2、地图使用规范 168 | 169 | 地图源数据至少需要以下字段 170 | ``` 171 | 经度 172 | 纬度 173 | 区域名称 174 | 区域id 175 | 区域上级id 176 | 区域级别id(注:区域级别需按以下分类,否则可能导致数据显示混乱) 177 | 1-国家 178 | 2-省份 179 | 3-地市 180 | 4-区县/乡镇(街道) 181 | 7-社区 182 | 8-具体位置 183 | ``` 184 | 185 | ##### 数据示例说明(以mysql为例): 186 | ``` 187 | CREATE TABLE `map_testdata` ( 188 | `fdate` date, 189 | `fid` int comment '区域id', 190 | `region_name` varchar(100) comment '区域名称', 191 | `fpid` int comment '区域上级id', 192 | `region_id` int comment '区域级别id', 193 | `value1` int, 194 | `value2` int, 195 | `value3` int, 196 | `lat` varchar(100) comment '经度', 197 | `lng` varchar(100) comment '纬度' 198 | ) ENGINE=innodb DEFAULT CHARSET=utf8; 199 | ``` 200 | ``` 201 | insert into map_testdata values 202 | ('2019-05-08',1,'中国',0,1,25000,10000,20000,'37.550339','104.114129'), 203 | ('2019-05-08',107712,'广东省',1,2,5000,3000,1000,'23.408003729025','113.39481755876'), 204 | ('2019-05-08',112083,'深圳市',107712,3,3000,1000,1500,'22.546053546205','114.02597365732'), 205 | ('2019-05-08',112313,'南山区',112083,4,2000,300,200,'22.558887751083','113.95072266574'), 206 | ('2019-05-08',null,null,'112313',7,2,2,2,'22.5557455','114.026432'), 207 | ('2019-05-08',null,null,'112313',7,40,40,40,'22.5658103','114.0948389'), 208 | ('2019-05-08',null,null,'112313',7,166,166,166,'22.53615273','114.1144042'), 209 | ('2019-05-08',null,null,'112313',7,80,80,80,'22.5232017','114.0353637'), 210 | ('2019-05-08',null,null,'112313',7,14,14,14,'22.53781229','114.1248561'), 211 | ('2019-05-08',null,null,'112313',7,1,1,1,'22.578339','114.140053'), 212 | ('2019-05-08',null,null,'112313',7,1,1,1,'22.52957','114.200043'), 213 | ('2019-05-08',null,null,'112313',7,33,33,33,'22.60465685','114.1245172'), 214 | ('2019-05-08',null,null,'112313',7,154,154,154,'22.5465231','114.0245698'), 215 | ('2019-05-08',null,null,'112313',7,1,1,1,'22.530012','114.199852'), 216 | ('2019-05-08',null,null,'112313',7,12,12,12,'22.6010765','113.8757611'), 217 | ('2019-05-08',null,null,'112313',7,5,5,5,'22.5274626','114.195732'), 218 | ('2019-05-08',null,null,'112313',7,3,3,3,'22.58916333','113.9847973'), 219 | ('2019-05-08',null,null,'112313',7,2,2,2,'22.510313','114.146149'), 220 | ('2019-05-08',null,null,'112313',7,52,52,52,'22.56592831','114.1653427'), 221 | ('2019-05-08',null,null,'112313',7,6,6,6,'22.5230185','113.8840307'), 222 | ('2019-05-08',null,null,'112313',7,8,8,8,'22.55479675','113.876808'), 223 | ('2019-05-08',null,null,'112313',7,2,2,2,'22.527546','114.1382295'), 224 | ('2019-05-08',null,null,'112313',7,20,20,20,'22.60403745','113.8813462'), 225 | ('2019-05-08',null,null,'112313',7,3,3,3,'22.53090833','114.0340117'), 226 | ('2019-05-08',null,null,'112313',7,24,24,24,'22.52259221','114.1844415'), 227 | ('2019-05-08',null,null,'112313',7,6,6,6,'22.56845417','113.8482768'), 228 | ('2019-05-08',null,null,'112313',7,15,15,15,'22.50921313','114.1460804'), 229 | ('2019-05-08',null,null,'112313',7,1,1,1,'22.52459','113.873619'), 230 | ('2019-05-08',null,null,'112313',7,54,54,54,'22.51644502','113.9074763'), 231 | ('2019-05-08',null,null,'112313',7,110,120,135,'22.54528264','113.9450687'); 232 | ``` 233 | 234 | 235 | ##### 地图查询SQL示例: 236 | 以下SQL中的{}表示变量,使用地图时 237 | region_id,slat,elat,slng,elng,fpid均为必须参数 238 | 239 | postgresql写法: 240 | ``` 241 | SELECT 242 | a.region_name, 243 | a.fid as fpid, 244 | a.lat, 245 | a.lng, 246 | a.value1, 247 | a.value2, 248 | a.value3 249 | from map_testdata a 250 | WHERE region_id = {region_id} 251 | AND cast(lat AS decimal(20, 10)) > {slat} 252 | AND cast(lat AS decimal(20, 10))< {elat} 253 | AND cast(lng AS decimal(20, 10)) > {slng} 254 | AND cast(lng AS decimal(20, 10))< {elng} 255 | AND CASE cast( {fpid} AS bool ) WHEN TRUE THEN fpid={fpid} ELSE 1=1 END 256 | ``` 257 | 258 | mysql写法: 259 | ``` 260 | SELECT 261 | a.region_name, 262 | a.fid as fpid, 263 | a.lat, 264 | a.lng, 265 | a.value1, 266 | a.value2, 267 | a.value3 268 | from map_testdata a 269 | WHERE region_id = {region_id} 270 | AND cast(lat AS decimal(20, 10)) > {slat} 271 | AND cast(lat AS decimal(20, 10))< {elat} 272 | AND cast(lng AS decimal(20, 10)) > {slng} 273 | AND cast(lng AS decimal(20, 10))< {elng} 274 | AND CASE {fpid}=true WHEN TRUE THEN fpid={fpid} ELSE 1=1 END 275 | ``` -------------------------------------------------------------------------------- /docs/1、注册登录用户/README.md: -------------------------------------------------------------------------------- 1 | # 注册和登录用户 2 | Seed系统目前只支持账户密码登录的方式,下面将介绍如何进行注册,登录 和 找回密码。 3 | 4 | 注册新系统的第一个用户既为超级管理员,超级管理员有所有的权限。之后的用户即为普通用户,超级管理员可以通过系统里面的设置来更改其他用户的权限。 5 | 6 | ## 注册 7 | 输入Seed系统对应的域名会进入登陆页面,可以通过登陆页面的下方进入注册页面。如下图所示。 8 | ![注册用户](./注册用户.png) 9 | 注册内容和平时的网站一样,唯一需要值得注意的是,如果当前系统配置了可用的邮件服务配置,注册之后将会通过 邮件验证 来激活账户,才能正常的使用当前账户。 10 | 11 | 建议无论邮件服务配置是否开启,都建议使用自己有效可用的邮箱来注册。 12 | 13 | ## 登录 14 | 输入Seed系统对应的域名,在没有登陆的状态,将会直接跳转到登陆页面。登录页面入下图所示。 15 | ![登陆用户](./登录用户.png) 16 | 17 | ## 找回密码 18 | **注意: 找回密码因为涉及到邮件发送,只有在邮件服务配置成功的时候才能使用** 19 | 如果用户忘记了密码,可以通过点击登录页面的忘记密码进入,密码找回页面。密码找回页面如下图所示 20 | ![找回密码](./找回密码.png) 21 | 在输入账号或者邮箱之后, 系统会通过配置的邮件服务,发送找回密码的邮件给用户,让用户通过邮件中的链接输入新的密码即可。 -------------------------------------------------------------------------------- /docs/1、注册登录用户/找回密码.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/1、注册登录用户/找回密码.png -------------------------------------------------------------------------------- /docs/1、注册登录用户/注册用户.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/1、注册登录用户/注册用户.png -------------------------------------------------------------------------------- /docs/1、注册登录用户/登录用户.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/1、注册登录用户/登录用户.png -------------------------------------------------------------------------------- /docs/2、业务管理/README.md: -------------------------------------------------------------------------------- 1 | # 业务管理 2 | Seed系统是通过业务将不同属性的报表区分开的。 3 | 4 | 每个游戏工作室的报表需求不一样,那么可以通过给每个游戏工作室分配不同的业务。将对应业务的权限交给对应工作室的负责做统计报表的工作人员,那么对应的工作人员就可以在自己工作室的业务下进行相关的业务报表配置。 5 | 6 | ## 新建和编辑业务 7 | 用户登录进入系统之后,就可以进入业务选择的页面。和普通用户不一样的是,超级管理员拥有对业务添加的权限,分配的业务管理员拥有对业务编辑的权限。 8 | 由于新建业务和编辑业务是同样的操作逻辑,这里将会一并介绍。 9 | 10 | ### 业务列表 11 | 业务列表 如下图所示 12 | ![业务列表](新建业务/业务列表.png) 13 | 只有超级管理员才能看到新建业务, 超级管理员可以通过 点击 新建业务进入新建业务的业务逻辑。 14 | 15 | 超级管理员和业务管理员都可以通过点击某个业务右上方的配置按钮 对当前业务已有的配置进行编辑。 16 | 17 | 业务配置总共分三步,分别是业务概况,业务数据库配置和业务数据保存。 18 | 19 | ### 业务概况设置 20 | ![创建业务1](新建业务/创建业务-1.png) 21 | 由上图所示,业务概况需要配置业务名称,业务概要 和 选择当前业务的业务管理员。 22 | 业务名称和业务概要主要是用来介绍当前业务的主要属性。 23 | 24 | 业务管理员则是指定当前业务所有权限归谁使用,业务管理员可以指定多个人,因为超级管理员级别在业务管理员之上,在配置的时候,可以不用配置超级管理员为业务管理员。 25 | 26 | ### 业务数据库设置 27 | ![创建业务2](新建业务/创建业务-2.png) 28 | 接下来进入业务数据库配置,业务数据库主要是在配置报表和展示报表时的取数,如果当前业务的报表数据需要从PostgreSQL A中获取,则需要在这里配置PostgreSQL A的连接配置。 29 | 30 | 点击添加业务数据库,则可以进入业务数据库配置页面。如下图所示 31 | ![创建业务3](新建业务/创建业务-3.png) 32 | 需要提前选择配置数据库的类型,目前只支持MySQL和PostgreSQL两种常见的关系型数据库。然后就是常规的数据库连接配置了。可以在配置完之后,点一下测试连接。当测试连接成功之后,则可以点击保存。 33 | 34 | 新建业务数据库成功后,则返回了业务数据库列表。可以看出业务可以支持多个数据库的连接。 35 | ![创建业务4](新建业务/创建业务-4.png) 36 | 37 | ### 业务数据保存 38 | 在配置完业务数据库之后,点击下一步,将会将当前业务的所有配置展示在最后一步 39 | ![创建业务5](新建业务/创建业务-5.png) 40 | 41 | 点击完成,则添加了新的业务到业务列表中 42 | ![创建业务5](新建业务/创建业务-6.png) 43 | 44 | ## 删除业务 45 | 删除按钮则可以通过业务上方的删除按钮进行删除。 46 | 47 | ## 备注 48 | 暂无 -------------------------------------------------------------------------------- /docs/2、业务管理/新建业务/业务列表.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/2、业务管理/新建业务/业务列表.png -------------------------------------------------------------------------------- /docs/2、业务管理/新建业务/创建业务-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/2、业务管理/新建业务/创建业务-1.png -------------------------------------------------------------------------------- /docs/2、业务管理/新建业务/创建业务-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/2、业务管理/新建业务/创建业务-2.png -------------------------------------------------------------------------------- /docs/2、业务管理/新建业务/创建业务-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/2、业务管理/新建业务/创建业务-3.png -------------------------------------------------------------------------------- /docs/2、业务管理/新建业务/创建业务-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/2、业务管理/新建业务/创建业务-4.png -------------------------------------------------------------------------------- /docs/2、业务管理/新建业务/创建业务-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/2、业务管理/新建业务/创建业务-5.png -------------------------------------------------------------------------------- /docs/2、业务管理/新建业务/创建业务-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/2、业务管理/新建业务/创建业务-6.png -------------------------------------------------------------------------------- /docs/3、业务权限/README.md: -------------------------------------------------------------------------------- 1 | # 业务权限 2 | 对于业务的每一个数据都是敏感的,Seed系统做了针对页面的权限处理。每个有业务权限的用户都有不同的角色,每个角色关联了不同的页面。 3 | 4 | ## 菜单管理 5 | 菜单管理是用来对自定义的数据报表页面进行配置。 6 | 7 | 可以通过管理中心-菜单管理 进入到配置页面。 8 | ![创建菜单](菜单管理/创建菜单.png) 9 | 10 | 点击页面右上角的 添加一级菜单 可以添加一个新的一级菜单, 在弹出来的对话框中填入菜单的名字,点击保存。 11 | ![保存成功](菜单管理/保存菜单.png) 12 | 13 | 通过点击刚刚创建的 游戏玩家 的一级菜单对应右边的+ 创建二级菜单。 14 | ![创建二级菜单](菜单管理/保存成功.png) 15 | 16 | 通过同样的步骤,可以创建出最终入下图的菜单列表。 17 | ![保存菜单](菜单管理/保存菜单2.png) 18 | 19 | **注意: 真的的保存需要点击右上方的 保存修改 按钮才是正在的保存成功** 20 | 21 | ## 角色管理 22 | 可以通过管理中心-角色管理 进入 角色管理页面。 23 | 24 | ### 新增角色 25 | 点击页面右上方的 新增角色 添加新的角色。 26 | ![新增角色](角色管理/新建角色.png) 27 | 28 | 然后进入弹出角色命名对话框 29 | ![角色命名](角色管理/角色命名.png) 30 | 31 | 点击保存,成功添加角色。 32 | 33 | ### 修改角色 34 | 点击对应角色右边的编辑按钮,弹出 编辑角色 对话框。 35 | ![修改角色](角色管理/修改角色.png) 36 | 37 | 点击保存,成功修改角色 38 | 39 | ### 删除角色 40 | 点击对应角色右边的删除按钮即可。 41 | ![删除角色](角色管理/删除角色.png) 42 | 43 | ## 角色菜单关联 44 | 之前介绍了角色 和 菜单 的管理,那么接下来则是将角色和菜单之间建立关联。 45 | 46 | 点击页面的管理中心-角色菜单管理进入 角色菜单管理页面。 47 | ![角色菜单页面](角色菜单管理/角色菜单页面.png) 48 | 49 | 点击上方的角色按钮,则可以跳转到对应角色关联的菜单页面,点击是否可见对应页面的按钮,则可以开启当前角色对应页面的查看权限。 50 | 51 | ![修改角色菜单](角色菜单管理/修改角色菜单2.png) 52 | 修改财务角色对应的菜单页面查看角色如图。 53 | 54 | **注意: 真的的保存需要点击右上方的 保存修改 按钮才是正在的保存成功** 55 | 56 | ## 业务用户管理 57 | 管理员需要将需要看数据的用户添加到业务中,并分配对应的角色,这样普通用户才能有对应业务的权限和能看到对应页面的数据。 58 | 59 | 可以通过管理中心-业务用户管理 进入 业务用户管理页面 60 | 61 | ### 新增业务用户 62 | 点击右上方 添加业务用户 63 | ![新增业务用户](业务用户管理/新增业务用户.png) 64 | 65 | 弹出用户选择页面,可以进行选择多个用户,然后点击保存。 66 | ![选中业务用户](业务用户管理/选中业务用户.png) 67 | 68 | 点击保存后,用户成功添加到业务。 69 | ![完成添加业务](业务用户管理/完成添加业务.png) 70 | 71 | ### 分配用户角色 72 | 每添加一个业务用户,需要给对应的用户分配用户角色,才能看到角色对应有权限的页面。 73 | 74 | 点击用户对应的编辑按钮,弹出用户角色的分配对话框 75 | ![分配业务角色](业务用户管理/分配业务角色.png) 76 | 77 | 选择对应的角色,并保存 78 | ![保存业务角色](业务用户管理/保存业务角色.png) 79 | 80 | 完成用户的角色分配 81 | ![完成角色分配](业务用户管理/完成角色分配.png) 82 | ## 备注 83 | 暂无 -------------------------------------------------------------------------------- /docs/3、业务权限/业务用户管理/保存业务角色.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/业务用户管理/保存业务角色.png -------------------------------------------------------------------------------- /docs/3、业务权限/业务用户管理/分配业务角色.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/业务用户管理/分配业务角色.png -------------------------------------------------------------------------------- /docs/3、业务权限/业务用户管理/完成添加业务.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/业务用户管理/完成添加业务.png -------------------------------------------------------------------------------- /docs/3、业务权限/业务用户管理/完成角色分配.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/业务用户管理/完成角色分配.png -------------------------------------------------------------------------------- /docs/3、业务权限/业务用户管理/新增业务用户.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/业务用户管理/新增业务用户.png -------------------------------------------------------------------------------- /docs/3、业务权限/业务用户管理/选中业务用户.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/业务用户管理/选中业务用户.png -------------------------------------------------------------------------------- /docs/3、业务权限/菜单管理/保存成功.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/菜单管理/保存成功.png -------------------------------------------------------------------------------- /docs/3、业务权限/菜单管理/保存菜单.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/菜单管理/保存菜单.png -------------------------------------------------------------------------------- /docs/3、业务权限/菜单管理/保存菜单2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/菜单管理/保存菜单2.png -------------------------------------------------------------------------------- /docs/3、业务权限/菜单管理/创建二级菜单.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/菜单管理/创建二级菜单.png -------------------------------------------------------------------------------- /docs/3、业务权限/菜单管理/创建菜单.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/菜单管理/创建菜单.png -------------------------------------------------------------------------------- /docs/3、业务权限/角色管理/修改角色.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/角色管理/修改角色.png -------------------------------------------------------------------------------- /docs/3、业务权限/角色管理/删除角色.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/角色管理/删除角色.png -------------------------------------------------------------------------------- /docs/3、业务权限/角色管理/新建角色.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/角色管理/新建角色.png -------------------------------------------------------------------------------- /docs/3、业务权限/角色管理/角色命名.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/角色管理/角色命名.png -------------------------------------------------------------------------------- /docs/3、业务权限/角色菜单管理/修改角色菜单.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/角色菜单管理/修改角色菜单.png -------------------------------------------------------------------------------- /docs/3、业务权限/角色菜单管理/修改角色菜单2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/角色菜单管理/修改角色菜单2.png -------------------------------------------------------------------------------- /docs/3、业务权限/角色菜单管理/角色菜单页面.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/3、业务权限/角色菜单管理/角色菜单页面.png -------------------------------------------------------------------------------- /docs/4、业务数据/README.md: -------------------------------------------------------------------------------- 1 | # 业务数据 2 | 数据报表是整个Seed系统最核心的部分,目前整个系统支持4种数据展示图表组件,4种数据过滤组件。 3 | 4 | 从逻辑来看,整个业务数据主要包括两部分图表编辑和图表查看。图表编辑主要使用人群为超级管理员和业务管理员,图表查看主要为普通用户。 5 | 6 | ## 图表编辑 7 | 图表编辑主要以拖拽,选择和输入为主。图表编辑可以通过每个页面的右上方的 编辑页面进入。 8 | ![编辑新页面](图表编辑/编辑新页面.png) 9 | 10 | 点击进入之后,主要可以看到 分为两个区域的 插件选择区域 和 右边的数据展示区域。其中数据展示区域又分为 全局过滤插件区 和 报表展示区。具体如下图所示 11 | 12 | ![进入编辑页面](图表编辑/进入编辑页面.png) 13 | 14 | 整个页面的交互逻辑是 从左边的 插件选择区域 将需要的插件通过拖拽的方式 拖拽到对应的位置。然后页面会立马显示出来,然后点击对应插件的配置按钮通过配置界面对插件进行配置。 15 | 16 | 可以从插件选择区域看出,所有插件分成两类,一个是全局过滤插件 和 报表插件。其中报表插件在配置的过程中还可以配置局部的过滤插件。下面将从这三个方面进行介绍。 17 | 18 | ### 全局过滤插件 19 | 过滤插件顾名思义是用来过滤数据的,可以从全局的过滤插件中看到,当前支持单日期,双日期,多选,单选四种类型的过滤组件。从SQL的角度出发,过滤插件就是组装在where条件下,然后通过筛选过滤插件,动态的更新SQL的where条件获取不同的数据。 20 | 如何配置 21 | 那么全局过滤插件,则是说当前这个插件的过滤条件适用于当前页面的 报表插件 和 过滤插件。 在报表插件中可以用来过滤展示的数据,在过滤插件用可以用来做数据筛选的级联。 22 | 23 | #### 如何配置 24 | 从全局过滤插件中选择一种过滤插件拖拽到数据展示区域的全局过滤插件区域。如下图 25 | ![全局过滤组件拖拽](图表编辑/全局过滤组件拖拽.png) 26 | 27 | #### 如何编辑 28 | 点击对应过滤组件的编辑按钮,弹出组件编辑页面, 如下图所示。 29 | ![全局过滤组件编辑](图表编辑/编辑全局过滤组件.png) 30 | 可以从上图的配置页面可以看到,需要配置 中文名称,这是当前组件在整个页面的唯一标识,数据库字段则是当前过滤组件在其他图表插件中的数据库字段。 数据源有两种方式,一个是手动配置, 另一个是SQL配置。 31 | 32 | 这里选择了SQL配置,选择SQL配置需要选择数据从哪个数据库来,数据库查询的SQL。 33 | 34 | #### 保存成功 35 | 在配置完之后,点击保存,则会立刻去后台拉取数据,具体如下图。 36 | ![过滤组件保存成功](图表编辑/过滤组件保存成功.png) 37 | 38 | ### 报表插件 39 | 报表插件是用来展示数据的,当前支持折线图,对比图,饼状图和对比趋势图四种图表展示插件。 40 | 41 | #### 如何配置 42 | 从报表插件中选择一种报表插件拖拽到数据展示区域的报表展示区。 43 | ![报表组件拖拽](图表编辑/报表组件拖拽成功.png) 44 | 45 | #### 如何编辑 46 | 点击对应报表插件的编辑按钮,弹出插件编辑页面,如下图所示。 47 | ![报表组件设置编辑](图表编辑/报表组件设置编辑.png) 48 | 49 | 报表名称和报表描述是对当前报表的功能进行说明和标记。 50 | 数据源则是指定当前报表的SQL从哪个数据库取数。 51 | SQL配置则是输入取数的SQL。 52 | 53 | 其中在SQL配置下面有个 游戏 的标签,这就是之前配置的全局过滤组件的标签。将光标放置到where 后面,点击游戏这个标签,则会自动生成game_id={game_id}, 其中{}中的内容不可以改变,其他部分都可以根据SQL适当更改。 54 | 55 | SQL配置好之后,点击生成指标和维度。 56 | 下面会自动生成数据项,对数据项进行配置中文名,是指标还是维度类型,还有其他必要的配置。 57 | 58 | #### 保存成功 59 | 在配置完成之后,点击保存,则会立刻从后台拉取数据,具体体现下图。 60 | ![报表组件保存成功](图表编辑/报表组件保存成功.png) 61 | 62 | 其中可以通过选择不同的游戏来进行过滤图表中的,如下图所示 63 | ![过滤组件联动](图表编辑/过滤组建联动.png) 64 | -------------------------------------------------------------------------------- /docs/4、业务数据/图表编辑/全局过滤组件拖拽.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/4、业务数据/图表编辑/全局过滤组件拖拽.png -------------------------------------------------------------------------------- /docs/4、业务数据/图表编辑/报表组件保存成功.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/4、业务数据/图表编辑/报表组件保存成功.png -------------------------------------------------------------------------------- /docs/4、业务数据/图表编辑/报表组件拖拽.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/4、业务数据/图表编辑/报表组件拖拽.png -------------------------------------------------------------------------------- /docs/4、业务数据/图表编辑/报表组件拖拽成功.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/4、业务数据/图表编辑/报表组件拖拽成功.png -------------------------------------------------------------------------------- /docs/4、业务数据/图表编辑/报表组件设置编辑.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/4、业务数据/图表编辑/报表组件设置编辑.png -------------------------------------------------------------------------------- /docs/4、业务数据/图表编辑/编辑全局过滤组件.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/4、业务数据/图表编辑/编辑全局过滤组件.png -------------------------------------------------------------------------------- /docs/4、业务数据/图表编辑/编辑新页面.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/4、业务数据/图表编辑/编辑新页面.png -------------------------------------------------------------------------------- /docs/4、业务数据/图表编辑/过滤组件保存成功.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/4、业务数据/图表编辑/过滤组件保存成功.png -------------------------------------------------------------------------------- /docs/4、业务数据/图表编辑/过滤组建联动.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/4、业务数据/图表编辑/过滤组建联动.png -------------------------------------------------------------------------------- /docs/4、业务数据/图表编辑/进入编辑页面.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/4、业务数据/图表编辑/进入编辑页面.png -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Seed操作手册 2 | Seed系统操作主要分为以下四个部分 3 | 4 | - [注册登录用户](1、注册登录用户/README.md) 5 | - [业务管理](2、业务管理/README.md) 6 | - [业务权限](3、业务权限/README.md) 7 | - [业务数据](4、业务数据/README.md) -------------------------------------------------------------------------------- /docs/其他图片/默认业务图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/其他图片/默认业务图.png -------------------------------------------------------------------------------- /docs/其他图片/默认首页.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/docs/其他图片/默认首页.png -------------------------------------------------------------------------------- /requirements-base.txt: -------------------------------------------------------------------------------- 1 | alembic==1.0.0 2 | asn1crypto==0.24.0 3 | astroid==2.0.1 4 | certifi==2018.8.24 5 | cffi==1.11.5 6 | chardet==3.0.4 7 | click==6.7 8 | colorama==0.3.9 9 | cryptography==2.3 10 | decorator==4.3.0 11 | flake8==3.5.0 12 | Flask==1.0.2 13 | Flask-Cors==3.0.6 14 | flask-marshmallow==0.9.0 15 | Flask-Migrate==2.2.1 16 | Flask-Script==2.0.6 17 | Flask-SQLAlchemy==2.3.2 18 | idna==2.7 19 | infinity==1.4 20 | intervals==0.8.1 21 | isort==4.3.4 22 | itsdangerous==0.24 23 | Jinja2==2.10.1 24 | lazy-object-proxy==1.3.1 25 | Mako==1.0.7 26 | MarkupSafe==1.0 27 | marshmallow==2.15.3 28 | marshmallow-sqlalchemy==0.14.0 29 | mccabe==0.6.1 30 | psycopg2==2.7.5 31 | pycodestyle==2.3.1 32 | pycparser==2.18 33 | pyflakes==1.6.0 34 | pylint==2.0.1 35 | PyMySQL==0.9.2 36 | python-dateutil==2.7.3 37 | python-editor==1.0.3 38 | redis==2.10.6 39 | requests>=2.20.0 40 | rope==0.11.0 41 | six==1.11.0 42 | SQLAlchemy==1.3.3 43 | SQLAlchemy-Utils==0.33.3 44 | typed-ast==1.1.0 45 | urllib3==1.25 46 | validators==0.12.2 47 | Werkzeug==0.14.1 48 | wrapt==1.10.11 49 | WTForms==2.2.1 50 | WTForms-Alchemy==0.16.7 51 | WTForms-Components==0.10.3 52 | bcrypt==3.1.4 53 | uWSGI==2.0.18 54 | sqlparse==0.3.0 55 | impyla==0.14.1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from setuptools import setup, find_packages 5 | from distutils.command.build import build as BuildCommand 6 | from setuptools.command.develop import develop as DevelopCommand 7 | from setuptools.command.install import install as InstallCommand 8 | 9 | ROOT = os.path.realpath(os.path.join(os.path.dirname( 10 | sys.modules['__main__'].__file__))) 11 | 12 | # Add Sentry to path so we can import distutils 13 | sys.path.insert(0, os.path.join(ROOT, 'src')) 14 | 15 | from seed.utils.distutils.build_assets import BuildAssetsCommand 16 | 17 | 18 | VERSION = '0.1.5.6' 19 | 20 | with open('README.md', 'r', encoding='utf-8') as fh: 21 | long_description = fh.read() 22 | 23 | 24 | def get_requirements(env): 25 | with open('requirements-{}.txt'.format(env)) as fp: 26 | return [x.strip() for x in fp.read().split('\n') if not x.startswith('#')] 27 | 28 | 29 | install_requires = get_requirements('base') 30 | 31 | 32 | def package_files(directory): 33 | paths = [] 34 | for (path, directories, filenames) in os.walk(directory): 35 | for filename in filenames: 36 | paths.append(os.path.join(path, filename)) 37 | return paths 38 | 39 | 40 | static_files = package_files(os.path.join('src', 'seed', 'static')) 41 | static_files.extend(package_files(os.path.join('src', 'seed', 'data'))) 42 | 43 | 44 | class SeedBuildCommand(BuildCommand): 45 | def run(self): 46 | self.run_command('build_assets') 47 | BuildCommand.run(self) 48 | 49 | 50 | class SeedDevelopCommand(DevelopCommand): 51 | def run(self): 52 | DevelopCommand.run(self) 53 | 54 | 55 | class SeedInstallCommand(InstallCommand): 56 | def run(self): 57 | InstallCommand.run(self) 58 | 59 | 60 | cmdclass = { 61 | 'build': SeedBuildCommand, 62 | 'develop': SeedDevelopCommand, 63 | 'install': SeedInstallCommand, 64 | 'build_assets': BuildAssetsCommand 65 | } 66 | 67 | 68 | setup( 69 | name="boyaa-seed", 70 | version=VERSION, 71 | auther="Boyaa DataCenter", 72 | auther_email="d@boyaa.com", 73 | description="seed data report system", 74 | long_description=long_description, 75 | long_description_content_type="text/markdown", 76 | url="http://seed.boyaa.com", 77 | install_requires=install_requires, 78 | packages=find_packages("src"), 79 | package_dir={ 80 | "": "src" 81 | }, 82 | package_data={ 83 | "seed": ["static/*"] 84 | }, 85 | data_files=static_files, 86 | include_package_data=True, 87 | entry_points={ 88 | "console_scripts": [ 89 | "seed = seed.runner:main", 90 | ] 91 | }, 92 | cmdclass=cmdclass, 93 | classifiers=[ 94 | "Programming Language :: Python :: 3", 95 | "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", 96 | "Operating System :: OS Independent" 97 | ] 98 | ) 99 | -------------------------------------------------------------------------------- /src/seed/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | ROOT_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -------------------------------------------------------------------------------- /src/seed/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/api/__init__.py -------------------------------------------------------------------------------- /src/seed/api/_base.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | from flask import request, g 4 | from marshmallow import ValidationError 5 | 6 | from seed.models import db 7 | from seed.api.common import _MethodView 8 | from seed.models._base import BussinessModel 9 | 10 | RESTFUL_METHODS = ['GET', 'POST', 'PUT', 'DELETE'] 11 | 12 | 13 | class HttpMethods(object): 14 | GET = 'GET' 15 | POST = 'POST' 16 | PUT = 'PUT' 17 | DELETE = 'DELETE' 18 | 19 | 20 | class RestfulBaseView(_MethodView): 21 | """ BaseView for Restful style 22 | """ 23 | __abstract__ = True 24 | 25 | pk_type = 'string' 26 | 27 | session = db.session 28 | 29 | model_class = None 30 | schema_class = None 31 | 32 | access_methods = [HttpMethods.GET, HttpMethods.POST, HttpMethods.PUT, HttpMethods.DELETE] 33 | 34 | def __init__(self, *args, **kwargs): 35 | super(RestfulBaseView, self).__init__(*args, **kwargs) 36 | 37 | def get(self, model_id=None): 38 | """ GET 39 | GET /base 40 | get paragraph node list 41 | 42 | GET /base/ 43 | get single node which id is model_id 44 | 45 | Arguments: 46 | model_id {int} -- resource id 47 | """ 48 | query_session = self.session.query(self.model_class) 49 | 50 | if issubclass(self.model_class, BussinessModel): 51 | query_session = query_session.filter_by(bussiness_id=g.bussiness_id) 52 | 53 | if model_id: 54 | data = query_session.filter_by(id=model_id).first() 55 | data = self.schema_class(exclude=self.model_class.column_filter).dump(data) 56 | else: 57 | data = query_session.all() 58 | data = self.schema_class(many=True, exclude=self.model_class.column_filter).dump(data) 59 | 60 | return self.response_json(self.HttpErrorCode.SUCCESS, data=data.data) 61 | 62 | def post(self): 63 | """ POST 64 | """ 65 | input_json = request.get_json() 66 | 67 | if isinstance(input_json, list): 68 | schema = self.schema_class(many=True) 69 | else: 70 | schema = self.schema_class() 71 | datas, errors = schema.load(input_json) 72 | 73 | if errors: 74 | return self.response_json(self.HttpErrorCode.PARAMS_VALID_ERROR, msg=errors) 75 | 76 | if isinstance(datas, list): 77 | [data.save() for data in datas] 78 | else: 79 | datas.save() 80 | return self.response_json(self.HttpErrorCode.SUCCESS) 81 | 82 | def put(self, model_id=None): 83 | """ PUT 84 | 85 | Arguments: 86 | model_id {int} -- resource id 87 | """ 88 | input_json = request.get_json() 89 | if model_id: 90 | datas, errors = self.schema_class().load( 91 | input_json, instance=self.model_class.query.get(model_id), partial=True 92 | ) 93 | if errors: 94 | return self.response_json(self.HttpErrorCode.PARAMS_VALID_ERROR, msg=errors) 95 | datas.save() 96 | datas = self.schema_class().dump(datas) 97 | else: 98 | datas, errors = self.schema_class().load(input_json, many=True) 99 | if errors: 100 | return self.response_json(self.HttpErrorCode.PARAMS_VALID_ERROR, msg=errors) 101 | 102 | [data.save() for data in datas] 103 | datas = self.schema_class().dump(datas, many=True) 104 | 105 | return self.response_json(self.HttpErrorCode.SUCCESS, data=datas.data) 106 | 107 | def delete(self, model_id): 108 | """ DELETE 109 | 110 | Arguments: 111 | model_id {int} -- resource id 112 | """ 113 | data = self.model_class.query.get(model_id) 114 | if data: 115 | data.delete() 116 | return self.response_json(self.HttpErrorCode.SUCCESS, 'Success!') 117 | return self.response_json(self.HttpErrorCode.ERROR, 'The data is not exists!') 118 | 119 | @classmethod 120 | def register_api(cls, app): 121 | if hasattr(cls, 'url'): 122 | url = cls.url or '/' + cls.__name__.lower() 123 | else: 124 | url = cls.__name__.lower() 125 | 126 | view_func = cls.as_view(cls.__name__.lower()) 127 | 128 | for method in RESTFUL_METHODS: 129 | if method not in cls.access_methods: 130 | continue 131 | 132 | method_params = inspect.signature(getattr(cls, method.lower())).parameters 133 | defaults = { 134 | param_key: param_value.default 135 | for param_key, param_value in method_params.items() 136 | if param_key != 'self' and param_value.empty is not param_value.default 137 | } 138 | pk = list(method_params.keys())[1] if len(method_params.keys()) > 1 else '' 139 | 140 | if defaults: 141 | app.add_url_rule( 142 | url, defaults=defaults, 143 | view_func=view_func, 144 | methods=[method], 145 | ) 146 | app.add_url_rule( 147 | '%s/<%s:%s>' % (url, cls.pk_type, pk), 148 | view_func=view_func, 149 | methods=[method] 150 | ) 151 | elif len(method_params) > 1: 152 | app.add_url_rule( 153 | '%s/<%s:%s>' % (url, cls.pk_type, pk), 154 | view_func=view_func, 155 | methods=[method] 156 | ) 157 | else: 158 | app.add_url_rule( 159 | url, 160 | view_func=view_func, methods=[method] 161 | ) 162 | 163 | return app 164 | -------------------------------------------------------------------------------- /src/seed/api/common.py: -------------------------------------------------------------------------------- 1 | from flask.views import MethodView 2 | from flask import jsonify, request 3 | 4 | from seed.utils.response import response, HttpErrorCode 5 | 6 | 7 | class _MethodView(MethodView): 8 | """ MethodView base package 9 | """ 10 | __abstract__ = True 11 | 12 | HttpErrorCode = HttpErrorCode 13 | 14 | def __init__(self, *args, **kwargs): 15 | super(_MethodView, self).__init__(*args, **kwargs) 16 | 17 | def response_json(self, code, msg=None, data={}): 18 | return jsonify(response(code, msg, data)) 19 | 20 | @classmethod 21 | def register_api(cls, app): 22 | raise NotImplementedError() 23 | -------------------------------------------------------------------------------- /src/seed/api/endpoints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/api/endpoints/__init__.py -------------------------------------------------------------------------------- /src/seed/api/endpoints/account.py: -------------------------------------------------------------------------------- 1 | from flask import g 2 | from seed.schema.base import BaseSchema 3 | from seed.api._base import RestfulBaseView, HttpMethods 4 | 5 | from seed.models.account import Account as AccountModel 6 | from seed.utils.auth import api_require_user, require_super_admin 7 | 8 | 9 | class AccountSchema(BaseSchema): 10 | class Meta: 11 | model = AccountModel 12 | 13 | 14 | class Account(RestfulBaseView): 15 | """ Account 16 | """ 17 | model_class = AccountModel 18 | schema_class = AccountSchema 19 | 20 | access_methods = [HttpMethods.GET, HttpMethods.PUT, HttpMethods.DELETE] 21 | 22 | decorators = [api_require_user] 23 | 24 | def put(self, model_id=None): 25 | if not require_super_admin() and g.user.id != int(model_id): 26 | return self.response_json(self.HttpErrorCode.FORBIDDEN) 27 | 28 | return super(Account, self).put(model_id=model_id) 29 | 30 | def delete(self, model_id): 31 | if not require_super_admin(): 32 | return self.response_json(self.HttpErrorCode.FORBIDDEN) 33 | 34 | return super(Account, self).delete(model_id=model_id) 35 | -------------------------------------------------------------------------------- /src/seed/api/endpoints/buser.py: -------------------------------------------------------------------------------- 1 | from flask import request, g 2 | from sqlalchemy import and_ 3 | 4 | from seed.schema.base import BaseSchema 5 | from seed.api._base import RestfulBaseView, HttpMethods 6 | from seed.models import BUser as BUserModel 7 | from seed.models import Account as AccountModel 8 | from seed.models import BManager as BManagerModel 9 | 10 | from seed.models.role import Role 11 | from seed.models.buserrole import BUserRole 12 | 13 | from seed.utils.auth import api_require_admin 14 | 15 | 16 | class BUserSchema(BaseSchema): 17 | class Meta: 18 | model = BUserModel 19 | include_fk = True 20 | 21 | 22 | class Buser(RestfulBaseView): 23 | """ 用户业务映射关系添加 24 | """ 25 | model_class = BUserModel 26 | schema_class = BUserSchema 27 | decorators = [api_require_admin] 28 | 29 | def get(self, bussiness_id): 30 | users = self.session.query(BUserModel.id, AccountModel)\ 31 | .join(AccountModel, AccountModel.id == BUserModel.user_id)\ 32 | .filter(BUserModel.bussiness_id == bussiness_id)\ 33 | .all() 34 | 35 | datas = [] 36 | for user in users: 37 | data = user.Account.row2dict() 38 | data['account_id'], data['id'] = data['id'], user.id 39 | data['brole'] = self._get_role(data['id']) 40 | 41 | datas.append(data) 42 | 43 | return self.response_json(self.HttpErrorCode.SUCCESS, data=datas) 44 | 45 | def _get_role(self, uid): 46 | roles = self.session.query(Role)\ 47 | .join(BUserRole, and_( 48 | BUserRole.role_id == Role.id, 49 | BUserRole.user_id == uid, 50 | BUserRole.bussiness_id == g.bussiness_id) 51 | ).all() 52 | roles = [role.row2dict() for role in roles] 53 | return roles 54 | 55 | 56 | class UnBuserList(RestfulBaseView): 57 | """ 不属于当前业务的用户名单 58 | """ 59 | url = 'un_busers' 60 | decorators = [api_require_admin] 61 | access_methods = [HttpMethods.GET] 62 | 63 | def get(self): 64 | 65 | in_user_query = self.session.query(BUserModel.user_id)\ 66 | .filter(BUserModel.bussiness_id == g.bussiness_id) 67 | 68 | b_managers_query = self.session.query(BManagerModel.user_id)\ 69 | .filter(BManagerModel.bussiness_id == g.bussiness_id) 70 | 71 | users = self.session.query(AccountModel)\ 72 | .filter(~AccountModel.id.in_(in_user_query))\ 73 | .filter(~AccountModel.id.in_(b_managers_query)) 74 | 75 | users = [user.row2dict() for user in users] 76 | return self.response_json(self.HttpErrorCode.SUCCESS, data=users) 77 | -------------------------------------------------------------------------------- /src/seed/api/endpoints/buserrole.py: -------------------------------------------------------------------------------- 1 | from flask import request, g 2 | 3 | from seed.schema.base import BaseSchema 4 | from seed.api._base import RestfulBaseView 5 | from seed.models.buserrole import BUserRole as BUserRoleModel 6 | from seed.models.role import Role as RoleModel 7 | from seed.utils.auth import api_require_login 8 | 9 | 10 | class BUserRoleSchema(BaseSchema): 11 | class Meta: 12 | model = BUserRoleModel 13 | include_fk = True 14 | 15 | 16 | class BUserRole(RestfulBaseView): 17 | """ 用户角色 18 | """ 19 | model_class = BUserRoleModel 20 | schema_class = BUserRoleSchema 21 | decorators = [api_require_login] 22 | 23 | def get(self, model_id): 24 | query_session = self.session.query(self.model_class) 25 | datas = query_session.filter(self.model_class.user_id == model_id).all() 26 | datas = [row.row2dict() for row in datas] if datas else [] 27 | 28 | roles = RoleModel.get_roles(g.bussiness_id) 29 | role_id_map = {role.id: role.role for role in roles} 30 | 31 | for data in datas: 32 | data['role'] = role_id_map.get(data['role_id'], '未知') 33 | 34 | return self.response_json(self.HttpErrorCode.SUCCESS, data=datas) 35 | 36 | def put(self, model_id): 37 | input_json = request.get_json() 38 | 39 | # 删除旧数据 40 | query_session = self.session.query(self.model_class) 41 | query_session.filter(self.model_class.user_id == model_id).delete() 42 | self.session.commit() 43 | 44 | # 添加新数据 45 | datas, errors = self.schema_class().load(input_json, many=True) 46 | if errors: 47 | return self.response_json(self.HttpErrorCode.PARAMS_VALID_ERROR, msg=errors) 48 | [data.save() for data in datas] 49 | 50 | datas = [row.row2dict() for row in datas] if datas else [] 51 | 52 | roles = RoleModel.get_roles(g.bussiness_id) 53 | role_id_map = {role.id: role.role for role in roles} 54 | 55 | for data in datas: 56 | data['role'] = role_id_map.get(data['role_id'], '未知') 57 | 58 | return self.response_json(self.HttpErrorCode.SUCCESS, data=datas) -------------------------------------------------------------------------------- /src/seed/api/endpoints/buserselect.py: -------------------------------------------------------------------------------- 1 | from flask import request, g 2 | from seed.cache.user_bussiness import UserBussinessCache 3 | from seed.api._base import RestfulBaseView 4 | 5 | from seed.utils.permissions import has_bussiness_permission 6 | 7 | from seed.utils.auth import api_require_login 8 | 9 | 10 | class BUserSelect(RestfulBaseView): 11 | """ 用户当前选择业务 12 | """ 13 | decorators = [api_require_login] 14 | 15 | def get(self): 16 | current_bussiness_id = g.bussiness_id 17 | return self.response_json( 18 | self.HttpErrorCode.SUCCESS, 19 | data={'bussiness_id': current_bussiness_id} 20 | ) 21 | 22 | def post(self): 23 | input_json = request.get_json() 24 | bussiness_id = input_json['bussiness_id'] 25 | if not has_bussiness_permission(g.user, bussiness_id): 26 | return self.response_json(self.HttpErrorCode.FORBIDDEN) 27 | 28 | UserBussinessCache().set(g.user.id, bussiness_id) 29 | return self.response_json(self.HttpErrorCode.SUCCESS) -------------------------------------------------------------------------------- /src/seed/api/endpoints/bussiness.py: -------------------------------------------------------------------------------- 1 | from flask import request, g 2 | 3 | from seed.schema.base import BaseSchema 4 | from seed.api._base import RestfulBaseView 5 | from seed.models import Bussiness as BussinessModel 6 | from seed.models import BManager as BManagerModel 7 | from seed.models import Account as AccountModel 8 | 9 | from seed.utils.permissions import get_permission_datas_by_user 10 | from seed.utils.helper import common_batch_crud 11 | from seed.utils.auth import api_require_login 12 | 13 | 14 | class BussinessSchema(BaseSchema): 15 | class Meta: 16 | model = BussinessModel 17 | 18 | 19 | class BManagerSchema(BaseSchema): 20 | class Meta: 21 | model = BManagerModel 22 | include_fk = True 23 | 24 | 25 | class Bussiness(RestfulBaseView): 26 | """ bussiness 27 | """ 28 | model_class = BussinessModel 29 | schema_class = BussinessSchema 30 | decorators = [api_require_login] 31 | 32 | def get(self, bussiness_id=None): 33 | """ GET 34 | """ 35 | if bussiness_id: 36 | data = self.session.query( 37 | self.model_class 38 | ).filter_by(id=bussiness_id).first() 39 | data = data.row2dict() if data else {} 40 | return self.response_json(self.HttpErrorCode.SUCCESS, data=data) 41 | else: 42 | # 所有业务 43 | permission_datas, un_permission_datas = get_permission_datas_by_user(g.user) 44 | 45 | bmanagers = self.session.query( 46 | BManagerModel.id, BManagerModel.bussiness_id, 47 | BManagerModel.user_id, AccountModel.name 48 | )\ 49 | .join(AccountModel, BManagerModel.user_id == AccountModel.id)\ 50 | .as_list() 51 | 52 | b_managers_map = {} 53 | for bmanager in bmanagers: 54 | b_managers_map.setdefault(bmanager['bussiness_id'], []).append(bmanager) 55 | 56 | for data in permission_datas: 57 | data['managers'] = b_managers_map.get(data['id'], []) 58 | for data in un_permission_datas: 59 | data['managers'] = b_managers_map.get(data['id'], []) 60 | 61 | datas = {'my_bussiness': permission_datas, 'other_bussiness': un_permission_datas} 62 | 63 | return self.response_json(self.HttpErrorCode.SUCCESS, data=datas) 64 | 65 | def post(self): 66 | """ POST 67 | """ 68 | input_json = request.get_json() 69 | 70 | schema = self.schema_class() 71 | bussiness, errors = schema.load(input_json) 72 | 73 | if errors: 74 | return self.response_json(self.HttpErrorCode.PARAMS_VALID_ERROR, msg=errors) 75 | bussiness.save() 76 | 77 | # 管理员增改删 78 | managers = input_json.get('managers', []) 79 | for manager in managers: 80 | manager['bussiness_id'] = bussiness.id 81 | 82 | common_batch_crud(BManagerSchema, BManagerModel, managers) 83 | 84 | return self.response_json(self.HttpErrorCode.SUCCESS, data={'bussiness_id': bussiness.id}) 85 | 86 | put = post 87 | -------------------------------------------------------------------------------- /src/seed/api/endpoints/databases.py: -------------------------------------------------------------------------------- 1 | from flask import request, g 2 | 3 | from seed.schema.base import BaseSchema 4 | # from seed.api._base import RestfulBaseView 5 | from seed.api.endpoints.buser import Buser 6 | from seed.models.databases import Databases as DatabasesModel 7 | from seed.utils.auth import api_require_admin 8 | from seed.utils.helper import common_batch_crud 9 | 10 | 11 | class DatabasesSchema(BaseSchema): 12 | class Meta: 13 | model = DatabasesModel 14 | include_fk = True 15 | 16 | 17 | class Databases(Buser): 18 | """ 数据库设置 19 | """ 20 | model_class = DatabasesModel 21 | schema_class = DatabasesSchema 22 | decorators = [api_require_admin] 23 | 24 | def get(self, bussiness_id): 25 | """ 通过业务ID得到数据库列表 26 | """ 27 | query_session = self.session.query(self.model_class) 28 | datas = query_session.filter(self.model_class.bussiness_id == bussiness_id).all() 29 | datas = [row.row2dict() for row in datas] if datas else [] 30 | return self.response_json(self.HttpErrorCode.SUCCESS, data=datas) 31 | 32 | def put(self, bussiness_id): 33 | input_json = request.get_json() 34 | 35 | for database in input_json: 36 | database['bussiness_id'] = bussiness_id 37 | 38 | databases = common_batch_crud(DatabasesSchema, DatabasesModel, input_json) 39 | 40 | return self.response_json(self.HttpErrorCode.SUCCESS, data=databases) -------------------------------------------------------------------------------- /src/seed/api/endpoints/filters.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from flask import request, g 4 | from marshmallow import pre_load, post_dump 5 | 6 | from seed.schema.base import BaseSchema 7 | 8 | from seed.api._base import RestfulBaseView 9 | 10 | from seed.models.filters import Filters as FiltersModel 11 | from seed.utils.auth import api_require_login 12 | 13 | 14 | class FilterSchema(BaseSchema): 15 | class Meta: 16 | model = FiltersModel 17 | include_fk = True 18 | 19 | @post_dump(pass_many=True) 20 | def dumps_index(self, in_data, many): 21 | if many: 22 | for data in in_data: 23 | data['conditions'] = json.loads(data['conditions']) 24 | data['cascades'] = json.loads(data['cascades'] or '{}') 25 | else: 26 | in_data['conditions'] = json.loads(in_data['conditions']) 27 | in_data['cascades'] = json.loads(in_data['cascades'] or '{}') 28 | 29 | return in_data 30 | 31 | @pre_load(pass_many=True) 32 | def loads_conditions(self, out_data, many): 33 | if many: 34 | for data in out_data: 35 | data['conditions'] = json.dumps(data['conditions']) 36 | data['cascades'] = json.dumps(data.get('cascades', {})) 37 | else: 38 | out_data['conditions'] = json.dumps(out_data['conditions']) 39 | out_data['cascades'] = json.dumps(out_data.get('cascades', {})) 40 | return out_data 41 | 42 | 43 | class Filters(RestfulBaseView): 44 | """ 过滤组件 45 | """ 46 | model_class = FiltersModel 47 | schema_class = FilterSchema 48 | decorators = [api_require_login] 49 | -------------------------------------------------------------------------------- /src/seed/api/endpoints/menu.py: -------------------------------------------------------------------------------- 1 | from flask import request, g 2 | 3 | from seed.schema.base import BaseSchema 4 | from seed.api._base import RestfulBaseView, HttpMethods 5 | from seed.models.menu import Menu as MenuModel 6 | from seed.utils.auth import api_require_admin 7 | 8 | 9 | class MenuSchema(BaseSchema): 10 | class Meta: 11 | model = MenuModel 12 | 13 | 14 | class Menu(RestfulBaseView): 15 | """ menu 16 | """ 17 | model_class = MenuModel 18 | schema_class = MenuSchema 19 | 20 | decorators = [api_require_admin] 21 | 22 | access_methods = [HttpMethods.GET, HttpMethods.POST] 23 | 24 | def get(self): 25 | """ GET 26 | """ 27 | query_session = self.session.query(self.model_class).filter(self.model_class.bussiness_id==g.bussiness_id) 28 | menu_data = query_session.all() 29 | menus = self._encode_menus(menu_data) 30 | return self.response_json(self.HttpErrorCode.SUCCESS, data=menus) 31 | 32 | def post(self): 33 | """ 更新菜单结构 34 | """ 35 | menus = request.get_json() 36 | self._decode_menus(menus) 37 | 38 | query_session = self.session.query(self.model_class).filter(self.model_class.bussiness_id==g.bussiness_id) 39 | menu_data = query_session.all() 40 | menus = self._encode_menus(menu_data) 41 | 42 | return self.response_json(self.HttpErrorCode.SUCCESS, data=menus) 43 | 44 | def _encode_menus(self, menu_data): 45 | menu_data = {'-'.join([str(row.parent_id), str(row.left_id)]): row.row2dict() for row in menu_data} 46 | 47 | menus = {'id': 0} 48 | middle_menu = [menus] 49 | while middle_menu: 50 | current_menu = middle_menu.pop() 51 | parent_id, left_id = current_menu['id'], 0 52 | 53 | while True: 54 | current_key = '-'.join([str(parent_id), str(left_id)]) 55 | if current_key not in menu_data: 56 | break 57 | current_menu.setdefault('sub_menus', []).append(menu_data[current_key]) 58 | middle_menu.append(menu_data[current_key]) 59 | left_id = menu_data[current_key]['id'] 60 | 61 | return menus.get('sub_menus', []) 62 | 63 | def _decode_menus(self, menus, parent_id=0, left_id=0): 64 | if not menus: 65 | return 66 | 67 | for menu in menus: 68 | # 更新或插入新的菜单 69 | # 获取到菜单对应的ID 70 | left_id = current_id = self._update_menu_item(menu, parent_id, left_id) 71 | self._decode_menus(menu.get('sub_menus', []), parent_id=current_id, left_id=0) 72 | 73 | def _update_menu_item(self, menu, parent_id, left_id): 74 | menu.update({ 75 | 'parent_id': parent_id, 76 | 'left_id': left_id 77 | }) 78 | schema = self.schema_class() 79 | datas, errors = schema.load(menu) 80 | if menu.get('status', 0) == -1: 81 | datas.delete() 82 | else: 83 | datas.save() 84 | return datas.id 85 | -------------------------------------------------------------------------------- /src/seed/api/endpoints/pages.py: -------------------------------------------------------------------------------- 1 | from flask import request, g 2 | from sqlalchemy.exc import InvalidRequestError 3 | 4 | from seed.api._base import RestfulBaseView, HttpMethods 5 | 6 | from seed.models.filters import Filters as FiltersModel 7 | from seed.api.endpoints.filters import FilterSchema 8 | from seed.models.panels import Panels as PanelsModel 9 | from seed.api.endpoints.panels import PanelSchema 10 | 11 | from seed.utils.helper import common_batch_crud 12 | 13 | 14 | class Pages(RestfulBaseView): 15 | """ 页面 16 | """ 17 | access_methods = [HttpMethods.GET, HttpMethods.PUT] 18 | 19 | def get(self, page_id): 20 | global_filters = self._get_global_filters(page_id) 21 | panels = self._get_panels(page_id) 22 | 23 | data = { 24 | "global_filters": global_filters, 25 | "panels": panels 26 | } 27 | return self.response_json(self.HttpErrorCode.SUCCESS, data=data) 28 | 29 | def put(self, page_id): 30 | input_data = request.get_json() 31 | 32 | global_filters, panels = input_data['global_filters'], input_data['panels'] 33 | for global_filter in global_filters: 34 | global_filter['bussiness_id'] = g.bussiness_id 35 | global_filters = common_batch_crud(FilterSchema, FiltersModel, global_filters) 36 | 37 | panels = self._panels_batch_crud(PanelSchema, panels) 38 | 39 | data = {'global_filters': global_filters, 'panels': panels} 40 | 41 | return self.response_json(self.HttpErrorCode.SUCCESS, data=data) 42 | 43 | def _get_global_filters(self, page_id): 44 | """ 获取页面全局过滤组件数据 45 | """ 46 | query_session = self.session.query(FiltersModel) 47 | 48 | filters = query_session.filter_by(page_id=page_id, dtype='page').all() 49 | filters, errors = FilterSchema(many=True, exclude=FiltersModel.column_filter).dump(filters) 50 | 51 | return filters 52 | 53 | def _get_panels(self, page_id): 54 | """ 获取过滤组件数据 55 | """ 56 | panel_query_session = self.session.query(PanelsModel) 57 | panels = panel_query_session.filter_by(page_id=page_id).order_by(PanelsModel.sort).all() 58 | panels, errors = PanelSchema(many=True, exclude=PanelsModel.column_filter).dump(panels) 59 | 60 | filter_query_session = self.session.query(FiltersModel) 61 | filters = filter_query_session.filter_by(page_id=page_id, dtype='model').all() 62 | filters, errors = FilterSchema(many=True, exclude=FiltersModel.column_filter).dump(filters) 63 | 64 | filters_pid_map = {} 65 | for filter in filters: 66 | filters_pid_map.setdefault(filter['belong_id'], []).append(filter) 67 | 68 | for panel in panels: 69 | panel['filters'] = filters_pid_map.get(panel['id'], []) 70 | 71 | return panels 72 | 73 | def _panels_batch_crud(self, panel_schema, panels): 74 | schema_instance = panel_schema() 75 | 76 | # 删除panels 77 | saved_panels = [] 78 | for panel in panels: 79 | one_panel, errors = schema_instance.load(panel) 80 | if errors: 81 | raise Exception(errors) 82 | 83 | if panel.get('status') == -1: 84 | # 删除对应的panel 85 | try: 86 | one_panel.delete() 87 | except InvalidRequestError: 88 | pass 89 | 90 | else: 91 | # 新增和修改对应的panel 92 | one_panel.save() 93 | panel_filters = panel.get('filters', []) 94 | 95 | for filter in panel_filters: 96 | filter['belong_id'] = one_panel.id 97 | filter['bussiness_id'] = g.bussiness_id 98 | filter['page_id'] = one_panel.page_id 99 | 100 | panel_filters = common_batch_crud(FilterSchema, FiltersModel, panel_filters) 101 | panel, errors = panel_schema(exclude=PanelsModel.column_filter).dump(one_panel) 102 | panel['filters'] = panel_filters 103 | saved_panels.append(panel) 104 | return saved_panels 105 | -------------------------------------------------------------------------------- /src/seed/api/endpoints/panels.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from flask import request, g 4 | from marshmallow import pre_load, post_dump 5 | 6 | from seed.schema.base import BaseSchema 7 | from seed.api._base import RestfulBaseView 8 | 9 | from seed.models.panels import Panels as PanelsModel 10 | from seed.utils.auth import api_require_admin 11 | 12 | 13 | class PanelSchema(BaseSchema): 14 | class Meta: 15 | model = PanelsModel 16 | include_fk = True 17 | 18 | @post_dump(pass_many=True) 19 | def dumps_indexs(self, in_data, many): 20 | if many: 21 | for data in in_data: 22 | data['indexs'] = json.loads(data['indexs']) 23 | data['dimensions'] = json.loads(data['dimensions']) 24 | else: 25 | in_data['indexs'] = json.loads(in_data['indexs']) 26 | in_data['dimensions'] = json.loads(in_data['dimensions']) 27 | 28 | return in_data 29 | 30 | @pre_load 31 | def loads_indexs(self, out_data): 32 | out_data['indexs'] = json.dumps(out_data['indexs']) 33 | out_data['dimensions'] = json.dumps(out_data['dimensions']) 34 | return out_data 35 | 36 | 37 | class Panels(RestfulBaseView): 38 | """ 数据面板 39 | """ 40 | model_class = PanelsModel 41 | schema_class = PanelSchema 42 | decorators = [api_require_admin] -------------------------------------------------------------------------------- /src/seed/api/endpoints/queries.py: -------------------------------------------------------------------------------- 1 | 2 | from flask import request 3 | 4 | from seed.schema.base import BaseSchema 5 | from seed.api._base import RestfulBaseView, HttpMethods 6 | 7 | from seed.api.endpoints.panels import PanelSchema, PanelsModel 8 | from seed.api.endpoints.filters import FilterSchema, FiltersModel 9 | from seed.api.endpoints.databases import DatabasesSchema 10 | 11 | from seed.models._base import session 12 | from seed.models.databases import Databases 13 | from seed.models.account import Account 14 | 15 | from seed.utils.auth import api_require_login 16 | from seed.utils.database import get_db_instance 17 | 18 | from seed.libs.data_access.app import DataAccess 19 | from seed.libs.filter_access.app import FilterAccess 20 | 21 | from seed.cache.session import SessionCache 22 | 23 | 24 | class QueryData(RestfulBaseView): 25 | """ 获取数据 26 | """ 27 | url = 'query_data' 28 | decorators = [api_require_login] 29 | 30 | access_methods = [HttpMethods.POST] 31 | 32 | def post(self, panel_id=None): 33 | try: 34 | query_params = request.get_json() 35 | except: 36 | query_params = {} 37 | 38 | # 用户名称添加到参数中 39 | session_token = request.cookies.get('session_token', '') 40 | user = None 41 | query_params.get("query", {}).setdefault("isadmin", 0) 42 | if session_token: 43 | user_id = SessionCache().get_user_id_by_token(session_token) 44 | user = Account.query.filter_by(id=user_id).first() 45 | query_params.get("query", {}).setdefault("username", user.name) 46 | else: 47 | username = request.cookies.get('admin_name', None) 48 | user = Account.query.filter_by(account=username).first() 49 | query_params.get("query", {}).setdefault("username", username) 50 | 51 | if user and user.role in ('super_admin', 'admin'): 52 | query_params.get("query", {}).update({"isadmin": 1}) 53 | 54 | if panel_id: 55 | panel_data = self.session.query(PanelsModel).filter_by(id=panel_id).first() 56 | panel_data, errors = PanelSchema(exclude=PanelsModel.column_filter).dump(panel_data) 57 | if errors: 58 | return self.response_json(self.HttpErrorCode.ERROR, msg=str(errors)) 59 | else: 60 | panel_data = {} 61 | 62 | panel_data.update(query_params) 63 | try: 64 | dtype, db = get_db_by_id(panel_data['db_source']) 65 | query_datas = DataAccess(dtype, db, **panel_data).get_datas() 66 | except Exception as e: 67 | error_message = str(e) 68 | return self.response_json(self.HttpErrorCode.ERROR, msg=error_message) 69 | 70 | return self.response_json(self.HttpErrorCode.SUCCESS, data=query_datas) 71 | 72 | 73 | class QueryFilters(RestfulBaseView): 74 | """ 获取Filters数据 75 | """ 76 | url = 'query_filters' 77 | decorators = [api_require_login] 78 | 79 | access_methods = [HttpMethods.POST] 80 | 81 | def post(self, filter_id=None): 82 | try: 83 | query_params = request.get_json() 84 | except: 85 | query_params = {} 86 | 87 | if filter_id: 88 | filter_data = self.session.query(FiltersModel).filter_by(id=filter_id).first() 89 | filter_data, errors = FilterSchema(exclude=FiltersModel.column_filter).dump(filter_data) 90 | if errors: 91 | return self.response_json(self.HttpErrorCode.ERROR, msg=str(errors)) 92 | else: 93 | filter_data = {} 94 | 95 | filter_data.update(query_params) 96 | 97 | if not filter_data: 98 | return self.response_json(self.HttpErrorCode.ERROR, msg='过滤设置不能为空') 99 | 100 | if filter_data['condition_type'] in ('sql'): 101 | try: 102 | dtype, db = get_db_by_id(filter_data['db_source']) 103 | except Exception as e: 104 | error_message = str(e) 105 | return self.response_json(self.HttpErrorCode.ERROR, msg=error_message) 106 | 107 | filter_data['conditions'] = FilterAccess(db, filter_data['conditions'], filter_data.get('query', {})).query_datas() 108 | 109 | return self.response_json(self.HttpErrorCode.SUCCESS, data=filter_data) 110 | 111 | 112 | def get_db_by_id(db_source): 113 | db_data = session.query(Databases).filter_by(id=db_source).first() 114 | db_conf, errors = DatabasesSchema().dump(db_data) 115 | if errors: 116 | raise Exception(errors) 117 | 118 | db = get_db_instance(**db_conf) 119 | return db_conf['dtype'], db 120 | -------------------------------------------------------------------------------- /src/seed/api/endpoints/role.py: -------------------------------------------------------------------------------- 1 | from seed.schema.base import BaseSchema 2 | from seed.api._base import RestfulBaseView 3 | from seed.models.role import Role as RoleModel 4 | from seed.utils.auth import api_require_admin 5 | 6 | 7 | class RoleSchema(BaseSchema): 8 | class Meta: 9 | model = RoleModel 10 | include_fk = True 11 | 12 | 13 | class Role(RestfulBaseView): 14 | """ role 15 | """ 16 | model_class = RoleModel 17 | schema_class = RoleSchema 18 | 19 | decorators = [api_require_admin] -------------------------------------------------------------------------------- /src/seed/api/endpoints/rolemenu.py: -------------------------------------------------------------------------------- 1 | import json 2 | from flask import request, g 3 | from sqlalchemy import and_ 4 | 5 | from seed.schema.base import BaseSchema 6 | from seed.api._base import RestfulBaseView, HttpMethods 7 | from seed.models.rolemenu import RoleMenu as RoleMenuModel 8 | from seed.models.menu import Menu as MenuModel 9 | from seed.utils.auth import api_require_admin 10 | 11 | 12 | class RoleMenuSchema(BaseSchema): 13 | class Meta: 14 | model = RoleMenuModel 15 | include_fk = True 16 | 17 | 18 | class RoleMenu(RestfulBaseView): 19 | """ 角色菜单设置 20 | """ 21 | model_class = RoleMenuModel 22 | schema_class = RoleMenuSchema 23 | 24 | decorators = [api_require_admin] 25 | 26 | access_methods = [HttpMethods.GET, HttpMethods.PUT] 27 | 28 | def get(self, role_id): 29 | """ GET 30 | """ 31 | role_menu = self._get_role_menu(role_id) 32 | 33 | menus = self._encode_menus(role_menu) 34 | 35 | return self.response_json(self.HttpErrorCode.SUCCESS, data=menus) 36 | 37 | def put(self, role_id): 38 | request_json = request.get_json() 39 | menus = request_json['menu'] 40 | self._decode_menus(menus, role_id) 41 | return self.response_json(self.HttpErrorCode.SUCCESS) 42 | 43 | def _get_role_menu(self, model_id): 44 | role_datas = self.session.query(RoleMenuModel)\ 45 | .filter(RoleMenuModel.role_id == model_id, RoleMenuModel.bussiness_id == g.bussiness_id).all() 46 | role_data_map = {role_data.menu_id: role_data for role_data in role_datas} 47 | 48 | menu_datas = self.session.query(MenuModel)\ 49 | .filter(MenuModel.bussiness_id == g.bussiness_id).all() 50 | 51 | menu_datas_with_permission = {} 52 | 53 | for menu_data in menu_datas: 54 | menu_data = menu_data.row2dict() 55 | role_data = role_data_map[menu_data['id']] if menu_data['id'] in role_data_map else None 56 | 57 | menu_data['role_permission'] = role_data.role_permission if role_data else False 58 | menu_data['menu_id'] = menu_data['id'] 59 | if role_data: 60 | menu_data['id'] = role_data.id 61 | else: 62 | menu_data['id'] = None 63 | 64 | menu_datas_with_permission['-'.join([str(menu_data['parent_id']), str(menu_data['left_id'])])] = menu_data 65 | 66 | return menu_datas_with_permission 67 | 68 | def _encode_menus(self, menu_data): 69 | menu_list = [] 70 | for key, value in menu_data.items(): 71 | if not value.get('id'): 72 | value.pop('id') 73 | 74 | menu_list.append(value) 75 | 76 | return menu_list 77 | 78 | def _decode_menus(self, menus, role_id, parent_id=0, left_id=0): 79 | if not menus: 80 | return 81 | 82 | for menu in menus: 83 | # 更新或插入新的菜单 84 | # 获取到菜单对应的ID 85 | # if menu.get('role_permission', False): 86 | self._insert_or_update_menu(menu, role_id, menu.get('role_permission', False)) 87 | current_id = menu['menu_id'] 88 | self._decode_menus(menu.get('sub_menus', []), role_id=role_id, parent_id=current_id, left_id=0) 89 | 90 | def _insert_or_update_menu(self, menu, role_id, role_permission): 91 | role_menu = { 92 | "role_id": role_id, 93 | "menu_id": menu['menu_id'], 94 | "role_permission": role_permission 95 | } 96 | if 'id' in menu: 97 | role_menu['id'] = menu['id'] 98 | schema = self.schema_class() 99 | datas, errors = schema.load(role_menu) 100 | datas.save() 101 | return datas.id 102 | -------------------------------------------------------------------------------- /src/seed/api/endpoints/user.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import and_ 2 | from flask import current_app, g 3 | 4 | from seed.schema.base import BaseSchema 5 | from seed.api._base import RestfulBaseView, HttpMethods 6 | from seed.models.account import Account as AccountModel 7 | from seed.models.role import Role 8 | from seed.models.buserrole import BUserRole 9 | from seed.models.bmanager import BManager 10 | from seed.models.rolemenu import RoleMenu 11 | from seed.models.buser import BUser 12 | from seed.models.menu import Menu as MenuModel 13 | from seed.utils.auth import api_require_login, require_admin 14 | 15 | 16 | class UserSchema(BaseSchema): 17 | class Meta: 18 | model = AccountModel 19 | 20 | 21 | class User(RestfulBaseView): 22 | """ 用户相关 23 | """ 24 | model_class = AccountModel 25 | schema_class = UserSchema 26 | decorators = [api_require_login] 27 | 28 | access_methods = [HttpMethods.GET] 29 | 30 | def get(self): 31 | """ 获取用户信息, 如果是SSO的校验 32 | 成功后自动添加用户信息到用户列表 33 | """ 34 | user = g.user.row2dict() 35 | user['brole'] = self._get_role(g.user.id) 36 | 37 | return self.response_json(self.HttpErrorCode.SUCCESS, data=user) 38 | 39 | def _get_role(self, uid): 40 | roles = self.session.query(Role.role)\ 41 | .join(BUserRole, and_( 42 | BUserRole.role_id == Role.id, 43 | BUserRole.user_id == uid, 44 | BUserRole.bussiness_id == g.bussiness_id) 45 | ).all() 46 | return roles 47 | 48 | 49 | class UserMenu(RestfulBaseView): 50 | """ 获取当前用户的菜单 51 | """ 52 | url = '/user/menu' 53 | 54 | decorators = [api_require_login] 55 | 56 | access_methods = [HttpMethods.GET] 57 | 58 | def get(self): 59 | if require_admin(): 60 | # 如果是业务管理员以上,返回当前业务的所有菜单 61 | query_session = self.session.query(MenuModel, "True").filter(MenuModel.bussiness_id == g.bussiness_id) 62 | else: 63 | # 其他角色通过关联用户角色表来获取到当前的菜单 64 | query_session = self.session.query(MenuModel, RoleMenu.role_permission)\ 65 | .join(RoleMenu, and_(RoleMenu.bussiness_id == g.bussiness_id, MenuModel.id == RoleMenu.menu_id))\ 66 | .join(BUser, and_(BUser.bussiness_id==g.bussiness_id, BUser.user_id==g.user.id))\ 67 | .join(BUserRole, and_(BUserRole.bussiness_id == g.bussiness_id, RoleMenu.role_id == BUserRole.role_id, BUserRole.user_id==BUser.id)) 68 | 69 | menu_data = query_session.all() 70 | menus = self._encode_menus(menu_data) 71 | 72 | return self.response_json(self.HttpErrorCode.SUCCESS, data=menus) 73 | 74 | def _encode_menus(self, menus): 75 | menu_data = [] 76 | 77 | for menu, role_permission in menus: 78 | if role_permission: 79 | temp = menu.row2dict() 80 | temp.update({"role_permission": True}) 81 | menu_data.append(temp) 82 | 83 | del_list = [] 84 | for j in menu_data: 85 | menu_id = j["id"] 86 | for i in menu_data: 87 | if menu_id == i["parent_id"]: 88 | j.setdefault("sub_menus", []).append(i) 89 | del_list.append(i) 90 | 91 | for del_m in del_list: 92 | menu_data.remove(del_m) 93 | 94 | return menu_data 95 | -------------------------------------------------------------------------------- /src/seed/api/front.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import Blueprint, render_template, send_from_directory 4 | 5 | from seed.utils.helper import template_folder_path 6 | 7 | 8 | bp = Blueprint('front', __name__) 9 | 10 | 11 | @bp.route('/', defaults={'path': ''}) 12 | @bp.route('/') 13 | def index(path): 14 | """ 15 | 如果是静态文件则利用seed_from_directory来进行加载 16 | """ 17 | if path and os.path.exists(os.path.join(template_folder_path, path)): 18 | return send_from_directory(template_folder_path, path) 19 | 20 | return render_template('index.html') -------------------------------------------------------------------------------- /src/seed/api/urls.py: -------------------------------------------------------------------------------- 1 | from inspect import isclass 2 | 3 | from flask import Blueprint 4 | 5 | from seed import api 6 | from seed.utils.helper import get_package_members, get_immediate_cls_attr 7 | from seed.api.common import _MethodView 8 | 9 | 10 | def register_api(app): 11 | """ register api interface 12 | """ 13 | 14 | predicate = lambda m: isclass(m) and issubclass(m, _MethodView) and not get_immediate_cls_attr(m, '__abstract__') 15 | members = get_package_members(api, predicate, '') 16 | for pre_url, handlers in members.items(): 17 | bp = Blueprint(pre_url, __name__) 18 | for handler in set(handlers): 19 | bp = handler.register_api(bp) 20 | 21 | app.register_blueprint(bp, url_prefix=pre_url) 22 | -------------------------------------------------------------------------------- /src/seed/api/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/api/users/__init__.py -------------------------------------------------------------------------------- /src/seed/api/users/active_account.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | 3 | from seed.api._base import RestfulBaseView, HttpMethods 4 | from seed.cache.active_account import ActiveAccountCache 5 | from seed.models.account import Account 6 | 7 | 8 | class ActiveAccount(RestfulBaseView): 9 | """ 激活账户 10 | """ 11 | url = 'active_account' 12 | access_methods = [HttpMethods.GET] 13 | 14 | def get(self): 15 | active_token = request.args.get('active_token') 16 | if not active_token: 17 | return self.response_json( 18 | self.HttpErrorCode.PARAMS_VALID_ERROR, 19 | msg='激活token参数缺失' 20 | ) 21 | 22 | user_id = ActiveAccountCache().get_user_by_active_token(active_token) 23 | if not user_id: 24 | return self.response_json( 25 | self.HttpErrorCode.PARAMS_VALID_ERROR, 26 | msg='激活token已经失效' 27 | ) 28 | 29 | account = self.session.query(Account).filter_by(id=user_id).first() 30 | if not account: 31 | return self.response_json( 32 | self.HttpErrorCode.PARAMS_VALID_ERROR, 33 | msg='激活账户不存在' 34 | ) 35 | 36 | account.role = 'user' 37 | account.status = 1 38 | account.save() 39 | 40 | return self.response_json(self.HttpErrorCode.SUCCESS) 41 | -------------------------------------------------------------------------------- /src/seed/api/users/login.py: -------------------------------------------------------------------------------- 1 | import time 2 | import bcrypt 3 | 4 | from flask import request, make_response, current_app 5 | 6 | from seed.models.account import Account 7 | from seed.cache.session import SessionCache 8 | 9 | from seed.api._base import RestfulBaseView, HttpMethods 10 | 11 | 12 | class Login(RestfulBaseView): 13 | """ 登录 14 | """ 15 | access_methods = [HttpMethods.POST] 16 | 17 | def post(self): 18 | """ POST 19 | """ 20 | input_json = request.get_json() 21 | if 'account' not in input_json: 22 | return self.response_json(self.HttpErrorCode.PARAMS_VALID_ERROR, '账号不能为空') 23 | 24 | if 'password' not in input_json: 25 | return self.response_json(self.HttpErrorCode.PARAMS_VALID_ERROR, '密码不能为空') 26 | 27 | account, password = input_json.get('account'), input_json.get('password') 28 | 29 | # 获取账号 30 | account = Account.query.filter_by(account=account).first() 31 | 32 | if not bcrypt.checkpw(password.encode('utf-8'), account.password.encode('utf-8')): 33 | return self.response_json(self.HttpErrorCode.AUTHORIZED_ERROR) 34 | 35 | # Cookie设置 36 | res = make_response(self.response_json(self.HttpErrorCode.SUCCESS)) 37 | 38 | session_token = SessionCache().create_session(account.id) 39 | res.set_cookie( 40 | 'session_token', 41 | session_token, 42 | expires=time.time()+24*60*60, 43 | domain=request.host 44 | ) 45 | 46 | return res 47 | 48 | 49 | class Logout(RestfulBaseView): 50 | """ 登出 51 | """ 52 | access_methods = [HttpMethods.GET] 53 | 54 | def get(self): 55 | session_token = request.cookies.get('session_token', None) 56 | auth_type = current_app.config["AUTH_TYPE"] 57 | sso_url = current_app.config["SSO_URL"] 58 | login_url = request.host_url + "login" 59 | if sso_url and auth_type == "SSO": 60 | response = make_response(self.response_json(-14, data=sso_url)) 61 | else: 62 | response = make_response(self.response_json(self.HttpErrorCode.SUCCESS, data=login_url)) 63 | 64 | response.set_cookie("admin_uid", '', expires=0, domain=".oa.com") 65 | response.set_cookie("admin_key", '', expires=0, domain=".oa.com") 66 | if not session_token: 67 | return response 68 | 69 | SessionCache().delete(session_token) 70 | 71 | response.set_cookie('session_token', session_token, expires=0) 72 | 73 | return response 74 | -------------------------------------------------------------------------------- /src/seed/api/users/register.py: -------------------------------------------------------------------------------- 1 | import bcrypt 2 | from flask import request 3 | 4 | from seed.api._base import RestfulBaseView, HttpMethods 5 | from seed.cache.active_account import ActiveAccountCache 6 | from seed.schema.base import BaseSchema 7 | from seed.models.account import Account 8 | from seed.utils.mail import send_active_email 9 | 10 | 11 | class AccountSchema(BaseSchema): 12 | """ 账户 13 | """ 14 | class Meta: 15 | model = Account 16 | 17 | 18 | class Register(RestfulBaseView): 19 | """ 注册 20 | """ 21 | access_methods = [HttpMethods.POST] 22 | 23 | def post(self): 24 | """ POST 25 | """ 26 | input_json = request.get_json() 27 | 28 | if input_json['password'] != input_json['confirm_password']: 29 | return self.response_json( 30 | self.HttpErrorCode.PARAMS_VALID_ERROR, 31 | msg='密码和确认密码不一致' 32 | ) 33 | 34 | if 'active_url' not in input_json: 35 | return self.response_json( 36 | self.HttpErrorCode.PARAMS_VALID_ERROR, 37 | msg='active_url缺失' 38 | ) 39 | active_url = input_json['active_url'] 40 | 41 | if self.session.query(Account).filter_by(account=input_json['account']).first(): 42 | return self.response_json( 43 | self.HttpErrorCode.PARAMS_VALID_ERROR, 44 | msg='账号已经被占用了' 45 | ) 46 | 47 | if self.session.query(Account).filter_by(email=input_json['email']).first(): 48 | return self.response_json( 49 | self.HttpErrorCode.PARAMS_VALID_ERROR, 50 | msg='邮箱已经被注册了' 51 | ) 52 | 53 | input_json['password'] = bcrypt.hashpw(input_json['password'].encode('utf-8'), bcrypt.gensalt()) 54 | 55 | account, errors = AccountSchema().load(input_json, partial=True) 56 | if errors: 57 | return self.response_json(self.HttpErrorCode.PARAMS_VALID_ERROR, msg=errors) 58 | account.save() 59 | 60 | active_token = ActiveAccountCache().create_active_token(account.id) 61 | redirect_url = '{active_host}/users/active_account&active_token={active_token}' 62 | redirect_url = redirect_url.format(active_host=request.host, active_token=active_token) 63 | try: 64 | send_active_email(account.email, active_url, redirect_url) 65 | except Exception as e: 66 | # 如果邮件发送失败,则放弃邮件验证 67 | print(e) 68 | account.role = 'user' 69 | account.save() 70 | 71 | return self.response_json(self.HttpErrorCode.SUCCESS) 72 | -------------------------------------------------------------------------------- /src/seed/api/users/reset_password.py: -------------------------------------------------------------------------------- 1 | import bcrypt 2 | from flask import request 3 | 4 | from seed.models.account import Account 5 | from seed.api._base import RestfulBaseView, HttpMethods 6 | from seed.cache.reset_password import ResetPasswordCache 7 | from seed.utils.mail import send_reset_password_email 8 | 9 | 10 | class ForgetPassword(RestfulBaseView): 11 | """ 忘记密码, 发送重置密码的邮件 12 | """ 13 | access_methods = [HttpMethods.POST] 14 | url = 'forget_password' 15 | 16 | def post(self): 17 | input_json = request.get_json() 18 | if 'account' not in input_json: 19 | return self.response_json( 20 | self.HttpErrorCode.PARAMS_VALID_ERROR, 21 | msg='account参数缺失' 22 | ) 23 | account = input_json['account'] 24 | 25 | if 'reset_url' not in input_json: 26 | return self.response_json( 27 | self.HttpErrorCode.PARAMS_VALID_ERROR, 28 | msg='reset_url参数缺失' 29 | ) 30 | reset_url = input_json['reset_url'] 31 | 32 | account_session = self.session.query(Account) 33 | 34 | if '@' in account: 35 | account = account_session.filter_by(email=account).first() 36 | else: 37 | account = account_session.filter_by(account=account).first() 38 | 39 | if not account: 40 | return self.response_json( 41 | self.HttpErrorCode.PARAMS_VALID_ERROR, 42 | msg='该账户不存在,请查证后再试' 43 | ) 44 | 45 | reset_token = ResetPasswordCache().create_active_token(account.id) 46 | reset_redirect_url = '{active_host}/users/active_account&active_token={active_token}' 47 | reset_redirect_url = reset_redirect_url.format(active_host=request.host, reset_token=reset_token) 48 | send_reset_password_email(account.email, reset_url, reset_redirect_url) 49 | 50 | return self.response_json(self.HttpErrorCode.SUCCESS) 51 | 52 | 53 | class ResetPasssword(RestfulBaseView): 54 | """ 重置密码 55 | """ 56 | access_methods = [HttpMethods.POST] 57 | url = 'reset_password' 58 | 59 | def post(self): 60 | input_json = request.get_json() 61 | if input_json['password'] != input_json['confirm_password']: 62 | return self.response_json( 63 | self.HttpErrorCode.PARAMS_VALID_ERROR, 64 | msg='密码和确认密码不一致' 65 | ) 66 | 67 | if 'reset_token' not in input_json: 68 | return self.response_json( 69 | self.HttpErrorCode.PARAMS_VALID_ERROR, 70 | msg='reset_token参数不存在' 71 | ) 72 | 73 | reset_account_id = ResetPasswordCache().get_user_by_active_token(input_json['reset_token']) 74 | account = self.session.query(Account).filter_by(id=reset_account_id).first() 75 | 76 | password = bcrypt.hashpw(input_json['password'].encode('utf-8'), bcrypt.gensalt()) 77 | account.password = password 78 | account.save() 79 | 80 | return self.response_json(self.HttpErrorCode.SUCCESS) 81 | -------------------------------------------------------------------------------- /src/seed/api/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/api/utils/__init__.py -------------------------------------------------------------------------------- /src/seed/api/utils/dbtest.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | 3 | from seed.api._base import RestfulBaseView, HttpMethods 4 | from seed.drives import ALL_DRIVES 5 | 6 | 7 | class DatabaseTest(RestfulBaseView): 8 | url = 'database_test' 9 | 10 | access_methods = [HttpMethods.POST] 11 | 12 | def post(self): 13 | db_conf = request.get_json() 14 | 15 | try: 16 | drive = ALL_DRIVES[db_conf['dtype']] 17 | drive_instance = drive( 18 | db_conf['ip'], int(db_conf['port']), db_conf['name'], 19 | db_conf['user'], db_conf['password'] 20 | ) 21 | success = drive_instance.test_connection() 22 | message = '数据库连接成功!' 23 | except Exception as e: 24 | success = False 25 | message = str(e) 26 | 27 | if success: 28 | return self.response_json(self.HttpErrorCode.SUCCESS, msg=message) 29 | 30 | return self.response_json(self.HttpErrorCode.ERROR, msg=message) -------------------------------------------------------------------------------- /src/seed/api/utils/dbtypes.py: -------------------------------------------------------------------------------- 1 | 2 | from flask import request 3 | 4 | # from seed.api.common import _MethodView 5 | from seed.api._base import RestfulBaseView, HttpMethods 6 | from seed.drives import ALL_DRIVES 7 | 8 | 9 | class DatabaseTypes(RestfulBaseView): 10 | url = 'database_types' 11 | 12 | access_methods = [HttpMethods.GET] 13 | 14 | def get(self): 15 | 16 | data = [{'value': drive, 'label': drive} for drive in ALL_DRIVES.keys()] 17 | return self.response_json(self.HttpErrorCode.SUCCESS, data=data) -------------------------------------------------------------------------------- /src/seed/api/utils/files.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | 3 | # from seed.api.common import _MethodView 4 | from seed.utils.file import LocalFile 5 | from seed.api._base import RestfulBaseView, HttpMethods 6 | 7 | 8 | class Files(RestfulBaseView): 9 | 10 | access_methods = [HttpMethods.POST] 11 | 12 | def post(self): 13 | upload_file = request.files['file'] 14 | local_file = LocalFile() 15 | file_path = local_file.save(upload_file) 16 | if file_path: 17 | file_url = request.host_url + file_path.replace('\\', '/') 18 | return self.response_json(self.HttpErrorCode.SUCCESS, '上传成功', {'file_url': file_url}) 19 | return self.response_json(self.HttpErrorCode.ERROR, '上传失败') 20 | -------------------------------------------------------------------------------- /src/seed/api/utils/sql_analyze.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from flask import request 4 | import sqlparse 5 | from sqlparse.tokens import Keyword 6 | from sqlparse.sql import IdentifierList, Identifier 7 | 8 | from seed.api._base import RestfulBaseView, HttpMethods 9 | 10 | 11 | class SqlFieldAnalysis(RestfulBaseView): 12 | url = 'sql_fields' 13 | 14 | access_methods = [HttpMethods.POST] 15 | 16 | def post(self): 17 | input_json = request.get_json() 18 | 19 | if 'sqls' not in input_json: 20 | return self.response_json(self.HttpErrorCode.PARAMS_VALID_ERROR, msg='获取SQL失败') 21 | 22 | chartype = input_json.get("chartType") 23 | sqls = input_json['sqls'] 24 | print("origin_sql:", sqls) 25 | sql_str = sqls.lower().replace('\n', ' ').replace('\t', ' ') 26 | 27 | # 去除注释信息,否则列名会被解析错误 28 | sql = sqlparse.format(sql_str, strip_comments=True) 29 | stmt = sqlparse.parse(sql)[0] 30 | tokens_list = stmt.tokens 31 | 32 | fields = [] 33 | 34 | for token in tokens_list: 35 | if token.ttype is Keyword: 36 | continue 37 | if isinstance(token, IdentifierList): 38 | for identifier in token.get_identifiers(): 39 | field_name = identifier.get_name() 40 | fields.append(field_name) 41 | elif isinstance(token, Identifier): 42 | break 43 | 44 | sankey_fields = ["source", "target"] 45 | map_fields = ["region_name", "lat", "lng"] 46 | 47 | # 桑基图和地图SQL中必须包含指定字段 48 | if chartype == "sankey": 49 | if not set(sankey_fields).issubset(set(fields)): 50 | return self.response_json(self.HttpErrorCode.PARAMS_VALID_ERROR, msg='SQL字段不符合要求') 51 | 52 | elif chartype == "map": 53 | if not set(map_fields).issubset(set(fields)): 54 | return self.response_json(self.HttpErrorCode.PARAMS_VALID_ERROR, msg='SQL字段不符合要求') 55 | 56 | return self.response_json(self.HttpErrorCode.SUCCESS, data={'fields': fields}) 57 | -------------------------------------------------------------------------------- /src/seed/cache/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import current_app 2 | 3 | from seed.cache.redis import RedisCache 4 | 5 | __all__ = ['DefaultCache'] 6 | 7 | DefaultCache = RedisCache -------------------------------------------------------------------------------- /src/seed/cache/active_account.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | 4 | from flask import current_app 5 | 6 | from seed.cache.redis import RedisCache 7 | 8 | 9 | class ActiveAccountCache(RedisCache): 10 | def __init__(self): 11 | self.redis_client = current_app.cache 12 | self.middle_key = 'active_account' 13 | 14 | def make_key(self, key): 15 | key = ":".join([self.middle_key, str(key)]) 16 | return super(ActiveAccountCache, self).make_key(key) 17 | 18 | def create_active_token(self, user_id): 19 | active_token = ''.join([random.choice(string.ascii_lowercase + string.digits) for key in range(48)]) 20 | self.set(active_token, user_id, timeout=24 * 60 * 60) 21 | return active_token 22 | 23 | def get_user_by_active_token(self, active_token): 24 | return self.get(active_token) 25 | -------------------------------------------------------------------------------- /src/seed/cache/auth.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/cache/auth.py -------------------------------------------------------------------------------- /src/seed/cache/base.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | 3 | class BaseCache(Thread): 4 | prefix = "seed" 5 | 6 | def __init__(self, prefix=None): 7 | if not prefix: 8 | self.prefix = prefix 9 | 10 | def make_key(self, key): 11 | return '{}:{}'.format( 12 | self.prefix, 13 | key 14 | ) 15 | 16 | def get(self, key): 17 | raise NotImplementedError 18 | 19 | def set(self, key, value, timeout): 20 | raise NotImplementedError 21 | 22 | def delete(self, key): 23 | raise NotImplementedError 24 | -------------------------------------------------------------------------------- /src/seed/cache/redis.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from seed.cache.base import BaseCache 4 | 5 | 6 | class RedisCache(BaseCache): 7 | max_size = 50 * 1024 * 1024 8 | 9 | def __init__(self, redis): 10 | self.redis_client = redis 11 | 12 | def set(self, key, value, timeout=None): 13 | key = self.make_key(key) 14 | v = json.dumps(value) 15 | if len(v) > self.max_size: 16 | raise ValueError("Cache value too large: %r %r" %(key, len(value))) 17 | if timeout: 18 | self.redis_client.set(key, v, int(timeout)) 19 | else: 20 | self.redis_client.set(key, v) 21 | 22 | def get(self, key): 23 | key = self.make_key(key) 24 | result = self.redis_client.get(key) 25 | if result: 26 | result = json.loads(result) 27 | return result 28 | 29 | def delete(self, key): 30 | key = self.make_key(key) 31 | self.redis_client.delete(key) -------------------------------------------------------------------------------- /src/seed/cache/reset_password.py: -------------------------------------------------------------------------------- 1 | from flask import current_app 2 | from seed.cache.active_account import ActiveAccountCache 3 | 4 | 5 | class ResetPasswordCache(ActiveAccountCache): 6 | def __init__(self): 7 | self.redis_client = current_app.cache 8 | self.middle_key = 'reset_password' 9 | -------------------------------------------------------------------------------- /src/seed/cache/session.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | 4 | from flask import current_app 5 | 6 | from seed.cache.redis import RedisCache 7 | 8 | 9 | class SessionCache(RedisCache): 10 | def __init__(self): 11 | self.redis_client = current_app.cache 12 | self.middle_key = 'session' 13 | 14 | def make_key(self, key): 15 | key = ':'.join([self.middle_key, str(key)]) 16 | return super(SessionCache, self).make_key(key) 17 | 18 | def create_session(self, user_id): 19 | token = ''.join([random.choice(string.ascii_uppercase + string.digits) for key in range(32)]) 20 | self.set(token, user_id, timeout=24 * 60 * 60) 21 | return token 22 | 23 | def get_user_id_by_token(self, token): 24 | return self.get(token) 25 | -------------------------------------------------------------------------------- /src/seed/cache/user_bussiness.py: -------------------------------------------------------------------------------- 1 | from flask import current_app 2 | 3 | from seed.cache.redis import RedisCache 4 | 5 | 6 | class UserBussinessCache(RedisCache): 7 | def __init__(self): 8 | self.redis_client = current_app.cache 9 | self.middle_key = 'user_bussiness' 10 | 11 | def make_key(self, key): 12 | key = ':'.join([self.middle_key, str(key)]) 13 | return super(UserBussinessCache, self).make_key(key) -------------------------------------------------------------------------------- /src/seed/conf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/conf/__init__.py -------------------------------------------------------------------------------- /src/seed/conf/server.py: -------------------------------------------------------------------------------- 1 | """ 2 | seed.conf.server 3 | ~~~~~~~~~~~~~~~~ 4 | 5 | There setting act as default(base) settings for the seed-provided web-server 6 | """ 7 | 8 | import os 9 | 10 | ENVRIOMENT = os.environ.get('SEED_ENVRIOMENT', 'production') 11 | 12 | IS_DEV = ENVRIOMENT == 'development' 13 | 14 | DEBUG = IS_DEV 15 | 16 | # Seed logs formatting 17 | LOGGING = { 18 | 'default_level': 'INFO', 19 | 'version': 1, 20 | 'disable_existing_loggers': True, 21 | 'handlers': { 22 | 'null': { 23 | 'class': 'logging.NullHandler', 24 | }, 25 | 'console': { 26 | 'class': 'logging.StreamHandler' 27 | }, 28 | }, 29 | } -------------------------------------------------------------------------------- /src/seed/data/config/config.yaml.default: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/data/config/config.yaml.default -------------------------------------------------------------------------------- /src/seed/data/config/seed_conf.py.default: -------------------------------------------------------------------------------- 1 | # this file is just for python, with a touch of Flask which means 2 | # you can inhert and tweek setting to your hearts content 3 | 4 | ############# 5 | # databases # 6 | ############# 7 | # 8 | # 系统本身支持MySQL和PostgreSQL数据库 9 | # PostgreSQL ENGINE 选择 postgresql+psycopg2 10 | # MySQL ENGINE 选择 mysql+pymysql 11 | # 12 | DATABASES = { 13 | 'default': { 14 | 'ENGINE': 'mysql+pymysql', 15 | 'NAME': 'seed', 16 | 'USER': '', 17 | 'PASSWORD': '', 18 | 'HOST': '', 19 | 'PORT': '', 20 | 'CHARSET': 'utf8mb4', 21 | }, 22 | } 23 | 24 | SQLALCHEMY_DATABASE_URI = '{ENGINE}://{USER}:{PASSWORD}@{HOST}:{PORT}/{NAME}?charset={CHARSET}'.format(**DATABASES['default']) 25 | SQLALCHEMY_TRACK_MODIFICATIONS = True 26 | SQLALCHEMY_POOL_SIZE = 100 27 | SQLALCHEMY_POOL_TIMEOUT = 10 28 | SQLALCHEMY_MAX_OVERFLOW = 50 29 | 30 | SQLALCHEMY_POOL_RECYCEL = 3600 31 | 32 | 33 | ########### 34 | # General # 35 | ########### 36 | 37 | DEBUG = %(debug_flag)s 38 | 39 | # Web server host and port 40 | HOST = '127.0.0.1' 41 | PORT = 5000 42 | 43 | # login user type, you can choice sso and default login type 44 | # if you choose sso, you must setting XXXX 45 | 46 | AUTH_TYPE = "LOGIN" 47 | SSO_URL = "" 48 | 49 | DEFAULT_TIME_ZONE = "Asia/Shanghai" 50 | 51 | 52 | ######### 53 | # cache # 54 | ######### 55 | 56 | # Seed currently only support redis for cache 57 | SEED_CACHE = "redis" 58 | # For example:: 59 | # redis://[:password]@localhost:6379/0 60 | # rediss://[:password]@localhost:6379/0 61 | # unix://[:password]@/path/to/socket.sock?db=0 62 | REDIS_URL = "redis://127.0.0.1:6379/0" 63 | 64 | 65 | ######### 66 | # email # 67 | ######### 68 | 69 | # Seed email setting 70 | MAIL_BACKEND = 'smtp' 71 | MAIL_HOST = '' 72 | MAIL_PORT = 5000 73 | MAIL_USER = '' 74 | MAIL_PASSWORD = '' 75 | MAIL_USE_TLS = True 76 | MAIL_FROM = '' 77 | -------------------------------------------------------------------------------- /src/seed/drives/__init__.py: -------------------------------------------------------------------------------- 1 | from seed.drives.impala import Impala 2 | from seed.drives.mysql import MySQL 3 | from seed.drives.postgresql import PostgreSQL 4 | 5 | 6 | ALL_DRIVES = { 7 | 'impala': Impala, 8 | 'mysql': MySQL, 9 | 'postgresql': PostgreSQL 10 | } -------------------------------------------------------------------------------- /src/seed/drives/base.py: -------------------------------------------------------------------------------- 1 | DEFUALT_RETRY_COUNT = 3 2 | 3 | 4 | class BaseDrive(object): 5 | def __init__(self, ip, port, name, user, password): 6 | self.ip = ip 7 | self.port = port 8 | self.name = name 9 | self.user = user 10 | self.password = password 11 | 12 | self._connect() 13 | 14 | def query(self, sql, params=None, retry_count=1): 15 | raise NotImplementedError("Need to implemented!") 16 | 17 | def execute(self, sql, params=None): 18 | raise NotImplementedError("Need to implemented!") 19 | 20 | def test_connection(self): 21 | rows = self.query("SELECT 1") 22 | return len(rows) == 1 23 | 24 | def _connect(self): 25 | raise NotImplementedError("Need to implemented!") 26 | 27 | def _raise_retry_count(self, retry_count): 28 | if retry_count < 0: 29 | raise Exception("Retry time out") 30 | 31 | def _get_connection(self): 32 | if self.alive(): 33 | self._connect() 34 | 35 | def _commit(self): 36 | """ 完成SQL执行 37 | """ 38 | self.conn.commit() 39 | 40 | def _rollback(self): 41 | """ 回滚SQL 42 | """ 43 | self.conn.rollback() 44 | 45 | def _gen_cursor(self): 46 | cursor = self.conn.cursor() 47 | return cursor 48 | 49 | def alive(self): 50 | """ 测试连接是否还存在 51 | """ 52 | if self.conn: 53 | return True if self.conn.closed == 0 else False 54 | 55 | return False 56 | 57 | def close(self): 58 | """ 关闭连接 59 | """ 60 | self.conn.close() 61 | -------------------------------------------------------------------------------- /src/seed/drives/email.py: -------------------------------------------------------------------------------- 1 | import smtplib 2 | from email.mime.text import MIMEText 3 | from email.header import Header 4 | from email.utils import formatdate 5 | 6 | 7 | class Email(object): 8 | def __init__(self, configs): 9 | self.mail_backend = configs['MAIL_BACKEND'] 10 | self.mail_host = configs['MAIL_HOST'] 11 | self.mail_port = configs['MAIL_PORT'] 12 | self.mail_user = configs['MAIL_USER'] 13 | self.mail_password = configs['MAIL_PASSWORD'] 14 | self.mail_use_tls = configs['MAIL_USE_TLS'] 15 | self.mail_from = configs['MAIL_FROM'] 16 | 17 | def send_mail(self, to_mail_list, title, message, message_type='plain', cc_mail_list=[]): 18 | to_mails = '; '.join(to_mail_list) 19 | cc_mails = '; '.join(cc_mail_list) 20 | 21 | mime_text = MIMEText(message.encode('utf-8'), message_type, 'utf-8') 22 | mime_text['Subject'] = Header(title, 'utf-8') 23 | mime_text['From'] = self.mail_from 24 | mime_text['To'] = to_mails 25 | mime_text['Cc'] = cc_mails 26 | mime_text['Date'] = formatdate() 27 | 28 | smtp = smtplib.SMTP(self.mail_host, self.mail_port) 29 | smtp.ehlo() 30 | smtp.starttls() 31 | smtp.ehlo() 32 | smtp.login(self.mail_user, self.mail_password) 33 | 34 | smtp.sendmail(self.mail_from, to_mail_list+cc_mail_list, mime_text.as_string()) 35 | 36 | smtp.close() 37 | -------------------------------------------------------------------------------- /src/seed/drives/impala.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | from datetime import datetime, date 3 | 4 | from impala import dbapi 5 | 6 | from seed.drives.base import BaseDrive, DEFUALT_RETRY_COUNT 7 | 8 | 9 | class Impala(BaseDrive): 10 | def query(self, sql, params=None, retry_count=DEFUALT_RETRY_COUNT): 11 | """ 查询SQL语句 12 | """ 13 | self._raise_retry_count(retry_count) 14 | 15 | cursor = self._gen_cursor() 16 | try: 17 | query_data = self._query(cursor, sql, params) 18 | return query_data 19 | except (dbapi.OperationalError, dbapi.DatabaseError, dbapi.InterfaceError): 20 | if not retry_count: 21 | raise 22 | 23 | self._connect() 24 | return self.query(sql, params=params, retry_count=retry_count-1) 25 | finally: 26 | cursor.close() 27 | 28 | def _query(self, cursor, sql, params): 29 | self._execute(cursor, sql, params=params) 30 | columns_name = [d[0] for d in cursor.description] 31 | query_datas = [Row(zip(columns_name, row)) for row in cursor] 32 | 33 | return self._replace_type(query_datas) 34 | 35 | def _execute(self, cursor, sql, params=None): 36 | """ 执行SQL 37 | """ 38 | try: 39 | try: 40 | cursor.execute(sql, params) 41 | except Exception: 42 | cursor.execute("INVALIDATE METADATA;") 43 | cursor.execute(sql, params) 44 | self._commit() 45 | except dbapi.ProgrammingError as e: 46 | self._rollback() 47 | raise Exception(str(e)) 48 | except (dbapi.OperationalError, dbapi.DatabaseError, dbapi.InterfaceError) as e: 49 | raise 50 | except Exception: 51 | self._rollback() 52 | raise 53 | 54 | def _connect(self): 55 | try: 56 | self.conn = dbapi.connect( 57 | database=self.name, 58 | user=self.user, 59 | password=self.password, 60 | host=self.ip, 61 | port=self.port, 62 | ) 63 | except Exception: 64 | raise 65 | 66 | def _replace_type(self, datas): 67 | """ 替换数据类型 68 | """ 69 | for data in datas: 70 | for key, value in data.items(): 71 | if isinstance(value, datetime): 72 | data[key] = value.strftime('%Y-%m-%d') 73 | if isinstance(value, date): 74 | data[key] = value.strftime('%Y-%m-%d') 75 | if isinstance(value, decimal.Decimal): 76 | data[key] = float(value) 77 | return datas 78 | 79 | 80 | class Row(dict): 81 | """访问对象那样访问dict,行结果""" 82 | def __getattr__(self, name): 83 | try: 84 | return self[name] 85 | except KeyError: 86 | raise AttributeError(name) -------------------------------------------------------------------------------- /src/seed/drives/mysql.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | 3 | import pymysql 4 | from datetime import datetime, date 5 | 6 | # from seed.drives.postgresql import PostgreSQL 7 | from seed.drives.base import BaseDrive, DEFUALT_RETRY_COUNT 8 | 9 | 10 | class MySQL(BaseDrive): 11 | def query(self, sql, params=None, retry_count=DEFUALT_RETRY_COUNT): 12 | """ 查询SQL语句 13 | """ 14 | self._raise_retry_count(retry_count) 15 | 16 | cursor = self._gen_cursor() 17 | try: 18 | query_data = self._query(cursor, sql, params) 19 | return query_data 20 | except (pymysql.OperationalError, pymysql.DatabaseError, pymysql.InterfaceError) as e: 21 | if not retry_count: 22 | raise 23 | 24 | self._connect() 25 | return self.query(sql, params=params, retry_count=retry_count-1) 26 | finally: 27 | cursor.close() 28 | 29 | def _query(self, cursor, sql, params): 30 | self._execute(cursor, sql, params=params) 31 | datas = list(cursor.fetchall()) 32 | return self._replace_type(datas) 33 | 34 | def _execute(self, cursor, sql, params=None): 35 | """ 执行SQL 36 | """ 37 | try: 38 | cursor.execute(sql, params) 39 | self._commit() 40 | except pymysql.ProgrammingError as e: 41 | self._rollback() 42 | raise Exception(str(e)) 43 | except (pymysql.OperationalError, pymysql.DatabaseError, pymysql.InterfaceError) as e: 44 | raise 45 | except Exception as e: 46 | self._rollback() 47 | raise 48 | 49 | def _connect(self): 50 | try: 51 | self.conn = pymysql.connect( 52 | db=self.name, 53 | user=self.user, 54 | password=self.password, 55 | host=self.ip, 56 | port=self.port, 57 | charset='utf8', 58 | cursorclass=pymysql.cursors.DictCursor 59 | ) 60 | except Exception as e: 61 | raise 62 | 63 | def _replace_type(self, datas): 64 | """ 替换数据类型 65 | """ 66 | for data in datas: 67 | for key, value in data.items(): 68 | if isinstance(value, datetime): 69 | data[key] = value.strftime('%Y-%m-%d') 70 | if isinstance(value, date): 71 | data[key] = value.strftime('%Y-%m-%d') 72 | if isinstance(value, decimal.Decimal): 73 | data[key] = float(value) 74 | return datas 75 | -------------------------------------------------------------------------------- /src/seed/drives/postgresql.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | from datetime import datetime, date 3 | 4 | import psycopg2 5 | 6 | from seed.drives.base import BaseDrive, DEFUALT_RETRY_COUNT 7 | 8 | # 解决 pg TypeError: Decimal('30737') is not JSON serializable的问题 9 | DEC2FLOAT = psycopg2.extensions.new_type( 10 | psycopg2.extensions.DECIMAL.values, 11 | 'DEC2FLOAT', 12 | lambda value, curs: float(value) if value is not None else None) 13 | psycopg2.extensions.register_type(DEC2FLOAT) 14 | # 解决DataTime is not Json serializable的问题 15 | DATE2STR = psycopg2.extensions.new_type( 16 | psycopg2.extensions.PYDATE.values, 17 | 'DATE2STR', 18 | lambda value, curs: value.split('.')[0] if value is not None else None) 19 | psycopg2.extensions.register_type(DATE2STR) 20 | DATETIME2STR = psycopg2.extensions.new_type( 21 | psycopg2.extensions.PYDATETIME.values, 22 | 'DATETIME2STR', 23 | lambda value, curs: value.split('.')[0] if value is not None else None) 24 | psycopg2.extensions.register_type(DATETIME2STR) 25 | 26 | 27 | class PostgreSQL(BaseDrive): 28 | def query(self, sql, params=None, retry_count=DEFUALT_RETRY_COUNT): 29 | """ 查询SQL语句 30 | """ 31 | self._raise_retry_count(retry_count) 32 | 33 | cursor = self._gen_cursor() 34 | try: 35 | query_datas = self._query(cursor, sql, params) 36 | return query_datas 37 | except (psycopg2.OperationalError, psycopg2.DatabaseError, psycopg2.InterfaceError) as e: 38 | # 可能数据库会主动断开连接 39 | # 尝试重新连接, 并重试 40 | if not retry_count: 41 | raise 42 | 43 | self._connect() 44 | return self.query(sql, params=params, retry_count=retry_count-1) 45 | finally: 46 | cursor.close() 47 | 48 | def _query(self, cursor, sql, params): 49 | self._execute(cursor, sql, params=params) 50 | columns_name = [d[0] for d in cursor.description] 51 | query_datas = [Row(zip(columns_name, row)) for row in cursor] 52 | 53 | return self._replace_type(query_datas) 54 | 55 | def _execute(self, cursor, sql, params=None): 56 | """ 执行SQL 57 | """ 58 | try: 59 | cursor.execute(sql, params) 60 | self._commit() 61 | except psycopg2.ProgrammingError as e: 62 | # syntax error 63 | self._rollback() 64 | raise Exception(str(e)) 65 | except psycopg2.extensions.QueryCanceledError as e: 66 | raise psycopg2.QueryCanceledError(str(e)) 67 | except (psycopg2.OperationalError, psycopg2.DatabaseError, psycopg2.InterfaceError) as e: 68 | # server closed the connection unexpectedly 69 | raise 70 | except Exception as e: 71 | self._rollback() 72 | raise 73 | 74 | def _connect(self): 75 | try: 76 | self.conn = psycopg2.connect( 77 | dbname=self.name, 78 | user=self.user, 79 | password=self.password, 80 | host=self.ip, 81 | port=self.port, 82 | options='-c statement_timeout=60s', 83 | ) 84 | except Exception as e: 85 | raise 86 | 87 | def _replace_type(self, datas): 88 | """ 替换数据类型 89 | """ 90 | for data in datas: 91 | for key, value in data.items(): 92 | if isinstance(value, datetime): 93 | data[key] = value.strftime('%Y-%m-%d') 94 | if isinstance(value, date): 95 | data[key] = value.strftime('%Y-%m-%d') 96 | if isinstance(value, decimal.Decimal): 97 | data[key] = float(value) 98 | return datas 99 | 100 | 101 | class Row(dict): 102 | 103 | """访问对象那样访问dict,行结果""" 104 | 105 | def __getattr__(self, name): 106 | try: 107 | return self[name] 108 | except KeyError: 109 | raise AttributeError(name) -------------------------------------------------------------------------------- /src/seed/libs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/libs/__init__.py -------------------------------------------------------------------------------- /src/seed/libs/data_access/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/libs/data_access/__init__.py -------------------------------------------------------------------------------- /src/seed/libs/data_access/app.py: -------------------------------------------------------------------------------- 1 | from seed.libs.data_access.dataware.base import DataModel 2 | from seed.libs.data_access.middledata.base import MiddleData 3 | from seed.libs.data_access.formatters.base import FormatterFactory 4 | 5 | 6 | class DataAccess(object): 7 | def __init__(self, dtype, db, *args, **kwargs): 8 | # self.dtype = dtype 9 | self.db = db 10 | 11 | self.sql = kwargs['sql'] 12 | if not self.sql: 13 | raise Exception("SQL不能为空!") 14 | 15 | self.indexs = kwargs['indexs'] 16 | if kwargs.get("charttype", "table") in ('table',): 17 | if not self.indexs: 18 | pass 19 | else: 20 | if not self.indexs: 21 | raise Exception("指标不能为空!") 22 | 23 | self.dimensions = kwargs['dimensions'] 24 | if not self.dimensions: 25 | raise Exception("维度不能为空!") 26 | 27 | self.query = kwargs.get('query', {}) 28 | 29 | self.charttype = kwargs.get('charttype', 'chart') 30 | 31 | self.format_args = {'vth_columns': kwargs.get('vth_columns', [])} 32 | 33 | def get_datas(self): 34 | # 获取原数据 35 | try: 36 | source_data, sql_info = self.get_source_data() 37 | middle_data = self.transfer_middle_data(source_data) 38 | except Exception as e: 39 | raise Exception('数据库取数错误: %s' % str(e)) 40 | 41 | # 格式化 42 | try: 43 | format_data = self.format_data(middle_data) 44 | except Exception as e: 45 | raise Exception('数据格式化出错: %s' % str(e)) 46 | 47 | return format_data 48 | 49 | def get_source_data(self): 50 | source_data, sql_info = DataModel( 51 | self.db, self.sql, self.query, self.dimensions, self.indexs 52 | ).query_data() 53 | return source_data, sql_info 54 | 55 | def transfer_middle_data(self, source_data): 56 | middle_data = MiddleData( 57 | source_data, self.dimensions, self.indexs 58 | ).convert() 59 | return middle_data 60 | 61 | def format_data(self, middle_data): 62 | 63 | formatter = FormatterFactory(self.charttype).formatter_class() 64 | format_data = formatter( 65 | self.indexs, self.dimensions, middle_data, self.format_args 66 | ).format_data() 67 | 68 | return format_data 69 | -------------------------------------------------------------------------------- /src/seed/libs/data_access/dataware/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/libs/data_access/dataware/__init__.py -------------------------------------------------------------------------------- /src/seed/libs/data_access/dataware/base.py: -------------------------------------------------------------------------------- 1 | class DataModel(object): 2 | def __init__(self, db, sql, query, dimensions, indexs): 3 | self.db = db 4 | self.sql = sql 5 | self.query = query 6 | self.dimensions = dimensions 7 | self.indexs = indexs 8 | self.gen_sql() 9 | 10 | def gen_sql(self): 11 | """ 用维度和指标把SQL包一层 12 | """ 13 | select_fileds = [item['dimension'] for item in self.dimensions]\ 14 | + ['sum('+item['index']+') as ' + item['index'] for item in self.indexs] 15 | group_fileds = [item['dimension'] for item in self.dimensions] 16 | self.sql = """ 17 | SELECT 18 | {select_fileds} 19 | FROM ( 20 | {origin_sql} 21 | ) a 22 | GROUP BY 23 | {group_fileds} 24 | """.format( 25 | select_fileds=','.join(select_fileds), 26 | origin_sql=self.sql, 27 | group_fileds=','.join(group_fileds) 28 | ) 29 | 30 | def format_sql(self): 31 | query_list = [] 32 | for key, value in self.query.items(): 33 | if isinstance(value, list): 34 | # SQL in 连接条件只有一个元素时,不能带逗号. 故需分开处理 35 | if len(value) > 1: 36 | for va in value: 37 | query_list.append(va) 38 | 39 | self.query[key] = tuple(query_list) 40 | else: 41 | self.query[key] = "('%s')" % ', '.join([str(item) for item in value]) 42 | 43 | return self.sql.format(**self.query) 44 | 45 | def query_data(self): 46 | sql = self.format_sql() 47 | # SQL中双引号会被视为列,故需替换为单引号 48 | sql.replace('"', "'") 49 | print("formated sql:", sql) 50 | return self.db.query(sql), sql 51 | -------------------------------------------------------------------------------- /src/seed/libs/data_access/formatters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/libs/data_access/formatters/__init__.py -------------------------------------------------------------------------------- /src/seed/libs/data_access/formatters/bar.py: -------------------------------------------------------------------------------- 1 | from seed.libs.data_access.formatters.line import LineFormatter 2 | 3 | 4 | class BarFormatter(LineFormatter): 5 | def __init__(self, *args, **kwargs): 6 | super(BarFormatter, self).__init__(*args, **kwargs) -------------------------------------------------------------------------------- /src/seed/libs/data_access/formatters/base.py: -------------------------------------------------------------------------------- 1 | from inspect import isclass 2 | 3 | from seed.libs.data_access.utils.auto_register import get_package_members, get_immediate_cls_attr 4 | 5 | 6 | class FormatterFactory(object): 7 | registerd_charttypes = {} 8 | 9 | def __init__(self, charttype): 10 | self.charttype = charttype 11 | 12 | def formatter_class(self): 13 | from seed.libs.data_access.formatters.line import LineFormatter 14 | if self.charttype not in self.registerd_charttypes: 15 | self._register_charttypes() 16 | 17 | return self.registerd_charttypes.get(self.charttype.lower()+'formatter', LineFormatter) 18 | 19 | def _register_charttypes(self): 20 | from seed.libs.data_access import formatters 21 | predicate = lambda m: isclass(m) and issubclass(m, BaseFormatter) and not get_immediate_cls_attr(m, '__abstract__') 22 | members = get_package_members(formatters, predicate) 23 | self.registerd_charttypes = {member.__name__.lower(): member for member in members} 24 | 25 | 26 | class BaseFormatter(object): 27 | __abstract__ = True 28 | 29 | def __init__(self, indexs, dimensions, data, format_args): 30 | self.indexs = indexs 31 | self.dimensions = dimensions 32 | self.data = data 33 | self.format_args = format_args 34 | -------------------------------------------------------------------------------- /src/seed/libs/data_access/formatters/funnel.py: -------------------------------------------------------------------------------- 1 | import json 2 | from seed.libs.data_access.formatters.line import LineFormatter 3 | 4 | 5 | class FunnelFormatter(LineFormatter): 6 | def __init__(self, *args, **kwargs): 7 | super(FunnelFormatter, self).__init__(*args, **kwargs) 8 | 9 | def format_data(self): 10 | result = {"series": []} 11 | ret = {"data": []} 12 | for dimenstr, datas in self.data.items(): 13 | info = {} 14 | dimendict = json.loads(dimenstr) 15 | namelist = [dimen.get("dimension", "") for dimen in self.dimensions] 16 | infoname = [] 17 | for name in namelist: 18 | infoname.append(str(dimendict.get(name, ""))) 19 | 20 | info["name"] = "-".join(infoname) 21 | info["value"] = 0 22 | for key, value in datas.items(): 23 | if isinstance(value, (float, int)): 24 | info["value"] += value 25 | ret["data"].append(info) 26 | 27 | result["series"].append(ret) 28 | 29 | return result 30 | -------------------------------------------------------------------------------- /src/seed/libs/data_access/formatters/line.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from seed.libs.data_access.formatters.base import BaseFormatter 4 | 5 | 6 | class LineFormatter(BaseFormatter): 7 | def __init__(self, *args, **kwargs): 8 | super(LineFormatter, self).__init__(*args, **kwargs) 9 | 10 | def format_data(self): 11 | # 得到分布项 12 | categories, series = self._get_chart_categories_and_series() 13 | 14 | # 获取series图形数据 15 | series = self._convert_series_data(categories, series) 16 | 17 | return {"categories": categories, "series": series} 18 | 19 | def _get_chart_categories_and_series(self): 20 | """ 21 | 获取categoies和series的维度 22 | """ 23 | series = [] 24 | 25 | category_columns, series_columns = self._get_chart_columns() 26 | 27 | # 从数据中获取到对应的categories然后通过数据的维度进行组合 28 | categories = self._get_categories_sort_by_dimensions(category_columns, series_columns, series) 29 | 30 | categories = self._get_categories_sort_by_indexs(categories, category_columns) 31 | 32 | if not series: 33 | series = [item['index'] for item in self.indexs] 34 | 35 | return categories, series 36 | 37 | def _get_categories_sort_by_dimensions(self, category_columns, series_columns, series): 38 | categories_dict = [] 39 | 40 | for key in self.data.keys(): 41 | key = json.loads(key) 42 | temp_dict = {} 43 | cateogry = {category_column: key[category_column] for category_column in category_columns} 44 | ser_key = {series_column: key[series_column] for series_column in series_columns} 45 | temp_dict.update(cateogry) 46 | temp_dict.update(ser_key) 47 | 48 | if temp_dict not in categories_dict: 49 | categories_dict.append(temp_dict) 50 | 51 | if series_columns: 52 | series_key = '-'.join([str(key[series_column]) for series_column in series_columns]) 53 | if series_key not in series: 54 | series.append(series_key) 55 | 56 | # categories排序 57 | for dimension in self.dimensions: 58 | if dimension['sort'] == 'desc': 59 | categories_dict.sort(key=lambda x: x[dimension['dimension']], reverse=True) 60 | if dimension['sort'] == 'asc': 61 | categories_dict.sort(key=lambda x: x[dimension['dimension']]) 62 | 63 | categories = [ 64 | '-'.join([str(category_dict[category_column]) for category_column in category_columns]) 65 | for category_dict in categories_dict 66 | ] 67 | return categories 68 | 69 | def _get_chart_columns(self): 70 | category_columns, series_columns = [item['dimension'] for item in self.dimensions], [] 71 | return category_columns, series_columns 72 | 73 | def _convert_series_data(self, categories, series): 74 | # 从数据中获取到趋势类型的数据 75 | category_columns, series_columns = self._get_chart_columns() 76 | series_map = {index['index']: index for index in self.indexs} 77 | 78 | middle_data = {} 79 | if series_columns: 80 | for key, value in self.data.items(): 81 | key = json.loads(key) 82 | category_key = '-'.join([str(key[category_column]) for category_column in category_columns]) 83 | if series_columns: 84 | series_key = '-'.join([str(key[series_column]) for series_column in series_columns]) 85 | middle_data.setdefault(category_key, {})[series_key] = sum(value.values()) 86 | else: 87 | middle_data = { 88 | '-'.join([str(json.loads(key)[category_column]) for category_column in category_columns]): value for 89 | key, value in self.data.items() 90 | } 91 | 92 | series_datas = [] 93 | for serie in series: 94 | data = [] 95 | for category in categories: 96 | data.append(middle_data.get(str(category), {}).get(serie, '-')) 97 | 98 | series_data = {'data': data, 'dim': serie, 'name': serie} 99 | series_data.update(series_map.get(serie, {})) 100 | series_datas.append(series_data) 101 | 102 | return series_datas 103 | 104 | def _get_categories_sort_by_indexs(self, categories, category_columns): 105 | if not len(categories): 106 | return categories 107 | 108 | if any(index['sort'] in ('desc', 'asc') for index in self.indexs): 109 | # 按指标进行排序 110 | categories = self._categories_sort_by_value(category_columns) 111 | 112 | return categories 113 | 114 | def _categories_sort_by_value(self, category_columns): 115 | middle_date = {} 116 | 117 | sort_index = None 118 | sort_reverse = None 119 | for index in self.indexs: 120 | if index['sort'] in ('desc', 'asc'): 121 | sort_index = index['index'] 122 | sort_reverse = True if index['sort'] != 'desc' else False 123 | break 124 | 125 | for i, j in self.data.items(): 126 | i = json.loads(i) 127 | middle_date.update( 128 | {'-'.join([str(i[category_column]) for category_column in category_columns]): j[sort_index]} 129 | ) 130 | categories_data = sorted(middle_date.items(), key=lambda d: d[1] if d[1] else 0, reverse=sort_reverse) 131 | categories = [v[0] for v in categories_data] 132 | return categories 133 | -------------------------------------------------------------------------------- /src/seed/libs/data_access/formatters/linestack.py: -------------------------------------------------------------------------------- 1 | from seed.libs.data_access.formatters.line import LineFormatter 2 | 3 | 4 | class LineStackFormatter(LineFormatter): 5 | def __init__(self, *args, **kwargs): 6 | super(LineStackFormatter, self).__init__(*args, **kwargs) 7 | 8 | def _get_chart_columns(self): 9 | category_columns = ['fdate'] 10 | 11 | series_columns = [item['dimension'] for item in self.dimensions if item['dimension'] not in category_columns] 12 | 13 | return category_columns, series_columns 14 | 15 | def _get_chart_categories_and_series(self): 16 | categories, series = super(LineStackFormatter, self)._get_chart_categories_and_series() 17 | categories = list(set(categories)) 18 | 19 | # 全是数字 20 | is_num = all([isinstance(c, (int, float)) for c in categories]) if categories else False 21 | # 全是字符串数字 22 | is_strnum = all([isinstance(c, (str,)) and c.isdigit() for c in categories]) if categories else False 23 | 24 | if is_num or is_strnum: 25 | categories = sorted(categories, key=lambda k: float(k)) 26 | else: 27 | categories = sorted(categories) 28 | 29 | return categories, series 30 | -------------------------------------------------------------------------------- /src/seed/libs/data_access/formatters/map.py: -------------------------------------------------------------------------------- 1 | import json 2 | from seed.libs.data_access.formatters.base import BaseFormatter 3 | 4 | 5 | class MapFormatter(BaseFormatter): 6 | def __init__(self, *args, **kwargs): 7 | super(MapFormatter, self).__init__(*args, **kwargs) 8 | 9 | def format_data(self): 10 | data = [] 11 | for key, value in self.data.items(): 12 | k = json.loads(key) 13 | k.update(value) 14 | data.append(k) 15 | 16 | return {"data": data} 17 | -------------------------------------------------------------------------------- /src/seed/libs/data_access/formatters/pie.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | # from seed.libs.data_access.formatters.base import BaseFormatter 4 | from seed.libs.data_access.formatters.line import LineFormatter 5 | 6 | 7 | class PieFormatter(LineFormatter): 8 | def __init__(self, *args, **kwargs): 9 | super(PieFormatter, self).__init__(*args, **kwargs) -------------------------------------------------------------------------------- /src/seed/libs/data_access/formatters/sankey.py: -------------------------------------------------------------------------------- 1 | import json 2 | from seed.libs.data_access.formatters.base import BaseFormatter 3 | 4 | 5 | class SankeyFormatter(BaseFormatter): 6 | def __init__(self, *args, **kwargs): 7 | super(SankeyFormatter, self).__init__(*args, **kwargs) 8 | 9 | def format_data(self): 10 | links = [] 11 | node_list = [] 12 | nodes = [] 13 | for key, value in self.data.items(): 14 | data = json.loads(key) 15 | for _, v in data.items(): 16 | node_list.append(v) 17 | 18 | tmp = {} 19 | for _, v in value.items(): 20 | tmp['value'] = v 21 | 22 | data.update(tmp) 23 | links.append(data) 24 | 25 | node_list = list(set(node_list)) 26 | for n in node_list: 27 | nodes.append({"name": n}) 28 | 29 | return {"links": links, "nodes": nodes} 30 | -------------------------------------------------------------------------------- /src/seed/libs/data_access/formatters/table.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | 4 | from seed.libs.data_access.formatters.base import BaseFormatter 5 | 6 | 7 | class TableFormatter(BaseFormatter): 8 | def __init__(self, *args, **kwargs): 9 | super(TableFormatter, self).__init__(*args, **kwargs) 10 | 11 | def format_data(self): 12 | 13 | self._ver_to_hor() 14 | 15 | table_keys = [item['dimension'] for item in self.dimensions] + [item['index'] for item in self.indexs] 16 | 17 | # 得到table的表格数据 18 | table_datas = self._convert_table_data(table_keys, self.data) 19 | 20 | # 对表格数据进行排序 21 | table_datas = self._sort_table_column(table_datas) 22 | 23 | # 处理比率类型数据 24 | table_datas = self._convert_rate_data(table_keys, table_datas) 25 | 26 | # 计算总值均值数据 27 | table_datas = self._compute_total_and_mean(table_datas) 28 | 29 | # 得到table的显示项 30 | display_name = self._get_display_name(table_keys) 31 | 32 | return {"data": table_datas, "displayName": display_name} 33 | 34 | def _ver_to_hor(self): 35 | if not self.format_args.get('vth_columns'): 36 | return 37 | 38 | converted_data = {} 39 | 40 | index = self.indexs[0]['index'] 41 | vth_columns = self.format_args.get('vth_columns') 42 | 43 | self.dimensions = [item for item in self.dimensions if item['dimension'] not in vth_columns] 44 | self.indexs = [] 45 | 46 | for dimensions, values in self.data.items(): 47 | dimensions = json.loads(dimensions) 48 | converted_dimensions, vth_dimensions = {}, [] 49 | for key, value in dimensions.items(): 50 | if key in vth_columns: 51 | vth_dimensions.append(str(value)) 52 | else: 53 | converted_dimensions[key] = value 54 | if '-'.join(vth_dimensions) not in self.indexs: 55 | self.indexs.append('-'.join(vth_dimensions)) 56 | converted_data.setdefault(json.dumps(converted_dimensions), {})['-'.join(vth_dimensions)] = values[index] 57 | 58 | self.indexs = [{'index': index, 'name': index} for index in self.indexs] 59 | self.data = converted_data 60 | 61 | def _convert_table_data(self, table_keys, datas): 62 | # 将数据从中间格式转换成表格格式 63 | table_datas = [] 64 | 65 | for key, values in datas.items(): 66 | values.update(json.loads(key)) 67 | row = {} 68 | for key in table_keys: 69 | row[key] = values.get(key, '-') 70 | 71 | table_datas.append(row) 72 | 73 | return table_datas 74 | 75 | def _sort_table_column(self, data): 76 | # 进行table类型的排序 77 | dimen_indexs = self.dimensions + self.indexs 78 | 79 | field_list = [] 80 | for di in dimen_indexs: 81 | tmp = {} 82 | if di.get("dimension"): 83 | tmp["field"] = di.get("dimension") 84 | elif di.get("index"): 85 | tmp["field"] = di.get("index") 86 | 87 | tmp["sort"] = di.get("sort") 88 | field_list.append(tmp) 89 | 90 | # 倒序让排序时维度排序为主 91 | field_list.reverse() 92 | 93 | for field in field_list: 94 | if field['sort'] == 'desc': 95 | data.sort(key=lambda x: x[field["field"]] if isinstance(x[field["field"]], (float, int)) else 0, 96 | reverse=True) 97 | if field['sort'] == 'asc': 98 | data.sort(key=lambda x: x[field["field"]] if isinstance(x[field["field"]], (float, int)) else 0) 99 | 100 | return data 101 | 102 | def _convert_rate_data(self, table_keys, datas): 103 | """处理比率类型指标的数据""" 104 | table_datas = [] 105 | 106 | indexs = self.indexs[:] 107 | temp_indexs = [i['index'] for i in indexs] 108 | 109 | for tk in table_keys: 110 | if tk not in temp_indexs: 111 | indexs.append({"index": tk}) 112 | 113 | for data in datas: 114 | row = {} 115 | for index in indexs: 116 | row[index['index']] = self._format_rate_data(index.get('rate'), data.get(index['index'], '-')) 117 | 118 | table_datas.append(row) 119 | 120 | return table_datas 121 | 122 | def _format_rate_data(self, israte, data): 123 | """转换比例数据""" 124 | if not israte: 125 | return data 126 | 127 | if isinstance(data, (float, int)): 128 | ratedata = str(round(data * 100, 2)) + '%' 129 | return ratedata 130 | 131 | return data 132 | 133 | def _compute_total_and_mean(self, datas): 134 | """计算总值均值数据""" 135 | 136 | rate_dimension = [item['dimension'] for item in self.dimensions] + [item['index'] for item in self.indexs if 137 | item["rate"]] 138 | num = len(datas) 139 | 140 | total_dict = {} 141 | mean_dict = {} 142 | 143 | for data in datas: 144 | for key, value in data.items(): 145 | total_dict[key] = total_dict.setdefault(key, 0) + ( 146 | value if value and isinstance(value, (int, float)) else 0) 147 | 148 | for k, v in total_dict.items(): 149 | mean_dict[k] = round(v / float(num), 2) 150 | 151 | first_dimension = self.dimensions[0]["dimension"] 152 | 153 | for k, v in total_dict.items(): 154 | if k in rate_dimension: 155 | total_dict[k] = '-' 156 | else: 157 | total_dict[k] = round(v, 2) 158 | 159 | total_dict[first_dimension] = "总值" 160 | 161 | for k, v in mean_dict.items(): 162 | if k in rate_dimension: 163 | mean_dict[k] = '-' 164 | 165 | mean_dict[first_dimension] = "均值" 166 | 167 | datas.append(total_dict) 168 | datas.append(mean_dict) 169 | 170 | return datas 171 | 172 | def _get_display_name(self, keys): 173 | # 获取displayName格式的数据显示 174 | titles = [] 175 | 176 | dim_display_map = {} 177 | 178 | for item in self.dimensions: 179 | dim_display_map[item['dimension']] = item['name'] 180 | 181 | for item in self.indexs: 182 | dim_display_map[item['index']] = item['name'] 183 | 184 | for key in keys: 185 | titles.append({"name": key, "displayName": dim_display_map[key]}) 186 | 187 | return titles 188 | -------------------------------------------------------------------------------- /src/seed/libs/data_access/middledata/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/libs/data_access/middledata/__init__.py -------------------------------------------------------------------------------- /src/seed/libs/data_access/middledata/base.py: -------------------------------------------------------------------------------- 1 | import json 2 | import collections 3 | 4 | 5 | class MiddleData(object): 6 | """ 原始数据格式 转 中间数据格式 7 | row = [ 8 | {'dau': 100, 'dsu': 200, 'fdate': '2017-01-01'}, 9 | {'dau': 100, 'dsu': 200, 'fdate': '2017-01-02'}, 10 | ] 11 | 12 | reuslt = { 13 | json.loads({'fdata': '2017-01-01'}): {'dau': 100, 'dsu': 200}, 14 | json.loads({'fdata': '2017-01-02'}): {'dau': 200, 'dsu': 100}, 15 | } 16 | """ 17 | 18 | def __init__(self, source_data, dimensions, indexs): 19 | self.source_data = source_data 20 | self.dimensions = [item['dimension'] for item in dimensions] 21 | self.indexs = [item['index'] for item in indexs] 22 | 23 | def convert(self): 24 | middle_datas = {} 25 | # 兼容表格指标可以为空的情况 26 | self.indexs = self.dimensions if not self.indexs else self.indexs 27 | 28 | for row in self.source_data: 29 | 30 | # 根据dimensions构造当前数据项的key 31 | row_keys = collections.OrderedDict() 32 | for dimension in self.dimensions: 33 | row_keys[dimension] = row[dimension] 34 | 35 | for index in self.indexs: 36 | middle_key = json.dumps(row_keys) 37 | if middle_datas.get(middle_key, {}).get(index) is None: 38 | middle_datas.setdefault(middle_key, {})[index] = row.get(index, None) 39 | 40 | return middle_datas 41 | -------------------------------------------------------------------------------- /src/seed/libs/data_access/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/libs/data_access/utils/__init__.py -------------------------------------------------------------------------------- /src/seed/libs/data_access/utils/auto_register.py: -------------------------------------------------------------------------------- 1 | from inspect import getmembers 2 | 3 | 4 | def get_module_members(modules, predicate): 5 | members = getmembers(modules, predicate) 6 | return map(lambda m: m[1], members) 7 | 8 | 9 | def get_package_members(package, predicate): 10 | from pkgutil import iter_modules 11 | from importlib import import_module 12 | 13 | members = [] 14 | for _, name, ispkg in iter_modules(package.__path__, package.__name__+'.'): 15 | if ispkg: 16 | sub_members = get_package_members( 17 | import_module(name), predicate 18 | ) 19 | members.append(sub_members) 20 | 21 | members.extend(get_module_members(import_module(name), predicate)) 22 | 23 | return members 24 | 25 | 26 | def get_immediate_cls_attr(cls, attrname): 27 | if not issubclass(cls, object): 28 | return None 29 | 30 | for base in cls.__mro__: 31 | if attrname in base.__dict__ and base is cls: 32 | return getattr(base, attrname) 33 | 34 | return None 35 | -------------------------------------------------------------------------------- /src/seed/libs/filter_access/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/libs/filter_access/__init__.py -------------------------------------------------------------------------------- /src/seed/libs/filter_access/app.py: -------------------------------------------------------------------------------- 1 | class FilterAccess(object): 2 | def __init__(self, db, conditions, query, *args, **kwargs): 3 | self.db_instance = db 4 | self.sql = conditions 5 | self.query = query 6 | 7 | def query_datas(self): 8 | for key, value in self.query.items(): 9 | if isinstance(value, list): 10 | self.query[key] = '(%s)' % ', '.join([str(item) for item in value]) 11 | 12 | sql = self.sql.format(**self.query) 13 | print(sql) 14 | 15 | rows = self.db_instance.query(sql) 16 | return rows 17 | -------------------------------------------------------------------------------- /src/seed/logging/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/logging/README.md -------------------------------------------------------------------------------- /src/seed/logging/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/logging/__init__.py -------------------------------------------------------------------------------- /src/seed/logging/handlers.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/logging/handlers.py -------------------------------------------------------------------------------- /src/seed/models/__init__.py: -------------------------------------------------------------------------------- 1 | from ._base import * 2 | from .account import * 3 | from .bussiness import * 4 | from .databases import * 5 | from .menu import * 6 | from .role import * 7 | from .bmanager import * 8 | from .buser import * 9 | from .panels import * 10 | from .filters import * 11 | 12 | from .analogdatas import * 13 | -------------------------------------------------------------------------------- /src/seed/models/_base.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import datetime 3 | 4 | from flask import g 5 | from flask_migrate import Migrate 6 | from flask_marshmallow import Marshmallow 7 | from sqlalchemy.sql import sqltypes 8 | from sqlalchemy.ext.declarative import declared_attr 9 | 10 | from flask_sqlalchemy import SQLAlchemy, BaseQuery, orm 11 | 12 | from seed.utils.time import convert_utc_to_local 13 | 14 | __all__ = ['db', 'ma', 'migrate', 'session', 'BaseModel', 'BussinessModel'] 15 | 16 | 17 | DEFAULT_DATETIME = '1970-01-01 00:00:00' 18 | 19 | 20 | class SeedQuery(BaseQuery): 21 | def __init__(self, *args, **kwargs): 22 | super(SeedQuery, self).__init__(*args, **kwargs) 23 | 24 | def as_list(self, *clolumns): 25 | return [{key: getattr(row, key, None) for key in row.keys()} for row in self] 26 | 27 | 28 | class SessionMixin(object): 29 | column_filter = [] 30 | 31 | def __init__(self, **kwargs): 32 | for k, v in kwargs.items(): 33 | setattr(self, k, v) 34 | 35 | def save(self): 36 | db.session.add(self) 37 | db.session.commit() 38 | return self 39 | 40 | def flush(self): 41 | db.session.flush(self) 42 | db.session.commit() 43 | return self 44 | 45 | def delete(self): 46 | db.session.delete(self) 47 | db.session.commit() 48 | return self 49 | 50 | def row2dict(self): 51 | d = {} 52 | for column in self.__table__.columns: 53 | if column.name in self.column_filter: 54 | continue 55 | 56 | if isinstance(column.type, sqltypes.DateTime): 57 | local_time = convert_utc_to_local(getattr(self, column.name), 'Asia/Shanghai') 58 | d[column.name] = local_time.strftime('%Y-%m-%d %H:%M:%S') if local_time else DEFAULT_DATETIME 59 | else: 60 | d[column.name] = getattr(self, column.name) 61 | return d 62 | 63 | 64 | db = SQLAlchemy(query_class=SeedQuery) 65 | migrate = Migrate() 66 | session = orm.scoped_session(orm.sessionmaker(autocommit=True)) 67 | ma = Marshmallow() 68 | 69 | 70 | class BaseModel(db.Model, SessionMixin): 71 | __abstract__ = True 72 | column_filter = ['created', 'updated'] 73 | 74 | id = db.Column(db.Integer, primary_key=True) 75 | created = db.Column( 76 | db.DateTime, nullable=False, default=datetime.utcnow() 77 | ) 78 | updated = db.Column( 79 | db.DateTime, default=datetime.utcnow(), 80 | onupdate=datetime.utcnow() 81 | ) 82 | 83 | def __init__(self, *args, **kwargs): 84 | super(BaseModel, self).__init__(*args, **kwargs) 85 | 86 | def delete(self): 87 | if hasattr(self, 'status'): 88 | self.status = -1 89 | self.save() 90 | else: 91 | super(BaseModel, self).delete() 92 | 93 | 94 | class BussinessModel(BaseModel): 95 | """ 与业务绑定的Model 96 | """ 97 | __abstract__ = True 98 | column_filter = ['created', 'updated', 'bussiness_id'] 99 | 100 | @declared_attr 101 | def bussiness_id(cls): 102 | return db.Column(db.Integer, db.ForeignKey('bussiness.id')) 103 | 104 | def __init__(self, *args, **kwargs): 105 | self.bussiness_id = g.bussiness_id 106 | super(BussinessModel, self).__init__(*args, **kwargs) 107 | 108 | 109 | class PageModel(BussinessModel): 110 | """ 与页面绑定的Model 111 | """ 112 | __abstract__ = True 113 | 114 | column_filter = ['created', 'updated', 'bussiness_id', 'page_id'] 115 | 116 | @declared_attr 117 | def page_id(cls): 118 | return db.Column(db.Integer, db.ForeignKey('menu.id')) -------------------------------------------------------------------------------- /src/seed/models/account.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | # from flask.ext.sqlalchemy import Column 3 | from ._base import db, BaseModel 4 | 5 | 6 | __all__ = ['Account',] 7 | 8 | 9 | class Account(BaseModel): 10 | column_filter = ['updated', 'password'] 11 | 12 | sso_id = db.Column(db.Integer, default=-1) # SSO_ID 13 | 14 | account = db.Column(db.String(40), unique=True, index=True, nullable=False) 15 | password = db.Column(db.String(256)) 16 | email = db.Column(db.String(40), nullable=False) 17 | 18 | avatar = db.Column(db.Text) 19 | name = db.Column(db.String(40), nullable=False) 20 | sex = db.Column(db.String(20), nullable=False, default='male') 21 | depart_id = db.Column(db.Integer, default=-1) # -1为未知 22 | 23 | role = db.Column(db.String(20), default='new') 24 | status = db.Column(db.Integer, default=0) # -1:不可用 0: 未激活 1: 正常使用 25 | 26 | login_at = db.Column( 27 | db.DateTime, default=datetime.utcnow() 28 | ) -------------------------------------------------------------------------------- /src/seed/models/analogdatas.py: -------------------------------------------------------------------------------- 1 | from seed.models._base import db 2 | 3 | __all__ = ['AnalogdataDimensions', 'AnalogdataGamedatas'] 4 | 5 | 6 | class AnalogdataDimensions(db.Model): 7 | """ 测试数据-维度表 8 | """ 9 | id = db.Column(db.Integer, primary_key=True) 10 | game_id = db.Column(db.Integer) 11 | game_name = db.Column(db.Text) 12 | platform_id = db.Column(db.Integer) 13 | platform_name = db.Column(db.Text) 14 | version_id = db.Column(db.Integer) 15 | version_name = db.Column(db.Text) 16 | 17 | 18 | class AnalogdataGamedatas(db.Model): 19 | """ 测试数据-数据表 20 | """ 21 | id = db.Column(db.Integer, primary_key=True) 22 | fdate = db.Column(db.Text) 23 | game_id = db.Column(db.Integer) 24 | platform_id = db.Column(db.Integer) 25 | version_id = db.Column(db.Integer) 26 | fregucnt = db.Column(db.Integer) 27 | factucnt = db.Column(db.Integer) 28 | fpayucnt = db.Column(db.Integer) 29 | fincome = db.Column(db.Float) 30 | -------------------------------------------------------------------------------- /src/seed/models/bmanager.py: -------------------------------------------------------------------------------- 1 | from ._base import db, BaseModel 2 | 3 | __all__ = ["BManager", ] 4 | 5 | 6 | class BManager(BaseModel): 7 | """ 业务管理员模型 8 | """ 9 | bussiness_id = db.Column(db.Integer, db.ForeignKey('bussiness.id')) 10 | user_id = db.Column(db.Integer, nullable=False) 11 | -------------------------------------------------------------------------------- /src/seed/models/buser.py: -------------------------------------------------------------------------------- 1 | from ._base import db, BussinessModel 2 | __all__ = ["BUser", ] 3 | 4 | 5 | class BUser(BussinessModel): 6 | """ 业务用户权限模型 7 | """ 8 | user_id = db.Column(db.Integer, nullable=False) -------------------------------------------------------------------------------- /src/seed/models/buserrole.py: -------------------------------------------------------------------------------- 1 | from ._base import db, BussinessModel 2 | 3 | __all__ = ["BUserRole", ] 4 | 5 | 6 | class BUserRole(BussinessModel): 7 | """ 用户权限模型 8 | """ 9 | user_id = db.Column(db.Integer, nullable=False) 10 | role_id = db.Column(db.Integer, nullable=False) -------------------------------------------------------------------------------- /src/seed/models/bussiness.py: -------------------------------------------------------------------------------- 1 | from . import db, BaseModel 2 | 3 | __all__ = ['Bussiness'] 4 | 5 | 6 | class Bussiness(BaseModel): 7 | """ bussiness model class 8 | """ 9 | 10 | name = db.Column(db.Text, nullable=False) 11 | description = db.Column(db.Text) 12 | 13 | bmanager = db.relationship('BManager', cascade="all,delete", backref="bussiness") 14 | databases = db.relationship('Databases', cascade="all,delete", backref="bussiness") 15 | filters = db.relationship('Filters', cascade="all,delete", backref="bussiness") 16 | menu = db.relationship('Menu', cascade="all,delete", backref="bussiness") 17 | role = db.relationship('Role', cascade="all,delete", backref="bussiness") 18 | role_menu = db.relationship('RoleMenu', cascade="all,delete", backref="bussiness") -------------------------------------------------------------------------------- /src/seed/models/databases.py: -------------------------------------------------------------------------------- 1 | from . import db, BaseModel 2 | 3 | __all__ = ['Databases'] 4 | 5 | 6 | class Databases(BaseModel): 7 | """ databases configure 8 | """ 9 | bussiness_id = db.Column(db.Integer, db.ForeignKey('bussiness.id')) 10 | dtype = db.Column( 11 | db.Text, nullable=False, 12 | comment='Database type, like mysql, postgresql' 13 | ) 14 | name = db.Column( 15 | db.Text, nullable=False, 16 | comment='Database name' 17 | ) 18 | ip = db.Column( 19 | db.Text, nullable=False, 20 | comment='Database IP address' 21 | ) 22 | port = db.Column( 23 | db.Text, nullable=False, 24 | comment='Database port' 25 | ) 26 | user = db.Column( 27 | db.Text, nullable=False, 28 | comment='Database user login' 29 | ) 30 | password = db.Column( 31 | db.Text, nullable=False, 32 | comment='Database user password' 33 | ) 34 | -------------------------------------------------------------------------------- /src/seed/models/filters.py: -------------------------------------------------------------------------------- 1 | from seed.models._base import db, PageModel 2 | 3 | __all__ = ['Filters'] 4 | 5 | 6 | class Filters(PageModel): 7 | """ Filter model class 8 | """ 9 | belong_id = db.Column( 10 | db.Integer, 11 | comment='Filter belong which model' 12 | ) 13 | dtype = db.Column( 14 | db.Text, nullable=False, 15 | comment='Filter belong type, page or model' 16 | ) 17 | stype = db.Column( 18 | db.Text, nullable=False, 19 | default='single', comment='Single or mulitple choice' 20 | ) 21 | ename = db.Column( 22 | db.Text, nullable=False, 23 | comment='Field english name' 24 | ) 25 | cname = db.Column( 26 | db.Text, nullable=False, 27 | comment='Field chinese name' 28 | ) 29 | cascades = db.Column( 30 | db.Text, default=0, 31 | comment='The Filter field cascade id if it exists' 32 | ) 33 | db_source = db.Column( 34 | db.Integer, 35 | comment='The filter data query database id only for sql' 36 | ) 37 | condition_type = db.Column( 38 | db.Text, nullable=False, 39 | comment='The Filter condition setting type, manual or sql' 40 | ) 41 | conditions = db.Column( 42 | db.Text, nullable=False, 43 | comment='The Filter conditions, it will save as json string' 44 | ) 45 | sort = db.Column( 46 | db.Integer, default=0, 47 | comment='The filter sort' 48 | ) 49 | -------------------------------------------------------------------------------- /src/seed/models/menu.py: -------------------------------------------------------------------------------- 1 | from ._base import db, BussinessModel 2 | 3 | # from seed.models import Panels, Filters 4 | 5 | __all__ = ["Menu",] 6 | 7 | 8 | class Menu(BussinessModel): 9 | name = db.Column(db.Text, nullable=False) 10 | 11 | parent_id = db.Column(db.Integer, default=0) 12 | left_id = db.Column(db.Integer, default=0) 13 | 14 | panels = db.relationship('Panels', cascade="all,delete", backref="menu") 15 | filters = db.relationship('Filters', cascade="all,delete", backref="menu") -------------------------------------------------------------------------------- /src/seed/models/panels.py: -------------------------------------------------------------------------------- 1 | from seed.models._base import db, PageModel 2 | 3 | __all__ = ['Panels'] 4 | 5 | 6 | class Panels(PageModel): 7 | """ data panels model 8 | """ 9 | name = db.Column( 10 | db.Text, nullable=False, 11 | comment='Panel name' 12 | ) 13 | 14 | desc = db.Column( 15 | db.Text, nullable=False, 16 | comment='Panel description' 17 | ) 18 | 19 | x = db.Column( 20 | db.Float, nullable=False, 21 | comment='The panel x coordinate' 22 | ) 23 | 24 | y = db.Column( 25 | db.Float, nullable=False, 26 | comment='The panel y coordinate' 27 | ) 28 | 29 | w = db.Column( 30 | db.Float, nullable=False, 31 | comment='The panel width' 32 | ) 33 | 34 | h = db.Column( 35 | db.Float, nullable=False, 36 | comment='The panel height' 37 | ) 38 | 39 | db_source = db.Column( 40 | db.Integer, nullable=False, 41 | comment='Panel sql query database id' 42 | ) 43 | 44 | charttype = db.Column( 45 | db.Text, nullable=False, 46 | comment='Panel data chart type' 47 | ) 48 | 49 | sql = db.Column( 50 | db.Text, nullable=False, 51 | comment='Panel sql string' 52 | ) 53 | 54 | indexs = db.Column( 55 | db.Text, nullable=False, 56 | comment='Data index json configure' 57 | ) 58 | 59 | dimensions = db.Column( 60 | db.Text, nullable=False, 61 | comment='Data dimensions configure' 62 | ) 63 | 64 | sort = db.Column( 65 | db.Integer, nullable=False, 66 | comment='The panel sort' 67 | ) -------------------------------------------------------------------------------- /src/seed/models/role.py: -------------------------------------------------------------------------------- 1 | from . import db, BussinessModel 2 | 3 | __all__ = ['Role', ] 4 | 5 | 6 | class Role(BussinessModel): 7 | role = db.Column(db.Text, default='new') 8 | 9 | @classmethod 10 | def get_roles(cls, bussiness_id=None): 11 | query_session = cls.query 12 | if bussiness_id: 13 | query_session = query_session.filter_by(bussiness_id=bussiness_id) 14 | 15 | return query_session.all() 16 | -------------------------------------------------------------------------------- /src/seed/models/rolemenu.py: -------------------------------------------------------------------------------- 1 | from ._base import db, BussinessModel 2 | 3 | __all__ = ["RoleMenu",] 4 | 5 | 6 | class RoleMenu(BussinessModel): 7 | role_id = db.Column(db.Integer) 8 | menu_id = db.Column(db.Integer) 9 | 10 | role_permission = db.Column(db.Boolean, default=True) -------------------------------------------------------------------------------- /src/seed/runner/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import click 3 | 4 | from seed.utils.imports import import_string 5 | 6 | version_string = '0.1' 7 | 8 | 9 | @click.group() 10 | @click.option( 11 | '--config', 12 | default='', 13 | envvar='SEED_CONF', 14 | help='Path to config file', 15 | metavar='PATH' 16 | ) 17 | @click.version_option(version=version_string) 18 | @click.pass_context 19 | def cli(ctx, config): 20 | if config: 21 | os.environ['SEED_CONF'] = config 22 | else: 23 | os.environ['SEED_CONF'] = '~/.seed' 24 | 25 | 26 | cmds = [ 27 | 'seed.runner.commands.run.run', 28 | 'seed.runner.commands.init.init', 29 | 'seed.runner.commands.upgrade.upgrade', 30 | ] 31 | 32 | for cmd in cmds: 33 | cli.add_command(import_string(cmd)) 34 | 35 | 36 | def main(): 37 | cli() 38 | -------------------------------------------------------------------------------- /src/seed/runner/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/runner/commands/__init__.py -------------------------------------------------------------------------------- /src/seed/runner/commands/init.py: -------------------------------------------------------------------------------- 1 | import os 2 | import click 3 | 4 | from seed.runner.commands.run import Address 5 | 6 | 7 | @click.command() 8 | @click.option( 9 | '--dev', default=False, is_flag=True, help='Use settings more conducive to local development' 10 | ) 11 | @click.argument('directory', required=False) 12 | @click.pass_context 13 | def init(ctx, dev, directory): 14 | """ Initialize new configuration directory. 15 | """ 16 | from seed.runner.setting import discover_configs, generate_settings 17 | 18 | if directory: 19 | os.environ['SEED_CONF'] = directory 20 | 21 | directory, py, yaml = discover_configs() 22 | 23 | if directory and not os.path.exists(directory): 24 | os.makedirs(directory) 25 | 26 | py_contents, yaml_contents = generate_settings(dev) 27 | 28 | if os.path.isfile(yaml): 29 | click.confirm( 30 | "File already exists at '%s', overwrite?" % click.format_filename(yaml), 31 | abort=True 32 | ) 33 | 34 | with click.open_file(yaml, 'w') as fp: 35 | fp.write(yaml_contents) 36 | 37 | if os.path.isfile(py): 38 | click.confirm( 39 | "File already exists at '%s', overwrite?" % click.format_filename(py), 40 | abort=True 41 | ) 42 | 43 | with click.open_file(py, 'w') as fp: 44 | fp.write(py_contents) -------------------------------------------------------------------------------- /src/seed/runner/commands/run.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | 4 | class AddressParamType(click.ParamType): 5 | name = 'address' 6 | 7 | def __call__(self, value, param=None, ctx=None): 8 | if value is None: 9 | return (None, None) 10 | return self.convert(value, param, ctx) 11 | 12 | def convert(self, value, param, ctx): 13 | if ':' in value: 14 | host, port = value.split(':', 1) 15 | port = int(port) 16 | else: 17 | host = value 18 | port = None 19 | return host, port 20 | 21 | 22 | Address = AddressParamType() 23 | 24 | 25 | @click.group() 26 | def run(): 27 | "Run a service." 28 | 29 | @run.command() 30 | @click.option( 31 | '--debug', '-d', default=False, 32 | help='The debug option for web' 33 | ) 34 | def web(debug): 35 | from seed.services.app import application 36 | from seed.services.wsgi import SeedWSGIHttpServer 37 | 38 | if debug: 39 | application.run() 40 | else: 41 | SeedWSGIHttpServer(application).run() 42 | -------------------------------------------------------------------------------- /src/seed/runner/commands/upgrade.py: -------------------------------------------------------------------------------- 1 | import os 2 | import click 3 | 4 | 5 | @click.command() 6 | @click.option('--sql', is_flag=False, 7 | help=("Don't emit SQL to database - dump to standard " 8 | "output instead")) 9 | @click.pass_context 10 | def upgrade(ctx, sql=False): 11 | """ Upgrade database data and strcuct 12 | """ 13 | from seed.services.app import SeedHttpServer 14 | from seed.runner.setting import discover_configs 15 | 16 | _, config_file, _ = discover_configs() 17 | SeedHttpServer( 18 | config_file=config_file 19 | ).upgrade(sql) 20 | -------------------------------------------------------------------------------- /src/seed/runner/initializer.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | def initialize_app(config): 5 | """ 6 | initialize a application 7 | """ 8 | 9 | configure_logging() 10 | 11 | 12 | def configure_logging(): 13 | """ 14 | configure logging 15 | """ 16 | from seed.conf import server 17 | logging.config.dictConfig(server.LOGGING) -------------------------------------------------------------------------------- /src/seed/runner/setting.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | DEFAULT_SETTINGS_CONF = 'config.yaml' 5 | DEFAULT_SETTINGS_OVERRIDE = 'seed_conf.py' 6 | 7 | 8 | def discover_configs(): 9 | """ discover seed config files 10 | """ 11 | 12 | try: 13 | config = os.environ['SEED_CONF'] 14 | except KeyError: 15 | config = '~/.seed' 16 | 17 | config = os.path.expanduser(config) 18 | 19 | return ( 20 | config, os.path.join(config, DEFAULT_SETTINGS_OVERRIDE), 21 | os.path.join(config, DEFAULT_SETTINGS_CONF) 22 | ) 23 | 24 | 25 | def load_config_template(path, version='default'): 26 | from pkg_resources import resource_string 27 | return resource_string('seed', 'data/config/%s.%s' % (path, version)).decode('utf-8') 28 | 29 | 30 | def generate_settings(dev): 31 | """ generate default setting file contents 32 | """ 33 | 34 | context = { 35 | 'debug_flag': dev, 36 | } 37 | 38 | py = load_config_template(DEFAULT_SETTINGS_OVERRIDE, 'default') % context 39 | yaml = load_config_template(DEFAULT_SETTINGS_CONF, 'default') % context 40 | 41 | return py, yaml 42 | -------------------------------------------------------------------------------- /src/seed/schema/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/schema/__init__.py -------------------------------------------------------------------------------- /src/seed/schema/base.py: -------------------------------------------------------------------------------- 1 | from seed.models._base import session, ma 2 | 3 | 4 | class BaseSchema(ma.ModelSchema): 5 | 6 | class Meta: 7 | sqla_session = session 8 | include_fk = True 9 | -------------------------------------------------------------------------------- /src/seed/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/services/__init__.py -------------------------------------------------------------------------------- /src/seed/services/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import redis 5 | import flask_migrate 6 | from flask import Flask, g 7 | from flask_cors import CORS 8 | 9 | from seed.runner.setting import discover_configs 10 | 11 | from seed.models import db, migrate, session, ma 12 | from seed.api.urls import register_api 13 | from seed.utils.auth import SSOAuth, SessionAuth 14 | from seed.cache.user_bussiness import UserBussinessCache 15 | from seed.models.init import init_database_default_analogdata, init_database_default_datas, is_new_databases 16 | from seed.utils.helper import template_folder_path, static_folder_path 17 | 18 | 19 | class SeedHttpServer(object): 20 | def __init__( 21 | self, config_file, extra_options=None 22 | ): 23 | self.app = self.create_app(config_file) 24 | self.register_cors() 25 | 26 | self.register_databases() 27 | self.register_cache() 28 | self.register_api() 29 | self.register_hook() 30 | 31 | def create_app(self, config_file): 32 | app = Flask( 33 | __name__, 34 | static_url_path='/static', 35 | static_folder=static_folder_path, 36 | template_folder=template_folder_path 37 | ) 38 | 39 | app.config.from_pyfile(config_file) 40 | 41 | return app 42 | 43 | def register_databases(self): 44 | db.init_app(self.app) 45 | migrate.init_app(app=self.app, db=db) 46 | ma.init_app(self.app) 47 | 48 | with self.app.app_context(): 49 | session.configure(bind=db.engine) 50 | 51 | def register_cache(self): 52 | redis_pool = redis.ConnectionPool.from_url( 53 | self.app.config['REDIS_URL'], 54 | decode_responses=True, 55 | charset="utf-8", 56 | ) 57 | self.app.cache = redis.Redis(connection_pool=redis_pool) 58 | 59 | def register_api(self): 60 | register_api(self.app) 61 | 62 | from seed.api import front 63 | self.app.register_blueprint(front.bp, url_prefix='') 64 | 65 | def register_hook(self): 66 | """ 注册Hook 67 | """ 68 | @self.app.before_request 69 | def login_user(): 70 | 71 | if self.app.config['AUTH_TYPE'] == 'SSO': 72 | auth = SSOAuth() 73 | else: 74 | auth = SessionAuth() 75 | 76 | g.user = auth.get_current_user() 77 | 78 | # debugger 79 | g.user = auth.debbuger_user() 80 | 81 | if g.user: 82 | g.bussiness_id = UserBussinessCache().get(g.user.id) or -1 83 | else: 84 | g.bussiness_id = -1 85 | 86 | def register_cors(self): 87 | CORS(self.app, supports_credentials=True) 88 | 89 | def run(self): 90 | self.app.run(self.app.config['HOST'], self.app.config['PORT']) 91 | 92 | def upgrade(self, sql): 93 | print('数据库开始初始化') 94 | with self.app.app_context(): 95 | migrate_directory = self.app.extensions['migrate'].directory 96 | 97 | migrate_path, _, _ = discover_configs() 98 | migrate_path = os.path.join(migrate_path, migrate_directory) 99 | 100 | if not os.path.exists(os.path.join(migrate_path, 'alembic.ini')): 101 | # 判断是不是第一次初始化数据库 102 | is_empty = input('第一次初始化数据库,请确保你的数据库是空的。Y/N? ') 103 | if is_empty != 'Y': 104 | return 105 | 106 | flask_migrate.init(migrate_path) 107 | 108 | flask_migrate.migrate(migrate_path, sql=sql) 109 | flask_migrate.upgrade(migrate_path, sql=sql) 110 | 111 | if is_new_databases(): 112 | # 写入默认数据 113 | print('初始化模拟数据') 114 | init_database_default_analogdata() 115 | print('初始化默认业务模块') 116 | init_database_default_datas() 117 | 118 | print("数据库升级完成") 119 | 120 | 121 | def apply_application(): 122 | _, config_file, _ = discover_configs() 123 | http_server = SeedHttpServer( 124 | config_file=config_file 125 | ) 126 | return http_server.app 127 | 128 | 129 | application = apply_application() 130 | -------------------------------------------------------------------------------- /src/seed/services/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | DEFAULT_HOST = '127.0.0.1' 5 | DEFAULT_PORT = 5000 6 | 7 | 8 | def convert_options_to_uwsgi_env(options): 9 | for key, value in options.items(): 10 | key = 'UWSGI_' + key.upper().replace('-', '_') 11 | 12 | if isinstance(value, int): 13 | value = str(value) 14 | 15 | yield key, value 16 | 17 | 18 | class SeedWSGIHttpServer(object): 19 | def __init__(self, debug=False, workers=None): 20 | 21 | options = {} 22 | options.setdefault('module', 'seed.services.app:application') 23 | options.setdefault('protocol', 'http') 24 | options.setdefault('workers', 3) 25 | options.setdefault('threads', 4) 26 | options.setdefault('http-timeout', 60) 27 | options.setdefault('need-app', True) 28 | options.setdefault('virtualenv', sys.prefix) 29 | options.setdefault('die-on-term', True) 30 | options.setdefault( 31 | 'log-format', 32 | '%(addr) - %(user) [%(ltime)] "%(method) %(uri) %(proto)" %(status) %(size) "%(referer)" "%(uagent)"' 33 | ) 34 | 35 | from seed.services.app import application 36 | options.setdefault('%s-socket' % options['protocol'], '%s:%s' % ( 37 | application.config.get('HOST', DEFAULT_HOST), 38 | application.config.get('PORT', DEFAULT_PORT) 39 | )) 40 | 41 | options['master'] = True 42 | options['enable-threads'] = True 43 | 44 | self.options = options 45 | 46 | def prepare_environment(self, env=None): 47 | if not env: 48 | env = os.environ 49 | 50 | for key, value in convert_options_to_uwsgi_env(self.options): 51 | env.setdefault(key, value) 52 | 53 | virtualenv_path = os.path.dirname(os.path.abspath(sys.argv[0])) 54 | current_path = str(os.path.realpath(__file__)) 55 | if virtualenv_path not in current_path: 56 | env['PATH'] = '%s:%s' % (virtualenv_path, current_path) 57 | 58 | def run(self): 59 | self.prepare_environment() 60 | os.execvp('uwsgi', ('uwsgi', )) 61 | -------------------------------------------------------------------------------- /src/seed/static/OpenSans-Regular.629a55a7e793da068dc5.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/OpenSans-Regular.629a55a7e793da068dc5.ttf -------------------------------------------------------------------------------- /src/seed/static/assets/compConfig/compConfig.js: -------------------------------------------------------------------------------- 1 | const config_ = { 2 | "list": [ 3 | { 4 | "groupType": "compGroup", 5 | "groupTitle": "报表插件", 6 | "groupList": [ 7 | { 8 | "name": "折线图", 9 | "icon": "./../../assets/images/zxt.jpg", 10 | "type": "line" 11 | }, 12 | { 13 | "name": "对比图", 14 | "icon": "./../../assets/images/dbt.jpg", 15 | "type": "bar_cross" 16 | }, 17 | { 18 | "name": "饼状图", 19 | "icon": "./../../assets/images/bt.jpg", 20 | "type": "pie" 21 | }, 22 | { 23 | "name": "对比趋势图", 24 | "icon": "./../../assets/images/dbqst.jpg", 25 | "type": "linestack" 26 | }, 27 | { 28 | "name": "漏斗图", 29 | "icon": "./../../assets/images/ldt.jpg", 30 | "type": "funnel" 31 | }, 32 | { 33 | "name": "桑基图", 34 | "icon": "./../../assets/images/sankey.jpg", 35 | "type": "sankey" 36 | }, 37 | { 38 | "name": "地图", 39 | "icon": "./../../assets/images/map.jpg", 40 | "type": "map" 41 | }, 42 | { 43 | "name": "表格", 44 | "icon": "./../../assets/images/bgt.jpg", 45 | "type": "table" 46 | } 47 | ] 48 | }, 49 | { 50 | "groupType": "compGroup", 51 | "groupTitle": "全局过滤插件", 52 | "groupList": [ 53 | { 54 | "name": "单日期", 55 | "icon": "./../../assets/images/drq.jpg", 56 | "type": "flatpickr_single", 57 | "cascades": {} 58 | }, 59 | { 60 | "name": "双日期", 61 | "icon": "./../../assets/images/srq.jpg", 62 | "type": "flatpickr_range", 63 | "cascades": {} 64 | }, 65 | { 66 | "name": "下拉单选", 67 | "icon": "./../../assets/images/xldx.jpg", 68 | "type": "singleSelect", 69 | "list": [], 70 | "value": "", 71 | "ename":"", 72 | "cname":"", 73 | "db":"", 74 | "sql":"", 75 | "sourceType":"dict", 76 | "cascades": {} 77 | }, 78 | { 79 | "name": "下拉多选", 80 | "icon": "./../../assets/images/xlfx.jpg", 81 | "type": "multiSelect", 82 | "list": [], 83 | "value": "", 84 | "ename":"", 85 | "cname":"", 86 | "db":"", 87 | "sql":"", 88 | "sourceType":"dict", 89 | "cascades": {} 90 | } 91 | ] 92 | } 93 | ] 94 | } 95 | 96 | export {config_}; 97 | -------------------------------------------------------------------------------- /src/seed/static/assets/compConfig/reportConfig.js: -------------------------------------------------------------------------------- 1 | const config_ = { 2 | "line":{ 3 | "totalNum": 100, // 指标纬度个数总和 4 | "dimensionsNum": 100, // 维度的个数 5 | "indexsNum": 100, //指标的个数 6 | "dimensionsLimit":1, // 维度至少的个数 7 | "tips": "提示:该报表支持多指标多维度配置!请至少配置一个指标和一个维度~", 8 | "changeList":[] 9 | }, 10 | "bar":{ 11 | "direction": "cross", 12 | "totalNum": 100, // 指标纬度个数总和 13 | "dimensionsNum": 100, // 维度的个数 14 | "indexsNum": 100, //指标的个数 15 | "dimensionsLimit":1, // 维度至少的个数 16 | "tips": "提示:该报表支持多指标多维度配置!请至少配置一个指标和一个维度~", 17 | "changeList":[['bar', '对比图'], ['pie', '饼状图']] 18 | }, 19 | "pie":{ 20 | "totalNum": 100, // 指标纬度个数总和 21 | "dimensionsNum": 100, // 维度的个数 22 | "indexsNum": 100, //指标的个数 23 | "dimensionsLimit":1, // 维度至少的个数 24 | "tips": "提示:该报表支持多指标多维度配置!请至少配置一个指标和一个维度~", 25 | "changeList":[['pie', '饼状图'], ['bar', '对比图']] 26 | }, 27 | "linestack":{ 28 | "totalNum": 100, // 指标纬度个数总和 29 | "dimensionsNum": 100, // 维度的个数 30 | "indexsNum": 1, //指标的个数 31 | "dimensionsLimit":2, // 维度至少的个数 32 | "tips": "提示:该报表只支持单指标多维度配置!维度中必须有fdate这个维度", 33 | "changeList":[['linestack', '对比趋势图'],['bar', '对比图'], ['pie', '饼状图']], 34 | "limit": { 35 | "dimensions": ["fdate"] 36 | } 37 | }, 38 | "funnel":{ 39 | "totalNum": 100, // 指标纬度个数总和 40 | "dimensionsNum": 100, // 维度的个数 41 | "indexsNum": 100, //指标的个数 42 | "dimensionsLimit":1, // 维度至少的个数 43 | "tips": "提示:该报表支持多指标多维度配置!请至少配置一个指标和一个维度~" 44 | }, 45 | "sankey":{ 46 | "totalNum": 100, // 指标纬度个数总和 47 | "dimensionsNum": 2, // 维度的个数 48 | "indexsNum": 1, //指标的个数 49 | "dimensionsLimit": 2, // 维度至少的个数 50 | "tips": "提示:该报表支持配置多个指标和两个维度(source、target)~", 51 | "noTable": true 52 | }, 53 | "map":{ 54 | "totalNum": 100, // 指标纬度个数总和 55 | "dimensionsNum": 4, // 维度的个数 56 | "indexsNum": 1, //指标的个数 57 | "dimensionsLimit": 4, // 维度至少的个数 58 | "tips": "提示:该报表支持配置多个指标和四个维度:地区(region_name)、地区上级id(fpid)、纬度(lat)、经度(lng)~", 59 | "noTable": true 60 | }, 61 | "table":{ 62 | "totalNum": 100, // 指标纬度个数总和 63 | "dimensionsNum": 100, // 维度的个数 64 | "indexsNum": 100, //指标的个数 65 | "needIndexs": false, //是否必须有指标 66 | "dimensionsLimit":1, // 维度至少的个数 67 | "tips": "提示:该报表支持多指标多维度配置!" 68 | } 69 | } 70 | 71 | export {config_}; 72 | -------------------------------------------------------------------------------- /src/seed/static/assets/fonts/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/fonts/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /src/seed/static/assets/fonts/OpenSans-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/fonts/OpenSans-BoldItalic.ttf -------------------------------------------------------------------------------- /src/seed/static/assets/fonts/OpenSans-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/fonts/OpenSans-ExtraBold.ttf -------------------------------------------------------------------------------- /src/seed/static/assets/fonts/OpenSans-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/fonts/OpenSans-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /src/seed/static/assets/fonts/OpenSans-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/fonts/OpenSans-Italic.ttf -------------------------------------------------------------------------------- /src/seed/static/assets/fonts/OpenSans-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/fonts/OpenSans-Light.ttf -------------------------------------------------------------------------------- /src/seed/static/assets/fonts/OpenSans-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/fonts/OpenSans-LightItalic.ttf -------------------------------------------------------------------------------- /src/seed/static/assets/fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /src/seed/static/assets/fonts/OpenSans-Semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/fonts/OpenSans-Semibold.ttf -------------------------------------------------------------------------------- /src/seed/static/assets/fonts/OpenSans-SemiboldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/fonts/OpenSans-SemiboldItalic.ttf -------------------------------------------------------------------------------- /src/seed/static/assets/images/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/arrow.png -------------------------------------------------------------------------------- /src/seed/static/assets/images/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/avatar.jpg -------------------------------------------------------------------------------- /src/seed/static/assets/images/bgt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/bgt.jpg -------------------------------------------------------------------------------- /src/seed/static/assets/images/bt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/bt.jpg -------------------------------------------------------------------------------- /src/seed/static/assets/images/businessIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/businessIcon.png -------------------------------------------------------------------------------- /src/seed/static/assets/images/businessNo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/businessNo.png -------------------------------------------------------------------------------- /src/seed/static/assets/images/cloudPlane.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/cloudPlane.png -------------------------------------------------------------------------------- /src/seed/static/assets/images/dbqst.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/dbqst.jpg -------------------------------------------------------------------------------- /src/seed/static/assets/images/dbt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/dbt.jpg -------------------------------------------------------------------------------- /src/seed/static/assets/images/drq.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/drq.jpg -------------------------------------------------------------------------------- /src/seed/static/assets/images/giphy1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/giphy1.gif -------------------------------------------------------------------------------- /src/seed/static/assets/images/ldt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/ldt.jpg -------------------------------------------------------------------------------- /src/seed/static/assets/images/line.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/line.gif -------------------------------------------------------------------------------- /src/seed/static/assets/images/loading-bars.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/seed/static/assets/images/loading-spinning-bubbles.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/seed/static/assets/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/loading.gif -------------------------------------------------------------------------------- /src/seed/static/assets/images/loginIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/loginIcon.png -------------------------------------------------------------------------------- /src/seed/static/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/logo.png -------------------------------------------------------------------------------- /src/seed/static/assets/images/logo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/logo1.png -------------------------------------------------------------------------------- /src/seed/static/assets/images/mans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/mans.png -------------------------------------------------------------------------------- /src/seed/static/assets/images/map.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/map.jpg -------------------------------------------------------------------------------- /src/seed/static/assets/images/password-meter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/password-meter.png -------------------------------------------------------------------------------- /src/seed/static/assets/images/sankey.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/sankey.jpg -------------------------------------------------------------------------------- /src/seed/static/assets/images/srq.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/srq.jpg -------------------------------------------------------------------------------- /src/seed/static/assets/images/superman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/superman.png -------------------------------------------------------------------------------- /src/seed/static/assets/images/userFace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/userFace.png -------------------------------------------------------------------------------- /src/seed/static/assets/images/xldx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/xldx.jpg -------------------------------------------------------------------------------- /src/seed/static/assets/images/xlfx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/xlfx.jpg -------------------------------------------------------------------------------- /src/seed/static/assets/images/zxt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/assets/images/zxt.jpg -------------------------------------------------------------------------------- /src/seed/static/assets/js/bowser.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bowser - a browser detector 3 | * https://github.com/ded/bowser 4 | * MIT License | (c) Dustin Diaz 2015 5 | */ 6 | !function(e,t,n){typeof module!="undefined"&&module.exports?module.exports=n():typeof define=="function"&&define.amd?define(t,n):e[t]=n()}(this,"bowser",function(){function t(t){function n(e){var n=t.match(e);return n&&n.length>1&&n[1]||""}function r(e){var n=t.match(e);return n&&n.length>1&&n[2]||""}var i=n(/(ipod|iphone|ipad)/i).toLowerCase(),s=/like android/i.test(t),o=!s&&/android/i.test(t),u=/nexus\s*[0-6]\s*/i.test(t),a=!u&&/nexus\s*[0-9]+/i.test(t),f=/CrOS/.test(t),l=/silk/i.test(t),c=/sailfish/i.test(t),h=/tizen/i.test(t),p=/(web|hpw)os/i.test(t),d=/windows phone/i.test(t),v=/SamsungBrowser/i.test(t),m=!d&&/windows/i.test(t),g=!i&&!l&&/macintosh/i.test(t),y=!o&&!c&&!h&&!p&&/linux/i.test(t),b=n(/edge\/(\d+(\.\d+)?)/i),w=n(/version\/(\d+(\.\d+)?)/i),E=/tablet/i.test(t),S=!E&&/[^-]mobi/i.test(t),x=/xbox/i.test(t),T;/opera/i.test(t)?T={name:"Opera",opera:e,version:w||n(/(?:opera|opr|opios)[\s\/](\d+(\.\d+)?)/i)}:/opr|opios/i.test(t)?T={name:"Opera",opera:e,version:n(/(?:opr|opios)[\s\/](\d+(\.\d+)?)/i)||w}:/SamsungBrowser/i.test(t)?T={name:"Samsung Internet for Android",samsungBrowser:e,version:w||n(/(?:SamsungBrowser)[\s\/](\d+(\.\d+)?)/i)}:/coast/i.test(t)?T={name:"Opera Coast",coast:e,version:w||n(/(?:coast)[\s\/](\d+(\.\d+)?)/i)}:/yabrowser/i.test(t)?T={name:"Yandex Browser",yandexbrowser:e,version:w||n(/(?:yabrowser)[\s\/](\d+(\.\d+)?)/i)}:/ucbrowser/i.test(t)?T={name:"UC Browser",ucbrowser:e,version:n(/(?:ucbrowser)[\s\/](\d+(?:\.\d+)+)/i)}:/mxios/i.test(t)?T={name:"Maxthon",maxthon:e,version:n(/(?:mxios)[\s\/](\d+(?:\.\d+)+)/i)}:/epiphany/i.test(t)?T={name:"Epiphany",epiphany:e,version:n(/(?:epiphany)[\s\/](\d+(?:\.\d+)+)/i)}:/puffin/i.test(t)?T={name:"Puffin",puffin:e,version:n(/(?:puffin)[\s\/](\d+(?:\.\d+)?)/i)}:/sleipnir/i.test(t)?T={name:"Sleipnir",sleipnir:e,version:n(/(?:sleipnir)[\s\/](\d+(?:\.\d+)+)/i)}:/k-meleon/i.test(t)?T={name:"K-Meleon",kMeleon:e,version:n(/(?:k-meleon)[\s\/](\d+(?:\.\d+)+)/i)}:d?(T={name:"Windows Phone",windowsphone:e},b?(T.msedge=e,T.version=b):(T.msie=e,T.version=n(/iemobile\/(\d+(\.\d+)?)/i))):/msie|trident/i.test(t)?T={name:"Internet Explorer",msie:e,version:n(/(?:msie |rv:)(\d+(\.\d+)?)/i)}:f?T={name:"Chrome",chromeos:e,chromeBook:e,chrome:e,version:n(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i)}:/chrome.+? edge/i.test(t)?T={name:"Microsoft Edge",msedge:e,version:b}:/vivaldi/i.test(t)?T={name:"Vivaldi",vivaldi:e,version:n(/vivaldi\/(\d+(\.\d+)?)/i)||w}:c?T={name:"Sailfish",sailfish:e,version:n(/sailfish\s?browser\/(\d+(\.\d+)?)/i)}:/seamonkey\//i.test(t)?T={name:"SeaMonkey",seamonkey:e,version:n(/seamonkey\/(\d+(\.\d+)?)/i)}:/firefox|iceweasel|fxios/i.test(t)?(T={name:"Firefox",firefox:e,version:n(/(?:firefox|iceweasel|fxios)[ \/](\d+(\.\d+)?)/i)},/\((mobile|tablet);[^\)]*rv:[\d\.]+\)/i.test(t)&&(T.firefoxos=e)):l?T={name:"Amazon Silk",silk:e,version:n(/silk\/(\d+(\.\d+)?)/i)}:/phantom/i.test(t)?T={name:"PhantomJS",phantom:e,version:n(/phantomjs\/(\d+(\.\d+)?)/i)}:/slimerjs/i.test(t)?T={name:"SlimerJS",slimer:e,version:n(/slimerjs\/(\d+(\.\d+)?)/i)}:/blackberry|\bbb\d+/i.test(t)||/rim\stablet/i.test(t)?T={name:"BlackBerry",blackberry:e,version:w||n(/blackberry[\d]+\/(\d+(\.\d+)?)/i)}:p?(T={name:"WebOS",webos:e,version:w||n(/w(?:eb)?osbrowser\/(\d+(\.\d+)?)/i)},/touchpad\//i.test(t)&&(T.touchpad=e)):/bada/i.test(t)?T={name:"Bada",bada:e,version:n(/dolfin\/(\d+(\.\d+)?)/i)}:h?T={name:"Tizen",tizen:e,version:n(/(?:tizen\s?)?browser\/(\d+(\.\d+)?)/i)||w}:/qupzilla/i.test(t)?T={name:"QupZilla",qupzilla:e,version:n(/(?:qupzilla)[\s\/](\d+(?:\.\d+)+)/i)||w}:/chromium/i.test(t)?T={name:"Chromium",chromium:e,version:n(/(?:chromium)[\s\/](\d+(?:\.\d+)?)/i)||w}:/chrome|crios|crmo/i.test(t)?T={name:"Chrome",chrome:e,version:n(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i)}:o?T={name:"Android",version:w}:/safari|applewebkit/i.test(t)?(T={name:"Safari",safari:e},w&&(T.version=w)):i?(T={name:i=="iphone"?"iPhone":i=="ipad"?"iPad":"iPod"},w&&(T.version=w)):/googlebot/i.test(t)?T={name:"Googlebot",googlebot:e,version:n(/googlebot\/(\d+(\.\d+))/i)||w}:T={name:n(/^(.*)\/(.*) /),version:r(/^(.*)\/(.*) /)},!T.msedge&&/(apple)?webkit/i.test(t)?(/(apple)?webkit\/537\.36/i.test(t)?(T.name=T.name||"Blink",T.blink=e):(T.name=T.name||"Webkit",T.webkit=e),!T.version&&w&&(T.version=w)):!T.opera&&/gecko\//i.test(t)&&(T.name=T.name||"Gecko",T.gecko=e,T.version=T.version||n(/gecko\/(\d+(\.\d+)?)/i)),!T.windowsphone&&!T.msedge&&(o||T.silk)?T.android=e:!T.windowsphone&&!T.msedge&&i?(T[i]=e,T.ios=e):g?T.mac=e:x?T.xbox=e:m?T.windows=e:y&&(T.linux=e);var N="";T.windowsphone?N=n(/windows phone (?:os)?\s?(\d+(\.\d+)*)/i):i?(N=n(/os (\d+([_\s]\d+)*) like mac os x/i),N=N.replace(/[_\s]/g,".")):o?N=n(/android[ \/-](\d+(\.\d+)*)/i):T.webos?N=n(/(?:web|hpw)os\/(\d+(\.\d+)*)/i):T.blackberry?N=n(/rim\stablet\sos\s(\d+(\.\d+)*)/i):T.bada?N=n(/bada\/(\d+(\.\d+)*)/i):T.tizen&&(N=n(/tizen[\/\s](\d+(\.\d+)*)/i)),N&&(T.osversion=N);var C=N.split(".")[0];if(E||a||i=="ipad"||o&&(C==3||C>=4&&!S)||T.silk)T.tablet=e;else if(S||i=="iphone"||i=="ipod"||o||u||T.blackberry||T.webos||T.bada)T.mobile=e;return T.msedge||T.msie&&T.version>=10||T.yandexbrowser&&T.version>=15||T.vivaldi&&T.version>=1||T.chrome&&T.version>=20||T.samsungBrowser&&T.version>=4||T.firefox&&T.version>=20||T.safari&&T.version>=6||T.opera&&T.version>=10||T.ios&&T.osversion&&T.osversion.split(".")[0]>=6||T.blackberry&&T.version>=10.1||T.chromium&&T.version>=20?T.a=e:T.msie&&T.version<10||T.chrome&&T.version<20||T.firefox&&T.version<20||T.safari&&T.version<6||T.opera&&T.version<10||T.ios&&T.osversion&&T.osversion.split(".")[0]<6||T.chromium&&T.version<20?T.c=e:T.x=e,T}function r(e){return e.split(".").length}function i(e,t){var n=[],r;if(Array.prototype.map)return Array.prototype.map.call(e,t);for(r=0;r=0){if(n[0][t]>n[1][t])return 1;if(n[0][t]!==n[1][t])return-1;if(t===0)return 0}}function o(e,r,i){var o=n;typeof r=="string"&&(i=r,r=void 0),r===void 0&&(r=!1),i&&(o=t(i));var u=""+o.version;for(var a in e)if(e.hasOwnProperty(a)&&o[a]){if(typeof e[a]!="string")throw new Error("Browser version in the minVersion map should be a string: "+a+": "+String(e));return s([u,e[a]])<0}return r}function u(e,t,n){return!o(e,t,n)}var e=!0,n=t(typeof navigator!="undefined"?navigator.userAgent||"":"");return n.test=function(e){for(var t=0;tSEED自助报表
-------------------------------------------------------------------------------- /src/seed/static/inline.07bd3aeef3ad0ebf96d7.bundle.js: -------------------------------------------------------------------------------- 1 | !function(e){function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}var r=window.webpackJsonp;window.webpackJsonp=function(t,a,c){for(var u,i,f,l=0,s=[];l 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/seed/static/mans.1302759b8be148f12915.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/static/mans.1302759b8be148f12915.png -------------------------------------------------------------------------------- /src/seed/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/utils/__init__.py -------------------------------------------------------------------------------- /src/seed/utils/auth.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import functools 4 | from datetime import datetime 5 | 6 | import requests 7 | from flask import current_app, jsonify, g, request 8 | 9 | from seed.utils.response import response, HttpErrorCode 10 | from seed.models.account import Account 11 | from seed.models.bmanager import BManager 12 | from seed.cache import DefaultCache 13 | from seed.cache.session import SessionCache 14 | from seed.cache.user_bussiness import UserBussinessCache 15 | 16 | 17 | class APIRequireRole(object): 18 | roles = { 19 | "new": 0, 20 | "user": 1, 21 | "admin": 2, 22 | "super_admin": 3 23 | } 24 | 25 | def __init__(self, role): 26 | self.role = role 27 | 28 | def __call__(self, method): 29 | @functools.wraps(method) 30 | def wrapper(*args, **kwargs): 31 | if not g.user: 32 | # 未登录 33 | auth_type = current_app.config["AUTH_TYPE"] 34 | sso_url = current_app.config["SSO_URL"] 35 | login_url = request.host_url + "login" 36 | if sso_url and auth_type == "SSO": 37 | return jsonify(response(-14, data=sso_url)) 38 | else: 39 | return jsonify(response(HttpErrorCode.UNAUTHORIZED, data=login_url)) 40 | 41 | if g.user.status == -1: 42 | # 废弃账号 43 | return jsonify((response(HttpErrorCode.FORBIDDEN))) 44 | 45 | if not self.role or g.user.id == 1: 46 | return method(*args, **kwargs) 47 | 48 | if self.roles[g.user.role] < self.roles[self.role]: 49 | # 没有足够的权限 50 | return jsonify(response(HttpErrorCode.FORBIDDEN)) 51 | 52 | return method(*args, **kwargs) 53 | 54 | return wrapper 55 | 56 | 57 | api_require_login = APIRequireRole(None) 58 | api_require_new = APIRequireRole("new") 59 | api_require_user = APIRequireRole("user") 60 | api_require_admin = APIRequireRole("admin") 61 | api_require_super_admin = APIRequireRole("super_admin") 62 | 63 | 64 | class RequireRole(object): 65 | roles = { 66 | "new": 0, 67 | "user": 1, 68 | "admin": 2, 69 | "super_admin": 3 70 | } 71 | 72 | def __init__(self, role): 73 | self.role = role 74 | 75 | def __call__(self): 76 | if not g.user: 77 | # 未登录 78 | return False 79 | 80 | if g.user.status == -1: 81 | # 废弃账号 82 | return False 83 | 84 | if not self.role or g.user.id == 1: 85 | return True 86 | 87 | if self.roles[g.user.role] < self.roles[self.role]: 88 | # 没有足够的权限 89 | return False 90 | 91 | return True 92 | 93 | 94 | require_login = RequireRole(None) 95 | require_new = RequireRole("new") 96 | require_user = RequireRole("user") 97 | require_admin = RequireRole("admin") 98 | require_super_admin = RequireRole("super_admin") 99 | 100 | 101 | class BaseAuth(object): 102 | """ 验证基础类 103 | """ 104 | def get_current_user(self): 105 | """ 获取当前用户 106 | """ 107 | raise NotImplementedError("Not Implemented") 108 | 109 | def login_user(self, user): 110 | """ 登录用户 111 | """ 112 | raise NotImplementedError("Not Implemented") 113 | 114 | def logout_user(self): 115 | """ 登出用户 116 | """ 117 | raise NotImplementedError("Not Implemented") 118 | 119 | def debbuger_user(self): 120 | if not require_super_admin() or 'debugger' not in request.args: 121 | return g.user 122 | 123 | user = Account.query.filter_by(id=int(request.args['debugger'])).first() 124 | 125 | if user: 126 | bussiness_id = UserBussinessCache().get(user.id) or 1 127 | if self._is_bussiness_admin(user.id, bussiness=bussiness_id): 128 | user.role = 'admin' 129 | 130 | return user if user else None 131 | 132 | def _is_bussiness_admin(self, uid, bussiness=1): 133 | roles = BManager.query.filter_by(bussiness_id=bussiness, user_id=uid).all() 134 | return True if roles else False 135 | 136 | 137 | class SessionAuth(BaseAuth): 138 | """ Session登录权限类 139 | """ 140 | def get_current_user(self): 141 | session_token = request.cookies.get('session_token', '') 142 | user_id = SessionCache().get_user_id_by_token(session_token) 143 | if not user_id: 144 | return None 145 | 146 | user = Account.query.filter_by(id=user_id).first() 147 | if user: 148 | bussiness_id = UserBussinessCache().get(user.id) or 1 149 | # TODO 需要修复db.model自动保存的问题 150 | if self._is_bussiness_admin(user.id, bussiness=bussiness_id) and user.role != 'super_admin': 151 | user.role = 'admin' 152 | 153 | user.role = 'super_admin' if user.id == 1 else user.role 154 | 155 | return user 156 | 157 | 158 | class SSOAuth(BaseAuth): 159 | """ SSO权限校验登录类 160 | """ 161 | def get_current_user(self): 162 | # 获取当前用户信息在redis中存储的信息 163 | uid = request.cookies.get('admin_uid', None) 164 | uid_key = request.cookies.get('admin_key', None) 165 | 166 | if not (uid and uid_key): 167 | return None 168 | 169 | # 判断信息的有效性 170 | today = datetime.strftime(datetime.today(), '%Y-%m-%d') 171 | login_at = self._get_login_cache(uid) 172 | if today != login_at: 173 | # 去SSO中校验用户的有效性 174 | user_info = self._sso_verification(uid, uid_key) 175 | if not user_info: 176 | return None 177 | 178 | # 创建新用户 或者 获取用户的user_id 179 | user = self._get_user_id(user_info) 180 | 181 | self._account_valid(user) 182 | 183 | # 存储用户信息到Redis中 184 | self._set_login_cache(uid, today) 185 | else: 186 | user = Account.query.filter_by(sso_id=int(uid)).first() 187 | 188 | if user: 189 | bussiness_id = UserBussinessCache().get(user.id) or 1 190 | if self._is_bussiness_admin(user.id, bussiness=bussiness_id) and user.role != 'super_admin': 191 | user.role = 'admin' 192 | 193 | user.role = 'super_admin' if user.id == 1 else user.role 194 | 195 | return user 196 | 197 | def cache(self): 198 | return DefaultCache(current_app.cache) 199 | 200 | def _account_valid(self, user): 201 | if user.status == -1: 202 | raise Exception("账号已经被注销") 203 | 204 | def _get_user_id(self, user_info): 205 | account = Account.query.filter_by(sso_id=int(user_info['id'])).first() 206 | if not account: 207 | # 创建信息用户 208 | account = Account( 209 | sso_id=int(user_info['id']), 210 | account=user_info['username'], 211 | email=user_info['email'], 212 | avatar='http://oahead-static.17c.cn/oahead/' + str(int(user_info['code'])) + '.png', 213 | name=user_info['cname'], 214 | role='user', 215 | status=1 216 | ) 217 | else: 218 | account.login_at = datetime.utcnow() 219 | account.save() 220 | return account 221 | 222 | def _sso_verification(self, uid, uid_key): 223 | check_sso_user_url = "http://sso.ifere.com:8871/api?do=getInfo&uid=%s&key=%s&appid=1172" % (uid, uid_key) 224 | 225 | try: 226 | data_str = requests.get(check_sso_user_url, timeout=5) 227 | ret = json.loads(data_str.text) 228 | 229 | if "ret" not in ret or '1' != str(ret['ret']): 230 | logging.warning('user is invalid uid:%s uid_key:%s' % (uid, uid_key)) 231 | return [] 232 | 233 | return ret 234 | except Exception as e: 235 | 236 | logging.warning('error: %s' % e) 237 | return [] 238 | 239 | def _set_login_cache(self, uid, login_at): 240 | self.cache().set(':'.join([uid, 'sso_login']), login_at) 241 | 242 | def _get_login_cache(self, uid): 243 | return self.cache().get(':'.join([uid, 'sso_login'])) 244 | -------------------------------------------------------------------------------- /src/seed/utils/database.py: -------------------------------------------------------------------------------- 1 | from seed.drives import ALL_DRIVES 2 | 3 | 4 | def get_db_instance(dtype, ip, port, name, user, password, **kwargs): 5 | # TODO 自动收集当前支持的驱动 6 | db = ALL_DRIVES[dtype](ip, int(port), name, user, password) 7 | return db 8 | -------------------------------------------------------------------------------- /src/seed/utils/distutils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyaaDataCenter/seed/dcfff55b3c448b948b207e9266d8a2025359e6ea/src/seed/utils/distutils/__init__.py -------------------------------------------------------------------------------- /src/seed/utils/distutils/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os 4 | import sys 5 | 6 | from distutils import log 7 | from subprocess import check_output 8 | from distutils.core import Command 9 | 10 | 11 | class BaseBuildCommand(Command): 12 | user_options = [ 13 | ('work-path', 'w', 'The working directory for source files. Default is .') 14 | ] 15 | 16 | def initialize_options(self): 17 | self.work_path = '.' 18 | 19 | def finalize_options(self): 20 | pass 21 | 22 | def update_mainfests(self): 23 | pass 24 | 25 | def _setup_git(self): 26 | work_path = self.work_path 27 | if os.path.exists(os.path.join(work_path, '.git')): 28 | log.info("initializing git submodules") 29 | self._run_command(['git', 'submodule', 'init']) 30 | self._run_command(['git', 'submodule', 'update', '--remote']) 31 | 32 | def _setup_npm(self): 33 | node_version = [] 34 | 35 | log.info("setup node and npm.....") 36 | for app in ['node', 'npm']: 37 | try: 38 | log.info('testing %s version.....' % app) 39 | node_version.append(self._run_command([app, '--version']).rstrip()) 40 | except OSError as e: 41 | log.fatal( 42 | 'Cannot find {app} excutable. Please install {app}' 43 | 'and try again.'.format(app=app) 44 | ) 45 | sys.exit(1) 46 | 47 | if node_version[0] < b'v6.8.1': 48 | log.fatal('The node version need v6.8.1, the current node version is %s' % node_version[0]) 49 | sys.exit(1) 50 | 51 | log.info('using node ({0}) and npm ({1})'.format(*node_version)) 52 | self._run_command(['npm', 'install']) 53 | 54 | def _run_command(self, cmd, env=None): 55 | log.debug('running [%s]' % (' '.join(cmd), )) 56 | try: 57 | return check_output(cmd, cwd=self.work_path, env=env, shell=True) 58 | except Exception: 59 | log.error('command failed [%s] via [%s]' % (' '.join(cmd), self.work_path, )) 60 | raise 61 | 62 | def sub_commands(self): 63 | pass 64 | 65 | def run(self): 66 | self._setup_git() 67 | self._setup_npm() 68 | self._build() 69 | self.update_mainfests() 70 | -------------------------------------------------------------------------------- /src/seed/utils/distutils/build_assets.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os 4 | import sys 5 | import shutil 6 | import traceback 7 | 8 | from distutils import log 9 | 10 | from .base import BaseBuildCommand 11 | 12 | 13 | class BuildAssetsCommand(BaseBuildCommand): 14 | user_options = BaseBuildCommand.user_options + [ 15 | 16 | ] 17 | 18 | description = 'build static media assets' 19 | 20 | def initialize_options(self): 21 | BaseBuildCommand.initialize_options(self) 22 | 23 | def _build(self): 24 | try: 25 | self._build_static() 26 | except Exception: 27 | traceback.print_exc() 28 | log.fatal( 29 | 'unable to build Seed\'s static assets!\n' 30 | ) 31 | sys.exit(1) 32 | 33 | self._move_statics() 34 | 35 | def _build_static(self): 36 | os.chdir('./seed_static') 37 | 38 | log.info('Seed static start install node modules.') 39 | self._run_command(['npm', 'install']) 40 | 41 | log.info('Seed static start build static files.') 42 | self._run_command(['npm', 'run', 'buildProd']) 43 | 44 | log.info('Seed static start build finish.') 45 | os.chdir('..') 46 | 47 | def _move_statics(self): 48 | source = os.path.join(*'./seed_static/dist'.split('/')) 49 | target = os.path.join(*'./src/seed/static'.split('/')) 50 | 51 | files = os.listdir(target) 52 | for file in files: 53 | if file in ['.gitignore']: 54 | continue 55 | 56 | if os.path.isdir(os.path.join(target, file)): 57 | shutil.rmtree(os.path.join(target, file)) 58 | else: 59 | os.remove(os.path.join(target, file)) 60 | 61 | files = os.listdir(source) 62 | for file in files: 63 | shutil.move(os.path.join(source, file), os.path.join(target, file)) 64 | 65 | log.info('Move static files to seed finish.') 66 | -------------------------------------------------------------------------------- /src/seed/utils/file.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | from seed.utils.helper import local_file_path 5 | 6 | 7 | class LocalFile(object): 8 | def save(self, file): 9 | filename = '_'.join([str(int(time.time())), file.filename]) 10 | upload_path = os.path.join(local_file_path, filename) 11 | 12 | if not os.path.exists(local_file_path): 13 | os.makedirs(local_file_path) 14 | 15 | try: 16 | file.save(upload_path) 17 | except Exception as e: 18 | print(e) 19 | return None 20 | 21 | return os.path.join('files', filename) 22 | -------------------------------------------------------------------------------- /src/seed/utils/helper.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from pathlib import Path 4 | from inspect import getmembers 5 | 6 | from sqlalchemy.exc import InvalidRequestError 7 | 8 | 9 | def get_module_members(modules, predicate): 10 | 11 | members = getmembers(modules, predicate) 12 | return map(lambda m: m[1], members) 13 | 14 | 15 | def get_immediate_cls_attr(cls, attrname): 16 | if not issubclass(cls, object): 17 | return None 18 | 19 | for base in cls.__mro__: 20 | if attrname in base.__dict__ and base is cls: 21 | return getattr(base, attrname) 22 | 23 | return None 24 | 25 | 26 | def get_package_members(package, predicate, pre_url=''): 27 | from pkgutil import iter_modules 28 | from importlib import import_module 29 | 30 | members = {} 31 | 32 | for _, name, ispkg in iter_modules(package.__path__, package.__name__+'.'): 33 | if ispkg: 34 | sub_members = get_package_members( 35 | import_module(name), predicate, 36 | '/'.join([pre_url, name.split('.')[-1]]) 37 | ) 38 | members.update(sub_members) 39 | 40 | members.setdefault(pre_url, []).extend( 41 | get_module_members(import_module(name), predicate) 42 | ) 43 | 44 | return members 45 | 46 | 47 | def common_batch_crud(schema, model, datas): 48 | """ 批量做增改删操作 49 | """ 50 | # 删除多余的数据 51 | schema_instance = schema() 52 | delete_datas = [data for data in datas if data.get('status') == -1] 53 | delete_datas, errors = schema_instance.load(delete_datas, many=True) 54 | if errors: 55 | raise Exception(errors) 56 | 57 | for delete_data in delete_datas: 58 | try: 59 | delete_data.delete() 60 | except InvalidRequestError: 61 | pass 62 | 63 | # 新增和修改数据 64 | modify_datas = [data for data in datas if data.get('status') != -1] 65 | modify_datas, errors = schema_instance.load(modify_datas, many=True) 66 | if errors: 67 | raise Exception(errors) 68 | [modify_data.save() for modify_data in modify_datas] 69 | 70 | datas = schema(many=True, exclude=model.column_filter).dump(modify_datas) 71 | return datas 72 | 73 | 74 | template_folder_path = os.path.join( 75 | str(Path(os.path.dirname(os.path.realpath(__file__))).parent), 76 | 'static' 77 | ) 78 | 79 | root_path = str(Path(os.path.dirname(os.path.realpath(__file__))).parent) 80 | 81 | static_folder_path = os.path.join(template_folder_path, 'static') 82 | 83 | local_file_path = os.path.join(template_folder_path, 'files') 84 | -------------------------------------------------------------------------------- /src/seed/utils/imports.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class ModelProxyCache(dict): 4 | def __missing__(self, path): 5 | if '.' not in path: 6 | return __import__(path) 7 | 8 | module_name, class_name = path.rsplit('.', 1) 9 | 10 | module = __import__(module_name, {}, {}, [class_name]) 11 | handler = getattr(module, class_name) 12 | 13 | self[path] = handler 14 | return handler 15 | 16 | 17 | _cache = ModelProxyCache() 18 | 19 | 20 | def import_string(path): 21 | """ Path must be models.path.ClassName 22 | """ 23 | result = _cache[path] 24 | return result 25 | -------------------------------------------------------------------------------- /src/seed/utils/mail.py: -------------------------------------------------------------------------------- 1 | from flask import current_app 2 | 3 | from seed.drives.email import Email 4 | 5 | 6 | def send_active_email(to_mail, active_url, redirect_url): 7 | """ 发送激活邮件 8 | """ 9 | title = '新用户激活' 10 | mail_content = """ 11 | 亲爱的用户: 12 | 感谢你使用Seed自定义报表系统,请点击以下链接激活用户: 13 | {active_url}?redirect_url={redirect_url} 14 | 此链接24小时之后失效,请尽快激活邮箱。 15 | Seed团队 16 | """.format(active_url=active_url, redirect_url=redirect_url) 17 | Email(current_app.config).send_mail([to_mail], title, mail_content) 18 | 19 | 20 | def send_reset_password_email(to_mail, active_url, redirect_url): 21 | """ 发送重置密码邮件 22 | """ 23 | title = '密码重置' 24 | mail_content = """ 25 | 亲爱的用户: 26 | 你的密码重置连接如下: 27 | {active_url}?redirect_url={redirect_url} 28 | 此链接24小时之后失效,请尽快点击重置密码。 29 | Seed团队 30 | """.format(active_url=active_url, redirect_url=redirect_url) 31 | Email(current_app.config).send_mail([to_mail], title, mail_content) 32 | -------------------------------------------------------------------------------- /src/seed/utils/permissions.py: -------------------------------------------------------------------------------- 1 | from seed.models.bussiness import Bussiness 2 | from seed.models.bmanager import BManager 3 | from seed.models.buser import BUser 4 | from seed.models._base import session 5 | 6 | 7 | def get_permission_datas_by_user(user): 8 | all_datas = session.query(Bussiness).all() 9 | all_datas = [data.row2dict() for data in all_datas] 10 | 11 | if user.role == 'super_admin': 12 | # 所有权限 13 | for data in all_datas: 14 | data.update({'edit': True, 'delete': True}) 15 | permission_datas = all_datas 16 | un_permission_datas = [] 17 | 18 | else: 19 | # 管理的业务 20 | order_datas = session.query(Bussiness)\ 21 | .join(BManager, Bussiness.id == BManager.bussiness_id)\ 22 | .filter(BManager.user_id == user.id)\ 23 | .all() 24 | order_ids = [data.id for data in order_datas] 25 | order_datas = [data.row2dict() for data in order_datas] 26 | for data in order_datas: 27 | data.update({'edit': True, 'delete': False}) 28 | 29 | # 有权限的业务 30 | permission_datas = session.query(Bussiness)\ 31 | .join(BUser, Bussiness.id == BUser.bussiness_id)\ 32 | .filter(BUser.user_id == user.id)\ 33 | .all() 34 | permission_ids = [data.id for data in permission_datas] 35 | permission_datas = [data.row2dict() for data in permission_datas] 36 | for data in permission_datas: 37 | data.update({'edit': False, 'delete': False}) 38 | 39 | permission_datas = [ 40 | data for data in permission_datas if data['id'] not in order_ids 41 | ] 42 | permission_datas = order_datas + permission_datas 43 | permission_ids = [data['id'] for data in permission_datas] 44 | 45 | un_permission_datas = [ 46 | data for data in all_datas if data['id'] not in permission_ids 47 | ] 48 | return permission_datas, un_permission_datas 49 | 50 | 51 | def has_bussiness_permission(user, bussiness_id): 52 | permission_datas, _ = get_permission_datas_by_user(user) 53 | return any([bussiness_id == bussiness['id'] for bussiness in permission_datas]) 54 | -------------------------------------------------------------------------------- /src/seed/utils/response.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | 3 | 4 | class HttpErrorCode(object): 5 | SUCCESS = 200 6 | ERROR = 400 7 | UNAUTHORIZED = 401 8 | FORBIDDEN = 403 9 | NOT_FUND = 404 10 | AUTHORIZED_ERROR = 420 11 | PARAMS_VALID_ERROR = 421 12 | 13 | 14 | ERROR_DICT = { 15 | HttpErrorCode.SUCCESS: 'SUCCESS!', 16 | HttpErrorCode.UNAUTHORIZED: 'Unauthorized!', 17 | HttpErrorCode.PARAMS_VALID_ERROR: 'Params validation error!', 18 | HttpErrorCode.FORBIDDEN: 'No permission!', 19 | HttpErrorCode.NOT_FUND: 'Not found!', 20 | HttpErrorCode.AUTHORIZED_ERROR: 'Password is valid!', 21 | } 22 | 23 | 24 | def response(code, message=None, data={}): 25 | response_content = {'code': code, 'data': data} 26 | response_content['message'] = message if message else ERROR_DICT.get(code, 'Unknown error code!') 27 | return response_content 28 | 29 | 30 | def response_json(code, msg=None, data={}): 31 | return jsonify(response(code, msg, data)) 32 | -------------------------------------------------------------------------------- /src/seed/utils/time.py: -------------------------------------------------------------------------------- 1 | from dateutil import tz 2 | 3 | 4 | def convert_utc_to_local(utc_datetime, time_zone): 5 | zone = tz.gettz(time_zone) 6 | return utc_datetime.replace(tzinfo=zone) --------------------------------------------------------------------------------