項(xiàng)目背景:傳統(tǒng)的企業(yè)內(nèi)網(wǎng)ip地址管理是采用電子文檔的形式,管理流程以手工為主,對(duì)ip地址和子網(wǎng)的使用情況無(wú)法進(jìn)行監(jiān)控和統(tǒng)計(jì),而且數(shù)據(jù)難以共享。隨著網(wǎng)絡(luò)變得越來(lái)越大,IP設(shè)備越來(lái)越多,手工IP地址管理將會(huì)成為網(wǎng)絡(luò)管理和擴(kuò)展的瓶頸。《IP地址資源管理系統(tǒng)》主要是針對(duì)傳統(tǒng)IP地址管理中存在的問(wèn)題提出的一個(gè)IP地址管理的解決方案,通過(guò)對(duì)IP地址資源從分配到回收的閉環(huán)管理,形成一個(gè)完善的、可以共享的、方便查詢統(tǒng)計(jì)的ip地址資源臺(tái)賬,以此提高管理的效率和精度。
一、項(xiàng)目開發(fā)環(huán)境
??本項(xiàng)目采用Python+Django開發(fā)。Python是時(shí)下大熱的一門開發(fā)語(yǔ)言,它的應(yīng)用領(lǐng)域非常廣泛,包括科學(xué)計(jì)算、數(shù)據(jù)分析、人工智能和web開發(fā)等。其中Django就是Python在web領(lǐng)域的一個(gè)強(qiáng)大的web框架,它的功能完善,要素齊全,自帶后臺(tái)管理和大量的工具及組件非常適合快速開發(fā)企業(yè)級(jí)應(yīng)用。在網(wǎng)絡(luò)應(yīng)用開發(fā)領(lǐng)域最著名的就是網(wǎng)絡(luò)爬蟲了,爬蟲爬出來(lái)的結(jié)構(gòu)化數(shù)據(jù)大多生成一個(gè)txt或csv文件,或者存儲(chǔ)在一個(gè)數(shù)據(jù)庫(kù)中,如果把這些內(nèi)容以web的形式呈現(xiàn)給讀者,或者開發(fā)一個(gè)后臺(tái)程序?qū)ζ溥M(jìn)行管理,哪最好的工具就是Django了,畢竟爬蟲和Django都是Python寫的,一個(gè)服務(wù)器上或者一種開發(fā)環(huán)境下兼容性完全不是問(wèn)題。
二、項(xiàng)目主要技術(shù)
1、權(quán)限控制
??在一個(gè)web應(yīng)用程序中,權(quán)限控制是必不可少的。本項(xiàng)目采用了基于角色的權(quán)限控制(RBAC)設(shè)計(jì),一個(gè)權(quán)限對(duì)應(yīng)多個(gè)角色,一個(gè)角色可以包含多個(gè)權(quán)限,一個(gè)用戶可以擁有多個(gè)角色,一個(gè)角色同樣也可以對(duì)應(yīng)多個(gè)用戶,權(quán)限和角色是多對(duì)多關(guān)系,角色和用戶是多對(duì)多關(guān)系,這種對(duì)應(yīng)關(guān)系是對(duì)現(xiàn)實(shí)的抽象,在數(shù)據(jù)庫(kù)中表現(xiàn)為五張表。實(shí)體關(guān)系模型如下,:
??web應(yīng)用權(quán)限的本質(zhì)就是url,實(shí)現(xiàn)權(quán)限控制就是實(shí)現(xiàn)對(duì)url的訪問(wèn)控制。該項(xiàng)目權(quán)限控制的工作原理分為四步:
1、GET請(qǐng)求,登錄頁(yè)面是否有訪問(wèn)權(quán)限;
2、POST請(qǐng)求,用戶提交用戶名和密碼,校驗(yàn)是否合法;
3、登錄成功后從數(shù)據(jù)庫(kù)中獲取當(dāng)前用戶的所有權(quán)限并放入session中進(jìn)行存儲(chǔ),由于web應(yīng)用基于http協(xié)議,它的請(qǐng)求應(yīng)答模式是無(wú)狀態(tài)的,就是每次請(qǐng)求都是獨(dú)立的,執(zhí)行情況和結(jié)果與前面和后面的請(qǐng)求無(wú)直接關(guān)系,所以每次發(fā)起請(qǐng)求,后臺(tái)程序都會(huì)到數(shù)據(jù)庫(kù)去查詢是否有權(quán)限,為避免對(duì)數(shù)據(jù)庫(kù)的頻繁操作,減輕數(shù)據(jù)庫(kù)的壓力,所以把權(quán)限存儲(chǔ)在session中。
4、當(dāng)用戶再次發(fā)起請(qǐng)求時(shí),在后臺(tái)編寫中間件對(duì)用戶當(dāng)前發(fā)起的url進(jìn)行權(quán)限判斷(在session中)
django處理流程圖如下:
import re
from django.conf import settings
from django.shortcuts import HttpResponse, redirect, render
from django.utils.deprecation import MiddlewareMixin
from django.urls import reverse
class RbacMiddleWare(MiddlewareMixin):
"""
權(quán)限校驗(yàn)中間件
"""
def process_request(self, request):
current_url = request.path_info
for valid_url in settings.VALID_URL_LIST:
regx = '^%s$' % valid_url
if re.match(regx, current_url):
return None
permission_dict = request.session.get(settings.MOBILEDJ_PERMISSION_SESSION_KEY)
if not permission_dict:
# return HttpResponse('未獲取到權(quán)限信息,請(qǐng)登錄')
return redirect(reverse('login'))
url_record = [{'title': '首頁(yè)', 'url': '#'}]
# 此處代碼進(jìn)行判斷
for url in settings.NO_PERMISSION_LIST:
regx = '^%s$' % url
if re.match(regx, request.path_info):
# 需要登錄,但無(wú)需權(quán)限校驗(yàn)
request.current_selected_permission = 0
request.breadcrumb = url_record
return None
flag = False
for item in permission_dict.values():
regx = '^%s$' % item['url']
if re.match(regx, current_url):
flag = True
if item['pid']:
url_record.extend([
{'title': item['p_title'], 'url': item['p_url']},
{'title': item['title'], 'url': item['url'], 'class': 'active'}
])
else:
url_record.extend([
{'title': item['title'], 'url': item['url'], 'class': 'active'},
])
request.breadcrumb = url_record
request.current_selected_permission = item['pid'] or item['id']
break
if not flag:
return render(request, 'web/404.html')
return None
??權(quán)限分配是對(duì)后臺(tái)數(shù)據(jù)庫(kù)中的數(shù)據(jù)進(jìn)行前端的呈現(xiàn)和操作,權(quán)限分配包括了單項(xiàng)權(quán)限分配和批量權(quán)限批操作,其中權(quán)限批量操作是一個(gè)集合的差集、并集和交集的運(yùn)算,待新建權(quán)限是程序中有而數(shù)據(jù)庫(kù)中沒(méi)有的權(quán)限,待刪除權(quán)限是程序中沒(méi)有而數(shù)據(jù)庫(kù)中有的權(quán)限,界面如下:
2、增刪改查組件
??在一個(gè)采用數(shù)據(jù)庫(kù)的管理系統(tǒng)中,開發(fā)人員大量的工作就是編寫數(shù)據(jù)庫(kù)表的增刪改查代碼。例如在Django項(xiàng)目中,開發(fā)人員首先用ORM創(chuàng)建模型并遷移至數(shù)據(jù)庫(kù),然后為每個(gè)操作(增刪改查)寫視圖函數(shù)和編寫靜態(tài)頁(yè)面模板,最后加入視圖函數(shù)的路由,項(xiàng)目中的每個(gè)模型都要重復(fù)以上幾個(gè)步驟。為了減少這種繁復(fù)的工作,項(xiàng)目設(shè)計(jì)了一個(gè)通用的增刪改查組件,該組件可以快速實(shí)現(xiàn)對(duì)數(shù)據(jù)庫(kù)表的增刪改查。
??通常Django項(xiàng)目在啟動(dòng)時(shí)會(huì)自動(dòng)注冊(cè)項(xiàng)目中的app,同時(shí)加載項(xiàng)目路由,如果在加載路由前能夠動(dòng)態(tài)生成app中的路由和視圖函數(shù),那么簡(jiǎn)化重復(fù)編碼的過(guò)程就可以迎刃而解,而且封裝后的代碼重用性提高,可以放在任何項(xiàng)目中使用。通過(guò)分析Django源碼發(fā)現(xiàn),Django中的autodiscover_modules模塊可以導(dǎo)入一個(gè)py文件,這個(gè)py文件會(huì)在路由加載前執(zhí)行(只要注冊(cè)的app中有stark.py文件,都會(huì)被執(zhí)行)。
from django.apps import AppConfig
from django.utils.module_loading import autodiscover_modules
class StarkConfig(AppConfig):
name = 'stark'
def ready(self):
autodiscover_modules('stark')
??接著利用單例模式,實(shí)現(xiàn)訪問(wèn)唯一對(duì)象的方式。在python中,如果導(dǎo)入的文件再次被重新導(dǎo)入,python不會(huì)再重新解釋一遍,而是選擇從內(nèi)存中直接將原來(lái)導(dǎo)入的值拿來(lái)使用,通過(guò)python的這種特性實(shí)現(xiàn)單例模式。
class StarkSite(object):
def __init__(self):
self._registry = []
self.app_name = 'stark'
self.namespace = 'stark'
def register(self, model_class, handler=None, prev=None):
if not handler:
handler = StarkHandler
self._registry.append(
{'model_class': model_class, 'handler': handler(model_class, prev, self), 'prev': prev})
def get_urls(self):
patterns = []
for item in self._registry:
model_class = item['model_class']
handler = item['handler']
prev = item['prev']
app_name, model_name = model_class._meta.app_label, model_class._meta.model_name
if prev:
patterns.extend([
re_path(r'^%s/%s/%s/' % (app_name, model_name, prev), (handler.get_urls(), None, None)),
])
else:
patterns.extend([
re_path(r'^%s/%s/' % (app_name, model_name), (handler.get_urls(), None, None)),
])
return patterns
@property
def urls(self):
return self.get_urls(), self.app_name, self.namespace
site = StarkSite()
??加載路由時(shí)導(dǎo)入from stark.service.v1 import site 實(shí)例
from django.contrib import admin
from django.urls import re_path, include
from stark.service.v1 import site
from web.views import account, userinfo
urlpatterns = [
re_path('admin/', admin.site.urls),
re_path(r'stark/', site.urls),
re_path(r'login/$', account.login, name='login'),
re_path(r'logout/$', account.logout, name='logout'),
re_path(r'current/userinfo/$', userinfo.current_userinfo_change, name='current_userinfo'),
re_path(r'^rbac/', include(('rbac.urls', 'rbac'), 'rbac'))
]
??注冊(cè)模型,通過(guò)單實(shí)例自動(dòng)生成路由和增刪改查的視圖函數(shù)
from stark.service.v1 import site
from web import models
from web.views.depart import DepartHandler
site.register(models.Depart, DepartHandler)
??下面是一個(gè)簡(jiǎn)化版的模型操作類,可以根據(jù)模型自動(dòng)生成路由和視圖函數(shù),可以自定義顯示字段,自定義查詢條件,自定義分頁(yè)
class StarkHandler(object):
list_template = None
add_template = None
change_template = None
delete_template = None
list_display = []
per_page = 10
def __init__(self, model_class, prev, site):
self.model_class = model_class
self.prev = prev
self.site = site
self.request = None
def list_view(self, request, *args, **kwargs):
"""
列表頁(yè)面
:param request:
:return:
"""
# ##################1.批量操作 ###############
action_list = self.get_action_list()
action_dict = {func.__name__: func.text for func in action_list}
if request.method == 'POST':
action_func_name = request.POST.get('action')
if action_func_name and action_func_name in action_dict:
func_response = getattr(self, action_func_name)(request, *args, **kwargs)
if func_response:
return func_response
# 獲取搜索條件
search_list = self.get_search_list()
"""
1.如果search_list沒(méi)有值則不顯示搜索框
2.獲取用戶提交的關(guān)鍵字
"""
search_value = request.GET.get('q', '')
from django.db.models import Q
"""
Q用于構(gòu)造復(fù)雜的ORM查詢條件
"""
conn = Q()
conn.connector = 'OR'
if search_value:
for item in search_list:
conn.children.append((item, search_value))
# 1.獲取排序
order_list = self.get_order_list()
search_group_condition = self.get_search_group_condition(request)
prev_queryset = self.get_queryset(request, *args, **kwargs)
queryset = prev_queryset.filter(conn).filter(**search_group_condition).order_by(*order_list)
# 處理分頁(yè)
all_count = queryset.count()
query_params = request.GET.copy()
query_params._mutable = True
pager = Pagination(
current_page=request.GET.get('page'),
all_count=all_count,
base_url=request.path_info,
query_params=query_params,
per_page=self.per_page
)
# 1.處理表頭
header_list = []
list_display = self.get_list_display(request, *args, **kwargs)
if list_display:
for key_or_func in list_display:
if isinstance(key_or_func, FunctionType):
header_list.append(key_or_func(self, None, True))
else:
verbose_name = self.model_class._meta.get_field(key_or_func).verbose_name
header_list.append(verbose_name)
else:
header_list.append(self.model_class._meta.model_name)
# 2.處理表格內(nèi)容
data_list = queryset[pager.start:pager.end]
body_list = []
for row in data_list:
tr_list = []
if list_display:
for key_or_func in list_display:
if isinstance(key_or_func, FunctionType):
tr_list.append(key_or_func(self, row, False, *args, **kwargs))
else:
tr_list.append('' if getattr(row, key_or_func) == None else getattr(row, key_or_func))
else:
tr_list.append(row)
body_list.append(tr_list)
# 按鈕添加
add_btn = self.get_add_btn(request, *args, **kwargs)
# 組合搜索
search_group = self.get_search_group(request)
search_group_list = []
for option_object in search_group:
search_group_list.append(option_object.get_queryset_or_tuple(request, self.model_class))
return render(
request,
self.list_template or 'stark/list.html',
{
'body_list': body_list,
'header_list': header_list,
'pager': pager,
'add_btn': add_btn,
'search_list': search_list,
'search_value': search_value,
'action_dict': action_dict,
'search_group_row_list': search_group_list
}
)
def add_view(self, request, *args, **kwargs):
"""
添加頁(yè)面
:return:
"""
model_form_class = self.get_model_class_form(True, request, None, *args, **kwargs)
if request.method == 'GET':
form = model_form_class()
return render(request, self.add_template or 'stark/change.html', {'form': form})
form = model_form_class(data=request.POST)
if form.is_valid():
response = self.save(request, form, False, *args, **kwargs)
return response or redirect(self.reverse_list_url(*args, **kwargs))
return render(request, self.add_template or 'stark/change.html', {'form': form})
def change_view(self, request, pk, *args, **kwargs):
"""
編輯頁(yè)面
:param request:
:param pk:
:return:
"""
model_form_class = self.get_model_class_form(False, request, pk, *args, **kwargs)
current_change_obj = self.get_change_object(request, pk, *args, **kwargs)
if not current_change_obj:
info = '數(shù)據(jù)不存在,請(qǐng)重新選擇!'
return render(request, 'stark/hint.html', {'msg': info})
if request.method == "GET":
form = model_form_class(instance=current_change_obj)
return render(request, self.change_template or 'stark/change.html', {'form': form})
form = model_form_class(instance=current_change_obj, data=request.POST)
if form.is_valid():
response = self.save(request, form, is_update=True, *args, **kwargs)
return response or redirect(self.reverse_list_url(*args, **kwargs))
return render(request, self.change_template or 'stark/change.html', {'form': form})
def delete_view(self, request, pk, *args, **kwargs):
"""
刪除頁(yè)面
:param request:
:return:
"""
origin_url = self.reverse_list_url(*args, **kwargs)
current_delete_obj = self.get_delete_object(request, pk, *args, **kwargs)
if not current_delete_obj:
info = '數(shù)據(jù)不存在,請(qǐng)重新選擇!'
return render(request, 'stark/hint.html', {'msg': info})
if request.method == 'GET':
return render(request, self.delete_template or 'stark/delete.html', {'cancel_url': origin_url})
current_delete_obj.delete()
return redirect(origin_url)
def get_urls(self):
"""
二次路由分發(fā)
:return:
"""
patterns = [
re_path(r'^list/$', self.wrapper(self.list_view), name=self.get_list_url_name),
re_path(r'^add/$', self.wrapper(self.add_view), name=self.get_add_url_name),
re_path(r'^change/(?P<pk>\d+)/$', self.wrapper(self.change_view),
name=self.get_change_url_name),
re_path(r'^delete/(?P<pk>\d+)/$', self.wrapper(self.delete_view),
name=self.get_delete_url_name)
]
patterns.extend(self.extra_url())
return patterns
def wrapper(self, func):
@functools.wraps(func)
def inner(request, *args, **kwargs):
self.request = request
return func(request, *args, **kwargs)
return inner
def save(self, request, form, is_update=False, *args, **kwargs):
"""
自定義保存函數(shù)
:param request:
:param form: 表單
:param is_update: 判斷是添加還是更新
:return:
"""
form.save()
3、業(yè)務(wù)開發(fā)
??權(quán)限控制組件和增刪改查組件開發(fā)完成后,業(yè)務(wù)開發(fā)就變得非常簡(jiǎn)單了,權(quán)限控制組件和增刪改查組件就好比大樓的地基,地基牢固了,上面建造的樓房才不會(huì)傾覆。
??業(yè)務(wù)流程,首先由系統(tǒng)管理員分配超網(wǎng)地址,比如一個(gè)B類地址,系統(tǒng)會(huì)根據(jù)B類地址的網(wǎng)絡(luò)號(hào)自動(dòng)生成所屬的C類子網(wǎng)和IP地址,然后網(wǎng)絡(luò)管理員對(duì)生成的C類子網(wǎng)進(jìn)行規(guī)劃,通常業(yè)務(wù)網(wǎng)絡(luò)的最大顆粒為C類,為節(jié)約網(wǎng)絡(luò)資源,還可以對(duì)C類子網(wǎng)按需求劃分為掩碼長(zhǎng)度為25為、26位、27位、28位、29位、30位不等的子網(wǎng),子網(wǎng)規(guī)劃完成后,接著分配規(guī)劃好的子網(wǎng),子網(wǎng)分配完成后,對(duì)應(yīng)二級(jí)單位的管理員可以在頁(yè)面中看到分配給自己部門的子網(wǎng)號(hào)和IP地址范圍,拿到IP地址范圍后,管理員就可以對(duì)相應(yīng)的IP地址的使用進(jìn)行維護(hù)和管理了。對(duì)于撤銷的單位同時(shí)回收子網(wǎng)號(hào),從而實(shí)現(xiàn)了對(duì)IP地址的規(guī)劃,分配,回收的整個(gè)生命周期的管理。
??業(yè)務(wù)系統(tǒng)主要分為網(wǎng)絡(luò)管理和主機(jī)管理兩個(gè)模塊:
??網(wǎng)絡(luò)管理包括子網(wǎng)規(guī)劃和子網(wǎng)分配,子網(wǎng)規(guī)劃支持自動(dòng)生成子網(wǎng)和手動(dòng)創(chuàng)建子網(wǎng),并且具備自動(dòng)校驗(yàn)子網(wǎng)功能,防止輸入錯(cuò)誤,子網(wǎng)分配功能支持豐富的關(guān)鍵字查詢和組合搜索,可以快速定位需要分配的子網(wǎng),同時(shí)記錄子網(wǎng)分配日志;
def action_multi_init_subnet(self, request, net_count, *args, **kwargs):
"""
根據(jù)子網(wǎng)掩碼長(zhǎng)度自動(dòng)生成子網(wǎng),同時(shí)更新主機(jī)表中的廣播地址和網(wǎng)絡(luò)地址以及IP所屬的子網(wǎng)號(hào)
:param request:
:param net_count: 子網(wǎng)掩碼長(zhǎng)度
:param args:
:param kwargs:
:return:
"""
pk_list = request.POST.getlist('pk')
for pk in pk_list:
ipv4_subnet_object = models.IpSubnet.objects.filter(id=pk).first()
if not ipv4_subnet_object:
continue
child_subnet_exists = models.IpSubnet.objects.filter(pid__isnull=False, pid=pk)
if child_subnet_exists:
continue
subnet = ipv4_subnet_object.subnet
ipv4_network_object = IPv4Network(subnet)
prefix_length = ipv4_network_object.prefixlen
if prefix_length > 24:
continue
ipv4_network_list = [item for item in list(ipv4_network_object.subnets(new_prefix=net_count))]
ipv4_object_list = []
ip_network_id = ipv4_subnet_object.ip_network_id
for item in ipv4_network_list:
ipv4_object_list.append(models.IpSubnet(ip_network_id=ip_network_id, pid_id=pk, subnet=str(item),
subnet_num=int(item.network_address)))
models.IpSubnet.objects.bulk_create(ipv4_object_list, batch_size=30)
ip_subnet_queryset = models.IpSubnet.objects.filter(subnet__in=ipv4_network_list)
for item in ip_subnet_queryset:
# network_address = str(IPv4Network(item).network_address)
# network_broadcast_address = str(IPv4Network(item).broadcast_address)
# 更新主機(jī)列表的網(wǎng)絡(luò)地址和廣播地址
network_address = ipv4_tools.get_network_address(item.subnet)
broadcast_address = ipv4_tools.get_broadcast_address(item.subnet)
models.Hosts.objects.filter(ip_address=network_address).update(ip_type=settings.NET_ADDR_IP_TYPE)
models.Hosts.objects.filter(ip_address=broadcast_address).update(
ip_type=settings.BROADCAST_ADDR_IP_TYPE)
# 更新主機(jī)列表的子網(wǎng)號(hào)
network_address_num = ipv4_tools.get_network_address(item.subnet, data_type='int')
broadcast_address_num = ipv4_tools.get_broadcast_address(item.subnet, data_type='int')
subnet_id = item.id
models.Hosts.objects.filter(ip_address_num__lte=broadcast_address_num,
ip_address_num__gte=network_address_num).update(ip_subnet_id=subnet_id)
??在主機(jī)管理中,系統(tǒng)根據(jù)劃分的子網(wǎng)自動(dòng)生成ip,管理人員可以對(duì)ip進(jìn)行分配,并記錄分配日志,實(shí)現(xiàn)IP地址的精確管理,同時(shí)為統(tǒng)計(jì)分析提供數(shù)據(jù)。
統(tǒng)計(jì)類,一個(gè)通用的統(tǒng)計(jì)類
class SubnetHostsAccount(object):
"""
統(tǒng)計(jì)類
"""
def __init__(self, subnet, *args, **kwargs):
self.subnet = subnet
self.args = args
self.account = kwargs
@property
def header(self):
header_list = [str(k) for k in self.account.keys()]
return header_list
@property
def content(self):
content_list = [v for k, v in self.account.items()]
return content_list
三、項(xiàng)目結(jié)語(yǔ)
??Python web框架有很多,比如Tornado和輕便的flask。但Django屬于重量級(jí)框架,一些輕量級(jí)的應(yīng)用不需要的功能模塊,Django也自帶了,比如用戶驗(yàn)證(Auth),管理后臺(tái)(Admin)和緩存管理(Cache)等功能,Django有完善的文檔,DJango有強(qiáng)大的數(shù)據(jù)庫(kù)訪問(wèn)組件(ORM,其訪問(wèn)數(shù)據(jù)庫(kù)的效率接近原生sql),使開發(fā)者無(wú)需學(xué)習(xí)sql語(yǔ)言一樣可以輕松操作數(shù)據(jù)庫(kù),Django這種基于MVC開發(fā)模式的傳統(tǒng)框架,非常適合開發(fā)基于PC的傳統(tǒng)網(wǎng)站,因?yàn)樗瑫r(shí)包括了后端(邏輯層,數(shù)據(jù)庫(kù)層)和前端的開發(fā)(如模板語(yǔ)言,樣式),基于PC的網(wǎng)站不會(huì)消失,不過(guò)其重要性會(huì)隨著移動(dòng)端的app和小程序的逐漸普及而降低。現(xiàn)代網(wǎng)絡(luò)應(yīng)用Web APP或者大型網(wǎng)站一般是一個(gè)后臺(tái),然后對(duì)應(yīng)各種客戶端(IOS,android,瀏覽器)。由于客戶端的開發(fā)語(yǔ)言與后臺(tái)的開發(fā)語(yǔ)言經(jīng)常不一樣,這時(shí)就需要后臺(tái)可以提供跨平臺(tái)跨語(yǔ)言的一種標(biāo)準(zhǔn)的資源或數(shù)據(jù)(如Json格式)供前后端溝通,這就是WEB API的作用了。Django本身開發(fā)不了符合REST規(guī)范的WEB API,不過(guò)借助Django-rest-framework(DRF)可以快速開發(fā)出優(yōu)秀的web api。
[項(xiàng)目源碼地址]https://gitee.com/mobiledj/mobiledj_ipm.git