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

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

一、 后端API接口开发

本地环境及工具准备:IDEA 、 JDK17、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.deveapp.cn

* @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进行访问即可。

以上。

文档更新时间: 2026-02 作者:LK