Jeepay plus 是一套商用支付系统,同时也是一套成熟开发脚手架。基于Jeepay平台作为开发框架进行业务功能的二次开发,需要开发者掌握如下技能:
- 后端: java开发语言,spring boot, spring security安全框架, mybatis plus
- 前端: vue全家桶(vue3, vue-router, pinia), 项目是基于ant design vue 3进行的二次开发, 熟悉antdv的同学可快速上手。
一、 后端API接口开发
本地环境及工具准备:IDEA 、 JDK8、Maven环境、 Mysql5.7 or 8 、 Redis 、 activeMQ
1.1 > 配置菜单:
表结构如下:
-- 权限表
DROP TABLE IF EXISTS `t_sys_entitlement`;
CREATE TABLE `t_sys_entitlement` (
`ent_id` VARCHAR(64) NOT NULL COMMENT '权限ID[ENT_功能模块_子模块_操作], eg: ENT_ROLE_LIST_ADD',
`ent_name` VARCHAR(32) NOT NULL COMMENT '权限名称',
`menu_icon` VARCHAR(32) COMMENT '菜单图标',
`menu_uri` VARCHAR(128) COMMENT '菜单uri/路由地址',
`component_name` VARCHAR(32) COMMENT '组件Name(前后端分离使用)',
`ent_type` CHAR(2) NOT NULL COMMENT '权限类型 ML-左侧显示菜单, MO-其他菜单, PB-页面/按钮',
`quick_jump` TINYINT(6) NOT NULL DEFAULT 0 COMMENT '快速开始菜单 0-否, 1-是',
`state` TINYINT(6) NOT NULL DEFAULT 1 COMMENT '状态 0-停用, 1-启用',
`pid` VARCHAR(32) NOT NULL COMMENT '父ID',
`ent_sort` INT(11) NOT NULL DEFAULT 0 COMMENT '排序字段, 规则:正序',
`sys_type` VARCHAR(10) NOT NULL COMMENT '所属系统: 参考:SYS_ROLE_TYPE',
`match_rule` VARCHAR(256) COMMENT '菜单匹配规则,具体规则匹配详见程序说明',
`created_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '创建时间',
`updated_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) COMMENT '更新时间',
PRIMARY KEY (`ent_id`, `sys_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统权限表';
请参考 init.sql
文件中的角色管理功能,按照下图对应的字段初始化, 注意上下级的关系。
-- 系统管理
insert into t_sys_entitlement values('ENT_SYS_CONFIG', '系统管理', 'setting', '', 'RouteView', 'ML', 0, 1, 'ROOT', '200', 'PLATFORM', null, now(), now());
insert into t_sys_entitlement values('ENT_UR', '用户角色管理', 'team', '', 'RouteView', 'ML', 0, 1, 'ENT_SYS_CONFIG', '10', 'PLATFORM', null, now(), now());
insert into t_sys_entitlement values('ENT_UR_USER', '操作员管理', 'contacts', '/users', 'SysUserPage', 'ML', 0, 1, 'ENT_UR', '10', 'PLATFORM', null, now(), now());
insert into t_sys_entitlement values('ENT_UR_USER_LIST', '页面:操作员列表', 'no-icon', '', '', 'PB', 0, 1, 'ENT_UR_USER', '0', 'PLATFORM', null, now(), now());
insert into t_sys_entitlement values('ENT_UR_USER_SEARCH', '按钮:搜索', 'no-icon', '', '', 'PB', 0, 1, 'ENT_UR_USER', '0', 'PLATFORM', null, now(), now());
insert into t_sys_entitlement values('ENT_UR_USER_ADD', '按钮:添加操作员', 'no-icon', '', '', 'PB', 0, 1, 'ENT_UR_USER', '0', 'PLATFORM', null, now(), now());
insert into t_sys_entitlement values('ENT_UR_USER_VIEW', '按钮: 详情', '', 'no-icon', '', 'PB', 0, 1, 'ENT_UR_USER', '0', 'PLATFORM', null, now(), now());
insert into t_sys_entitlement values('ENT_UR_USER_EDIT', '按钮: 修改基本信息', 'no-icon', '', '', 'PB', 0, 1, 'ENT_UR_USER', '0', 'PLATFORM', null, now(), now());
insert into t_sys_entitlement values('ENT_UR_USER_DELETE', '按钮: 删除操作员', 'no-icon', '', '', 'PB', 0, 1, 'ENT_UR_USER', '0', 'PLATFORM', null, now(), now());
insert into t_sys_entitlement values('ENT_UR_USER_UPD_ROLE', '按钮: 角色分配', 'no-icon', '', '', 'PB', 0, 1, 'ENT_UR_USER', '0', 'PLATFORM', null, now(), now());
注意:
- 字段【component_name】 为组件名称,需要提前与前端开发人员进行约定。 比如组件名称为:【MyBizPage】, 将MyBizPage初始化到菜单即可。 前端的组件后续章节会介绍。
- 页面、按钮级别的资源和其他菜单均不在左侧菜单中进行显示。 为了做权限的细粒度控制,也需要进行初始化操作。
1.2 > 新建model,mapper, service (如果需要):
1.2.1 > 将待创建的表结构DDL语句在数据库执行,然后将jeepay项目导入到IDEA或eclipse中。
1.2.2 > 打开jeepay-z-codegen项目下的: com.gen.MainGen文件 ,更改对应的 【数据库连接属性】,【要生成的表名】, 右键 RUN执行 即可生成对应的文件 如图。
1.2.3 > 将生成的文件放置到对应的目录:
【entity】: com.jeequan.jeepay.core.entity; (jeepay-components-db 项目)
【service】:com.jeequan.jeepay.service.impl; (jeepay-components-db 项目)
【mapper】:com.jeequan.jeepay.service.mapper; (jeepay-components-db 项目)
1.2.4 > 将文件复制好之后在jeepay-z-codegen项目下删除生成文件,否则该项目将报错(因为该项目仅作为代码生成器, 没有依赖开发环境,将提示找不到包)
1.3 > 编写业务API controller:
比如待开发业务为 myBiz, 在 com.jeequan.jeepay.mgr.ctrl 包下新建java文件: MyBizController.java
定义接口地址并与前端开发人员进行约定。
jeepay平台整体使用restful接口规范, 应尽量保持一致。
restful是什么? 可参考【http://www.ruanyifeng.com/blog/2014/05/restful_api.html】 这里不再赘述。
增删改查建议使用如下路径:
列表:
GET
/api/myBizs详情:
GET
/api/myBizs/{recordId}新增:
POST
/api/myBizs修改:
PUT
/api/myBizs/{recordId}删除:
DELETE
/api/myBizs/{recordId}
示例代码:
/**
* 操作日志信息 Ctrl
*
* @author terrfly
* @site https://www.jeequan.com
* @date 2022/4/7 11:27
*/
@RestController
@RequestMapping("api/sysLog")
public class SysLogController extends CommonCtrl {
@Autowired SysLogService sysLogService;
/** list, 查询从库 **/
@DataSourceSwitch(DynamicDataSource.DataSourceTypeEnum.SLAVE)
@PreAuthorize("hasAuthority('ENT_LOG_LIST')")
@RequestMapping(value="", method = RequestMethod.GET)
public ApiRes list() {
SysLog sysLog = getObject(SysLog.class);
LambdaQueryWrapper<SysLog> condition = SysLog.gw().orderByDesc(SysLog::getCreatedAt);
condition.eq(sysLog.getUserId() != null, SysLog::getUserId, sysLog.getUserId());
IPage<SysLog> pages = sysLogService.page(getIPage(), condition);
return ApiRes.page(pages);
}
/** 详情 **/
@PreAuthorize("hasAuthority('ENT_SYS_LOG_VIEW')")
@RequestMapping(value="/{sysLogId}", method = RequestMethod.GET)
public ApiRes detail(@PathVariable("sysLogId") String sysLogId) {
SysLog sysLog = sysLogService.getById(sysLogId);
return ApiRes.ok(sysLog);
}
/** 删除 **/
@PreAuthorize("hasAuthority('ENT_SYS_LOG_DEL')")
@MethodLog(remark = "删除日志信息")
@RequestMapping(value="/{selectedIds}", method = RequestMethod.DELETE)
public ApiRes delete(@PathVariable("selectedIds") String selectedIds) {
String[] ids = selectedIds.split(",");
List<Long> idsList = new LinkedList<>();
for (String id : ids) {
idsList.add(Long.valueOf(id));
}
boolean result = sysLogService.removeByIds(idsList);
return ApiRes.ok();
}
}
也可参考角色功能项:【com.jeequan.jeepay.mgr.ctrl.sysuser.SysRoleController】
应注意:
接口路径一般需以 [/api]开头,并进入springSecurity拦截器,可通用验证用户权限, 支持获取当前上下文的用户信息。 如无需走用户验证则将api定义为: /api/anon/** 规则即可全部放行。 配置详见【WebSecurityConfig】文件。
可选继承:
com.jeequan.jeepay.mgr.ctrl.CommonCtrl extends AbstractController
其中包含了公共的基础函数; 比如,获取当前ip, 得到当前用户对象, 获取检索分页信息, 搜索条件, 排序字段等。@PreAuthorize 应在每个api函数上显式添加, 以防止越权操作。 权限标识即初始化的权限表的entId。 spring security框架注解表达式请参考: https://docs.spring.io/spring-security/site/docs/4.0.1.RELEASE/reference/htmlsingle/#el-common-built-in
@MethodLog 应在重要的接口显式添加。 jeepay框架可自动记录当前用户在当前接口的操作详情。 对应菜单为:
[ 系统管理 / 系统日志 ]QueryWrapper 为
mybatis plus
插件所支持条件构造器: 详见:https://mybatis.plus/guide/wrapper.html切换主从库:
在ctrl或者 service层增加注解:@DataSourceSwitch(DataSourceTypeEnum.SLAVE)
实现方式详见:com.jeequan.jeepay.db.config.dynamic.DataSourceSpringConfig#dynamicDataSource本地启动的参数配置:
详见: config/application.txt的说明和 conf/readme.md 文档开发环境通用配置文件(每个项目通用配置) 使用方法: 1. 将此文件在当前文件夹下copy一份并重命名为:[ application.yml ]; 2. application.yml 作为项目启动的通用配置文件; 3. application.yml 建议加入到.gitignore忽略, 避免开发人员不经意的提交。 4. 若对通用配置进行变更,请修改application.txt文件并提交即可。
二、 前端页面开发:
前言: 前端为vue3 + antdv3.1.1 为基础进行的开发:
vue 单文件 script steup:https://v3.cn.vuejs.org/api/sfc-script-setup.html
antd 阿里巴巴官方文档:https://ant.design/
antdv3文档:https://www.antdv.com/components/table-cn
2.1 > 新建页面文件
与后端开发人员约定好组件名称: 比如上面提到的: 【MyBizPage】
在: /src/views/
目录下新建 mybiz/MyBizPage.vue
文件: 如图:
2.2 > 编写业务代码
参考代码:
<template>
<page-header-wrapper>
<a-card>
<JeepaySearchForm :searchFunc="searchFunc" :resetFunc="() => { vdata.searchData= {} }">
<jeepay-text-up v-model:value="vdata.searchData['isvNo']" :placeholder="'服务商号'" />
<jeepay-text-up v-model:value="vdata.searchData['isvName']" :placeholder="'服务商名称'" />
<a-form-item label="" class="table-search-item">
<a-select v-model:value="vdata.searchData['state']" placeholder="服务商状态">
<a-select-option value="">全部</a-select-option>
<a-select-option value="0">禁用</a-select-option>
<a-select-option value="1">启用</a-select-option>
</a-select>
</a-form-item>
</JeepaySearchForm>
<!-- 列表渲染 -->
<JeepayTable
ref="infoTable"
:initData="true"
:reqTableDataFunc="reqTableDataFunc"
:tableColumns="tableColumns"
:searchData="vdata.searchData"
rowKey="isvNo"
@btnLoadClose="vdata.btnLoading=false"
>
<template #topBtnSlot>
<a-button v-if="$access('ENT_ISV_INFO_ADD')" type="primary" @click="addFunc"><PlusOutlined /> 新建</a-button>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'isvName'"><b>{{ record.isvName }}</b> </template>
<template v-if="column.key === 'state'">
<a-badge :status="record.state === 0?'error':'processing'" :text="record.state === 0?'禁用':'启用'" />
</template>
<template v-if="column.key === 'op'">
<a-button v-if="$access('ENT_ISV_INFO_EDIT')" type="link" @click="editFunc(record.isvNo)">修改</a-button>
<a-button v-if="$access('ENT_ISV_PAY_CONFIG_LIST')" type="link" @click="showPayIfConfigList(record.isvNo)">支付配置</a-button>
<a-button v-if="$access('ENT_ISV_INFO_DEL')" type="link" style="color: red" @click="delFunc(record.isvNo)">删除</a-button>
</template>
</template>
</JeepayTable>
</a-card>
<!-- 新增页面组件 -->
<InfoAddOrEdit ref="infoAddOrEdit" :callbackFunc="searchFunc" />
<!-- 费率配置页面 -->
<JeepayPayConfigDrawer ref="jeepayPayConfigDrawerRef" configMode="mgrIsv" />
</page-header-wrapper>
</template>
<script setup lang="ts">
import { API_URL_ISV_LIST, req } from '@/api/manage'
import InfoAddOrEdit from './AddOrEdit.vue'
import { ref, reactive, getCurrentInstance } from 'vue'
// 导入全局函数
const { $infoBox, $access } = getCurrentInstance()!.appContext.config.globalProperties
// infoTable组件
const infoTable = ref()
const infoAddOrEdit = ref()
const jeepayPayConfigDrawerRef = ref()
// eslint-disable-next-line no-unused-vars
const tableColumns = ref([
{ key: 'isvName', width: 200, minWidth: 200, maxWidth: 200,title: '服务商名称', fixed: 'left' },
{ key: 'isvNo', title: '服务商号',width: 230,minWidth: 200,dataIndex: 'isvNo' },
{ key: 'state', title: '服务商状态',width: 230, minWidth: 200,},
{ key: 'createdAt', dataIndex: 'createdAt', title: '创建日期',width: 230, minWidth: 200, },
{ key: 'op', title: '操作', width: 260, minWidth: 260, maxWidth: 260, fixed: 'right', align: 'center' }
])
const vdata = reactive({
btnLoading: false,
searchData: {}
})
// 请求table接口数据
function reqTableDataFunc(params) {
return req.list(API_URL_ISV_LIST, params)
}
function delFunc (recordId) {
$infoBox.confirmDanger('确认删除?', '请确认该服务商下未分配商户', () => {
req.delById(API_URL_ISV_LIST, recordId).then(res => {
infoTable.value.refTable(false)
$infoBox.message.success('删除成功')
})
})
}
function searchFunc () { // 点击【查询】按钮点击事件
infoTable.value.refTable(true)
}
function addFunc () { // 业务通用【新增】 函数
infoAddOrEdit.value.show()
}
function editFunc (recordId) { // 业务通用【修改】 函数
infoAddOrEdit.value.show(recordId)
}
function showPayIfConfigList (recordId) { // 支付参数配置
jeepayPayConfigDrawerRef.value.show(recordId)
}
</script>
注意:
$access(‘权限ID’) 为该功能的权限对应的ID, 需与后端保持一致。 使用v-if 或者v-show进行显示/隐藏
也可在函数中使用 this.$access() 进行权限的判断。列表, 新增, 修改,删除 按实际功能进行开发即可。
JeepayTable的rowKey参数尤为重要, 需要为该表格中的唯一字段。
所有项目的通用组件 在: src\components\JeepayUIComponents 目录下, 若需要修改请在manager中进行更改并且本地测试通过。 (agent/merchant项目执行npm run dev 或者 npm run build 都会将文件复制到工程中)详见:bin\init.js
node >= v16.7.0
2.3 > 配置路由
页面编写完成需要在路由中进行定义,否则将无法正常访问。
打开: src/config/appConfig.ts
文件,
在 asyncRouteDefine 数组中 将【MyBizPage】 加入到路由定义中, 如下:
'MyBiz': { defaultPath: '/mybizs', component: () => import('@/views/mybiz/MyBiz.vue') } // 业务注释。。。
就完成了路由的配置工作, 按约定的路由URL进行访问即可。
以上。