Jeepay plus 是一套商用支付系统,同时也是一套成熟开发脚手架。基于Jeepay平台作为开发框架进行业务功能的二次开发,需要开发者掌握如下技能:

  • 后端: java开发语言,spring boot, spring security安全框架, mybatis plus
  • 前端: vue全家桶(vue3, vue-router, pinia), 项目是基于ant design vue 3进行的二次开发, 熟悉antdv的同学可快速上手。

一、 后端API接口开发

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】

应注意:

  1. 接口路径一般需以 [/api]开头,并进入springSecurity拦截器,可通用验证用户权限, 支持获取当前上下文的用户信息。 如无需走用户验证则将api定义为: /api/anon/** 规则即可全部放行。 配置详见【WebSecurityConfig】文件。

  2. 可选继承: com.jeequan.jeepay.mgr.ctrl.CommonCtrl extends AbstractController 其中包含了公共的基础函数; 比如,获取当前ip, 得到当前用户对象, 获取检索分页信息, 搜索条件, 排序字段等。

  3. @PreAuthorize 应在每个api函数上显式添加, 以防止越权操作。 权限标识即初始化的权限表的entId。 spring security框架注解表达式请参考: https://docs.spring.io/spring-security/site/docs/4.0.1.RELEASE/reference/htmlsingle/#el-common-built-in

  4. @MethodLog 应在重要的接口显式添加。 jeepay框架可自动记录当前用户在当前接口的操作详情。 对应菜单为:
    [ 系统管理 / 系统日志 ]

  5. QueryWrapper 为mybatis plus插件所支持条件构造器: 详见:https://mybatis.plus/guide/wrapper.html

  6. 切换主从库:
    在ctrl或者 service层增加注解:@DataSourceSwitch(DataSourceTypeEnum.SLAVE)
    实现方式详见:com.jeequan.jeepay.db.config.dynamic.DataSourceSpringConfig#dynamicDataSource

  7. 本地启动的参数配置:
    详见: 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>

注意:

  1. $access(‘权限ID’) 为该功能的权限对应的ID, 需与后端保持一致。 使用v-if 或者v-show进行显示/隐藏
    也可在函数中使用 this.$access() 进行权限的判断。

  2. 列表, 新增, 修改,删除 按实际功能进行开发即可。

  3. JeepayTable的rowKey参数尤为重要, 需要为该表格中的唯一字段。

  4. 所有项目的通用组件 在: src\components\JeepayUIComponents 目录下, 若需要修改请在manager中进行更改并且本地测试通过。 (agent/merchant项目执行npm run dev 或者 npm run build 都会将文件复制到工程中)详见:bin\init.js

  5. node >= v16.7.0

2.3 > 配置路由

页面编写完成需要在路由中进行定义,否则将无法正常访问。

打开: src/config/appConfig.ts 文件,
asyncRouteDefine 数组中 将【MyBizPage】 加入到路由定义中, 如下:

'MyBiz': { defaultPath: '/mybizs', component: () => import('@/views/mybiz/MyBiz.vue') } // 业务注释。。。

就完成了路由的配置工作, 按约定的路由URL进行访问即可。

以上。

文档更新时间: 2022-04-21 09:01   作者:大森林