Overview
jshERP is designed to be highly customizable without modifying core code. This guide covers various customization approaches from configuration changes to code extensions.
Always prefer configuration and plugin-based customization over modifying core code to ensure easy upgrades.
Configuration Customization
Application Properties
The main configuration file is application.properties:
# Server Configuration
server.port =9999
server.servlet.session.timeout =36000
server.servlet.context-path =/jshERP-boot
# Database Configuration
spring.datasource.url =jdbc:mysql://127.0.0.1:3306/jsh_erp? useUnicode =true& characterEncoding =utf8& useCursorFetch =true& defaultFetchSize =500& allowMultiQueries =true& rewriteBatchedStatements =true& useSSL =false
spring.datasource.driverClassName =com.mysql.cj.jdbc.Driver
spring.datasource.username =root
spring.datasource.password =123456
# MyBatis Configuration
mybatis-plus.mapper-locations =classpath:./mapper_xml/*.xml
# Redis Configuration
spring.redis.host =127.0.0.1
spring.redis.port =6379
spring.redis.password =1234abcd
# Tenant Configuration
manage.roleId =10
tenant.userNumLimit =1000000
tenant.tryDayLimit =3000
# Plugin Configuration
plugin.runMode =prod
plugin.pluginPath =plugins
plugin.pluginConfigFilePath =pluginConfig
# File Upload Configuration
file.uploadType =1
file.path =/opt/jshERP/upload
server.tomcat.basedir =/opt/tmp/tomcat
spring.servlet.multipart.max-file-size =10485760
spring.servlet.multipart.max-request-size =10485760
Common Customizations
Change Port and Context Path
server.port =8080
server.servlet.context-path =/erp
Access URL becomes: http://localhost:8080/erp/doc.html
spring.datasource.url =jdbc:mysql://db.example.com:3306/jsh_erp_prod
spring.datasource.username =erp_user
spring.datasource.password =secure_password
# Session timeout in seconds (default: 10 hours)
server.servlet.session.timeout =7200 # 2 hours
# Upload type: 1=Local, 2=OSS
file.uploadType =1
file.path =/var/jshERP/uploads
# Max file size (bytes)
spring.servlet.multipart.max-file-size =52428800 # 50MB
spring.servlet.multipart.max-request-size =52428800
# Maximum users per tenant
tenant.userNumLimit =100
# Trial period in days
tenant.tryDayLimit =30
Database Customization
Adding Custom Tables
Create custom tables following jshERP conventions:
CREATE TABLE ` jsh_custom_module ` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键' ,
`name` varchar ( 100 ) CHARACTER SET utf8 COLLATE utf8_general_ci COMMENT '名称' ,
`code` varchar ( 50 ) CHARACTER SET utf8 COLLATE utf8_general_ci COMMENT '编码' ,
`status` varchar ( 1 ) DEFAULT '1' COMMENT '状态' ,
`creator` bigint COMMENT '创建人' ,
`create_time` datetime COMMENT '创建时间' ,
`remark` varchar ( 500 ) COMMENT '备注' ,
`tenant_id` bigint COMMENT '租户id' ,
`delete_flag` varchar ( 1 ) DEFAULT '0' COMMENT '删除标记,0未删除,1删除' ,
PRIMARY KEY ( `id` ),
INDEX `tenant_id` ( `tenant_id` ),
INDEX `code` ( `code` )
) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT = '自定义模块' ;
Always include these standard fields :
id: bigint auto_increment primary key
tenant_id: For multi-tenancy
delete_flag: For soft delete pattern
Indexes on tenant_id and frequently queried fields
Generating Entity and Mapper
Use MyBatis Generator to create entity classes:
generatorConfig.xml
Generate Code
<? xml version = "1.0" encoding = "UTF-8" ?>
<! DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd" >
< generatorConfiguration >
< context id = "MySqlContext" targetRuntime = "MyBatis3" >
< jdbcConnection driverClass = "com.mysql.cj.jdbc.Driver"
connectionURL = "jdbc:mysql://localhost:3306/jsh_erp"
userId = "root"
password = "123456" />
< javaModelGenerator targetPackage = "com.jsh.erp.datasource.entities"
targetProject = "src/main/java" />
< sqlMapGenerator targetPackage = "mapper_xml"
targetProject = "src/main/resources" />
< javaClientGenerator type = "XMLMAPPER"
targetPackage = "com.jsh.erp.datasource.mappers"
targetProject = "src/main/java" />
< table tableName = "jsh_custom_module"
enableCountByExample = "true"
enableUpdateByExample = "true"
enableDeleteByExample = "true"
enableSelectByExample = "true"
selectByExampleQueryId = "true" />
</ context >
</ generatorConfiguration >
Backend Customization
Creating Custom Controller
CustomModuleController.java
package com.jsh.erp.controller;
import com.jsh.erp.datasource.entities.CustomModule;
import com.jsh.erp.service.CustomModuleService;
import com.jsh.erp.utils.BaseResponseInfo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation. * ;
import javax.annotation.Resource;
import java.util.List;
@ RestController
@ RequestMapping ( "/customModule" )
@ Api ( tags = { "自定义模块管理" })
public class CustomModuleController {
@ Resource
private CustomModuleService customModuleService ;
/**
* Get list with pagination
*/
@ GetMapping ( "/list" )
@ ApiOperation ( value = "获取列表" )
public BaseResponseInfo getList (
@ RequestParam ( "currentPage" ) Integer currentPage ,
@ RequestParam ( "pageSize" ) Integer pageSize ,
@ RequestParam ( value = "name" , required = false ) String name ) {
BaseResponseInfo res = new BaseResponseInfo ();
try {
List < CustomModule > list = customModuleService . getList (
currentPage, pageSize, name);
res . code = 200 ;
res . data = list;
} catch ( Exception e ) {
res . code = 500 ;
res . data = "获取数据失败" ;
}
return res;
}
/**
* Add new record
*/
@ PostMapping ( "/add" )
@ ApiOperation ( value = "新增" )
public BaseResponseInfo add (@ RequestBody CustomModule customModule ) {
BaseResponseInfo res = new BaseResponseInfo ();
try {
customModuleService . add (customModule);
res . code = 200 ;
res . data = "添加成功" ;
} catch ( Exception e ) {
res . code = 500 ;
res . data = "添加失败" ;
}
return res;
}
/**
* Update record
*/
@ PutMapping ( "/update" )
@ ApiOperation ( value = "更新" )
public BaseResponseInfo update (@ RequestBody CustomModule customModule ) {
BaseResponseInfo res = new BaseResponseInfo ();
try {
customModuleService . update (customModule);
res . code = 200 ;
res . data = "更新成功" ;
} catch ( Exception e ) {
res . code = 500 ;
res . data = "更新失败" ;
}
return res;
}
/**
* Delete (soft delete)
*/
@ DeleteMapping ( "/delete" )
@ ApiOperation ( value = "删除" )
public BaseResponseInfo delete (@ RequestParam ( "id" ) Long id ) {
BaseResponseInfo res = new BaseResponseInfo ();
try {
customModuleService . delete (id);
res . code = 200 ;
res . data = "删除成功" ;
} catch ( Exception e ) {
res . code = 500 ;
res . data = "删除失败" ;
}
return res;
}
}
Creating Custom Service
package com.jsh.erp.service;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.jsh.erp.datasource.entities.CustomModule;
import com.jsh.erp.datasource.entities.CustomModuleExample;
import com.jsh.erp.datasource.entities.User;
import com.jsh.erp.datasource.mappers.CustomModuleMapper;
import com.jsh.erp.utils.StringUtil;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
@ Service
public class CustomModuleService {
@ Resource
private CustomModuleMapper customModuleMapper ;
@ Resource
private UserService userService ;
/**
* Get paginated list
*/
public List < CustomModule > getList ( Integer currentPage , Integer pageSize , String name ) {
PageHelper . startPage (currentPage, pageSize);
CustomModuleExample example = new CustomModuleExample ();
CustomModuleExample . Criteria criteria = example . createCriteria ();
// Tenant isolation
User user = userService . getCurrentUser ();
criteria . andTenantIdEqualTo ( user . getTenantId ());
// Soft delete filter
criteria . andDeleteFlagEqualTo ( "0" );
// Name filter
if ( StringUtil . isNotEmpty (name)) {
criteria . andNameLike ( "%" + name + "%" );
}
List < CustomModule > list = customModuleMapper . selectByExample (example);
return list;
}
/**
* Add new record
*/
@ Transactional
public void add ( CustomModule customModule ) throws Exception {
User user = userService . getCurrentUser ();
customModule . setCreator ( user . getId ());
customModule . setCreateTime ( new Date ());
customModule . setTenantId ( user . getTenantId ());
customModule . setDeleteFlag ( "0" );
customModuleMapper . insert (customModule);
}
/**
* Update record
*/
@ Transactional
public void update ( CustomModule customModule ) throws Exception {
User user = userService . getCurrentUser ();
CustomModuleExample example = new CustomModuleExample ();
example . createCriteria ()
. andIdEqualTo ( customModule . getId ())
. andTenantIdEqualTo ( user . getTenantId ())
. andDeleteFlagEqualTo ( "0" );
customModuleMapper . updateByExampleSelective (customModule, example);
}
/**
* Delete (soft delete)
*/
@ Transactional
public void delete ( Long id ) throws Exception {
User user = userService . getCurrentUser ();
CustomModule record = new CustomModule ();
record . setDeleteFlag ( "1" );
CustomModuleExample example = new CustomModuleExample ();
example . createCriteria ()
. andIdEqualTo (id)
. andTenantIdEqualTo ( user . getTenantId ());
customModuleMapper . updateByExampleSelective (record, example);
}
}
Key Patterns :
Always filter by tenant_id for multi-tenancy
Use delete_flag = '0' for active records
Wrap database operations in @Transactional
Use PageHelper for pagination
Frontend Customization
Creating Custom Page
< template >
< div >
< a-card : bordered = " false " >
<!-- Search Form -->
< div class = "table-page-search-wrapper" >
< a-form layout = "inline" >
< a-row : gutter = " 24 " >
< a-col : md = " 6 " : sm = " 24 " >
< a-form-item label = "名称" >
< a-input v-model = " queryParam . name " placeholder = "请输入名称" />
</ a-form-item >
</ a-col >
< a-col : md = " 6 " : sm = " 24 " >
< span class = "table-page-search-submitButtons" >
< a-button type = "primary" @ click = " searchQuery " > 查询 </ a-button >
< a-button style = " margin-left : 8 px " @ click = " searchReset " > 重置 </ a-button >
</ span >
</ a-col >
</ a-row >
</ a-form >
</ div >
<!-- Toolbar -->
< div class = "table-operator" >
< a-button type = "primary" icon = "plus" @ click = " handleAdd " > 新增 </ a-button >
</ div >
<!-- Table -->
< a-table
ref = "table"
: columns = " columns "
: dataSource = " dataSource "
: pagination = " ipagination "
: loading = " loading "
@ change = " handleTableChange "
rowKey = "id"
>
< span slot = "action" slot-scope = "text, record" >
< a @ click = " handleEdit ( record ) " > 编辑 </ a >
< a-divider type = "vertical" />
< a-popconfirm title = "确定删除吗?" @ confirm = " () => handleDelete ( record . id ) " >
< a > 删除 </ a >
</ a-popconfirm >
</ span >
</ a-table >
</ a-card >
<!-- Add/Edit Modal -->
< custom-module-modal ref = "modalForm" @ ok = " modalFormOk " />
</ div >
</ template >
< script >
import { getList , deleteRecord } from '@/api/customModule'
import CustomModuleModal from './modules/CustomModuleModal'
export default {
name: 'CustomModuleList' ,
components: {
CustomModuleModal
} ,
data () {
return {
queryParam: {
name: ''
},
columns: [
{
title: '名称' ,
dataIndex: 'name' ,
key: 'name'
},
{
title: '编码' ,
dataIndex: 'code' ,
key: 'code'
},
{
title: '状态' ,
dataIndex: 'status' ,
key: 'status'
},
{
title: '操作' ,
dataIndex: 'action' ,
scopedSlots: { customRender: 'action' },
width: 150
}
],
dataSource: [],
ipagination: {
current: 1 ,
pageSize: 10 ,
total: 0
},
loading: false
}
} ,
created () {
this . loadData ()
} ,
methods: {
loadData () {
this . loading = true
const params = {
currentPage: this . ipagination . current ,
pageSize: this . ipagination . pageSize ,
name: this . queryParam . name
}
getList ( params ). then ( res => {
if ( res . code === 200 ) {
this . dataSource = res . data . rows
this . ipagination . total = res . data . total
}
this . loading = false
})
},
searchQuery () {
this . ipagination . current = 1
this . loadData ()
},
searchReset () {
this . queryParam = { name: '' }
this . loadData ()
},
handleAdd () {
this . $refs . modalForm . add ()
},
handleEdit ( record ) {
this . $refs . modalForm . edit ( record )
},
handleDelete ( id ) {
deleteRecord ({ id }). then ( res => {
if ( res . code === 200 ) {
this . $message . success ( '删除成功' )
this . loadData ()
} else {
this . $message . error ( '删除失败' )
}
})
},
handleTableChange ( pagination ) {
this . ipagination = pagination
this . loadData ()
},
modalFormOk () {
this . loadData ()
}
}
}
</ script >
import { axios } from '@/utils/request'
const api = {
list: '/customModule/list' ,
add: '/customModule/add' ,
update: '/customModule/update' ,
delete: '/customModule/delete'
}
export function getList ( params ) {
return axios ({
url: api . list ,
method: 'get' ,
params: params
})
}
export function addRecord ( data ) {
return axios ({
url: api . add ,
method: 'post' ,
data: data
})
}
export function updateRecord ( data ) {
return axios ({
url: api . update ,
method: 'put' ,
data: data
})
}
export function deleteRecord ( params ) {
return axios ({
url: api . delete ,
method: 'delete' ,
params: params
})
}
export const asyncRouterMap = [
{
path: '/custom' ,
name: 'custom' ,
component: TabLayout ,
meta: { title: '自定义模块' , icon: 'tool' },
children: [
{
path: '/custom/module' ,
name: 'CustomModule' ,
component : () => import ( '@/views/custom/CustomModuleList' ),
meta: { title: '模块管理' , keepAlive: true }
}
]
}
]
Customizing Existing Features
Extending Material Fields
Add custom fields to material management:
ALTER TABLE jsh_material_extend
ADD COLUMN `custom_field1` varchar ( 100 ) COMMENT '自定义字段1' ,
ADD COLUMN `custom_field2` decimal ( 24 , 6 ) COMMENT '自定义字段2' ;
Update the entity and mapper:
mvn mybatis-generator:generate
Modifying Business Logic
Override service methods by creating a custom service:
@ Service
@ Primary // Takes precedence over original service
public class CustomMaterialService extends MaterialService {
@ Override
public void addMaterial ( Material material ) throws Exception {
// Add custom validation
if ( material . getStock () < 0 ) {
throw new Exception ( "库存不能为负数" );
}
// Call parent method
super . addMaterial (material);
// Add custom post-processing
sendNotification (material);
}
private void sendNotification ( Material material ) {
// Custom notification logic
}
}
UI Customization
Custom Theme
src/assets/less/theme.less
// Override Ant Design variables
@ primary-color : #1890ff ; // Primary color
@ link-color : #1890ff ; // Link color
@ success-color : #52c41a ; // Success color
@ warning-color : #faad14 ; // Warning color
@ error-color : #f5222d ; // Error color
@ font-size-base : 14 px ; // Base font size
@ heading-color : rgba ( 0 , 0 , 0 , 0.85 ); // Heading color
@ text-color : rgba ( 0 , 0 , 0 , 0.65 ); // Body text color
@ border-radius-base : 4 px ; // Border radius
Custom Logo
Replace logo files:
# Replace logo in assets
cp my-logo.png src/assets/logo.png
# Update App.vue or layout component
Modify layout components:
src/components/layouts/BasicLayout.vue
< template >
< a-layout >
< a-layout-header >
<!-- Custom header -->
< div class = "custom-header" >
< img src = "@/assets/logo.png" alt = "Logo" />
< span class = "title" > 我的ERP系统 </ span >
</ div >
</ a-layout-header >
< a-layout-content >
< router-view />
</ a-layout-content >
< a-layout-footer >
<!-- Custom footer -->
< div class = "custom-footer" >
© 2024 My Company. All rights reserved.
</ div >
</ a-layout-footer >
</ a-layout >
</ template >
Adding Custom Reports
Backend Report API
CustomReportController.java
@ RestController
@ RequestMapping ( "/customReport" )
@ Api ( tags = { "自定义报表" })
public class CustomReportController {
@ Resource
private CustomReportService customReportService ;
@ GetMapping ( "/salesSummary" )
@ ApiOperation ( value = "销售汇总报表" )
public BaseResponseInfo getSalesSummary (
@ RequestParam ( "startDate" ) String startDate ,
@ RequestParam ( "endDate" ) String endDate ) {
BaseResponseInfo res = new BaseResponseInfo ();
try {
List < Map < String , Object >> data = customReportService
. getSalesSummary (startDate, endDate);
res . code = 200 ;
res . data = data;
} catch ( Exception e ) {
res . code = 500 ;
res . data = "获取报表失败" ;
}
return res;
}
@ GetMapping ( "/export" )
@ ApiOperation ( value = "导出报表" )
public void exportReport (
@ RequestParam ( "startDate" ) String startDate ,
@ RequestParam ( "endDate" ) String endDate ,
HttpServletResponse response ) throws Exception {
List < Map < String , Object >> data = customReportService
. getSalesSummary (startDate, endDate);
// Export to Excel
customReportService . exportToExcel (data, response);
}
}
Frontend Report Page
src/views/report/CustomReport.vue
< template >
< div >
< a-card >
< a-form layout = "inline" >
< a-form-item label = "开始日期" >
< a-date-picker v-model = " startDate " />
</ a-form-item >
< a-form-item label = "结束日期" >
< a-date-picker v-model = " endDate " />
</ a-form-item >
< a-form-item >
< a-button type = "primary" @ click = " loadData " > 查询 </ a-button >
< a-button @ click = " exportReport " > 导出 </ a-button >
</ a-form-item >
</ a-form >
< a-table : columns = " columns " : dataSource = " dataSource " />
</ a-card >
</ div >
</ template >
< script >
import { getSalesSummary } from '@/api/customReport'
export default {
data () {
return {
startDate: null ,
endDate: null ,
columns: [
{ title: '商品名称' , dataIndex: 'materialName' },
{ title: '销售数量' , dataIndex: 'quantity' },
{ title: '销售金额' , dataIndex: 'amount' }
],
dataSource: []
}
} ,
methods: {
loadData () {
const params = {
startDate: this . startDate ,
endDate: this . endDate
}
getSalesSummary ( params ). then ( res => {
if ( res . code === 200 ) {
this . dataSource = res . data
}
})
},
exportReport () {
window . open (
`/jshERP-boot/customReport/export?startDate= ${ this . startDate } &endDate= ${ this . endDate } `
)
}
}
}
</ script >
Integration with External Systems
REST API Integration
@ Service
public class ExternalIntegrationService {
private RestTemplate restTemplate = new RestTemplate ();
public void syncToExternalSystem ( DepotHead order ) {
String apiUrl = "https://external-api.com/orders" ;
HttpHeaders headers = new HttpHeaders ();
headers . setContentType ( MediaType . APPLICATION_JSON );
headers . set ( "Authorization" , "Bearer " + getApiToken ());
HttpEntity < DepotHead > request = new HttpEntity <>(order, headers);
ResponseEntity < String > response = restTemplate . postForEntity (
apiUrl, request, String . class );
if ( response . getStatusCode () != HttpStatus . OK ) {
throw new RuntimeException ( "同步失败: " + response . getBody ());
}
}
}
Message Queue Integration
@ Service
public class MessageQueueService {
@ Autowired
private RabbitTemplate rabbitTemplate ;
public void sendOrderMessage ( DepotHead order ) {
String message = JSONObject . toJSONString (order);
rabbitTemplate . convertAndSend ( "order.exchange" , "order.created" , message);
}
@ RabbitListener ( queues = "order.queue" )
public void handleOrderMessage ( String message ) {
DepotHead order = JSONObject . parseObject (message, DepotHead . class );
// Process order
}
}
Best Practices
Important Guidelines :
Never modify core files directly - Use inheritance, plugins, or configuration
Follow naming conventions - Use jsh_ prefix for tables, consistent package structure
Implement multi-tenancy - Always filter by tenant_id
Use soft delete - Never hard delete records
Add proper indexes - Include tenant_id and foreign keys
Handle exceptions - Use try-catch and return proper error messages
Write tests - Unit tests for services, integration tests for APIs
Document changes - Comment code and update documentation
Version Control for Customizations
Create a separate branch for customizations:
git checkout -b custom-features
git add .
git commit -m "Add custom module for XYZ"
git push origin custom-features
Upgrading with Customizations
mysqldump -u root -p jsh_erp > backup_before_upgrade.sql
git checkout main
git pull origin main
git checkout custom-features
git merge main
Review and resolve any merge conflicts
Test all custom features after merge