瀏覽代碼

初始化

lyh 1 年之前
當前提交
8a1e5b5674

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
+  </component>
+</project>

+ 0 - 0
apps/__init__.py


+ 6 - 0
apps/account/__init__.py

@@ -0,0 +1,6 @@
+#coding=utf-8
+
+
+def tenant_log(user, type, description, data=None):
+    from apps.foundation.models import BizLog
+    BizLog.objects.addnew(user, type, description, data)

+ 116 - 0
apps/account/models.py

@@ -0,0 +1,116 @@
+#coding=utf-8
+
+from django.db import models
+from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, Group, PermissionsMixin
+from django.utils import timezone
+from django.conf import settings
+
+from utils.exceptions import CustomError
+from apps.foundation.consts import CONTENT_TYPE_SORTING, MENU_TO_MODEL
+
+from apps.foundation.models import BizLog
+
+class UserManager(BaseUserManager):
+    def sort_perms(self, perms):
+        def get_index(app_label, model):
+            try:
+                return CONTENT_TYPE_SORTING.index('{}-{}'.format(app_label, model))
+            except:
+                return 9999
+
+        perms = perms.order_by('content_type__model', 'id')
+        perms = sorted(perms, key=lambda n: get_index(n.content_type.app_label, n.content_type.model))
+        return perms
+
+    def get_menuname_of_contenttype(self, app_label, model):
+        for menu in MENU_TO_MODEL:
+            val = '{}-{}'.format(app_label, model)
+            if val in menu[1]:
+                return menu[0]
+        return u'未分类'
+
+    def save_group(self, id, name, permissions, user):
+        name = name.strip(u' ')
+        #old_permissions = None
+        if id == None or id == '':
+            is_exist = Group.objects.filter(name=name).first()
+            if is_exist:
+                raise CustomError(u'名称为[%s]的权限组已存在' % name)
+            group = Group.objects.create(name=name)
+            BizLog.objects.addnew(user, BizLog.INSERT, u"添加权限组[%s],id=%d" % (group.name, group.id))
+        else:
+            is_exist = Group.objects.filter(name=name).exclude(pk=id).first()
+            if is_exist:
+                raise CustomError(u'名称为[%s]的权限组已存在' % name)
+            group = Group.objects.filter(pk=id).first()
+            if not group:
+                raise CustomError(u'未找到相应的权限组')
+            group.name = name
+            group.save()
+        #    old_permissions = [p.id for p in group.permissions.all()]
+            BizLog.objects.addnew(user, BizLog.UPDATE, u"修改权限组[%s],id=%d" % (group.name, group.id))
+        group.permissions = permissions
+
+    def _create_user(self, username,  password, **extra_fields):
+        """
+        Creates and saves a User with the given username, email and password.
+        """
+        if not username:
+            raise ValueError('The given username must be set')
+        username = self.model.normalize_username(username)
+        user = self.model(username=username, **extra_fields)
+        user.set_password(password)
+        user.save(using=self._db)
+        return user
+
+    def create_superuser(self, username,  password, **extra_fields):
+        extra_fields.setdefault('is_superuser', True)
+
+        if extra_fields.get('is_superuser') is not True:
+            raise ValueError('Superuser must have is_superuser=True.')
+
+        return self._create_user(username,  password, **extra_fields)
+
+class User(AbstractBaseUser, PermissionsMixin):
+    DIMISSION = 0
+    INSERVICE = 1
+    STATUS_CHOICES = (
+        (DIMISSION, u'离职'),
+        (INSERVICE, u'在职'),
+    )
+    name = models.CharField(max_length=20, verbose_name=u"姓名")
+    username = models.CharField(max_length=30, verbose_name=u'账号', unique=True, db_index=True,error_messages={'unique': u'已存在'})
+    status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES, verbose_name=u"是否在职", default=INSERVICE)
+    tel = models.CharField(max_length=15, verbose_name=u"手机号码", null=True, blank=True)
+    address = models.CharField(max_length=500, verbose_name=u"家庭住址", null=True, blank=True)
+    title = models.CharField(max_length=20, verbose_name=u"工作岗位", null=True, blank=True)
+    date_joined = models.DateTimeField(verbose_name=u'注册时间', default=timezone.now, null=True)
+
+    objects = UserManager()
+
+    USERNAME_FIELD = 'username'
+    REQUIRED_FIELDS = []
+
+    @staticmethod
+    def getById(id):
+        try:
+            id = int(id)
+        except:
+            raise CustomError(u'无效的员工ID')
+
+        instance = User.objects.filter(pk=id).first()
+        if not instance:
+            raise CustomError(u'未找到相应的员工')
+        return instance
+
+
+    def __str__(self):
+        return self.name
+
+    def __unicode__(self):
+        return self.name
+    class Meta:
+        db_table = "auth_user"
+        verbose_name = u"人员管理"
+        ordering = ('-id',)
+        default_permissions = ()

+ 49 - 0
apps/account/serializers.py

@@ -0,0 +1,49 @@
+# coding=utf-8
+
+from django.contrib.auth import get_user_model, authenticate
+
+from rest_framework import serializers
+from rest_framework_jwt.serializers import JSONWebTokenSerializer
+from rest_framework_jwt.settings import api_settings
+from apps.foundation.models import BizLog
+from apps.account import tenant_log
+from utils import get_remote_addr
+
+User = get_user_model()
+jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
+jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
+
+class TenantJWTSerializer(JSONWebTokenSerializer):
+    def validate(self, attrs):
+        credentials = {
+            self.username_field: attrs.get(self.username_field),
+            'password': attrs.get('password')
+        }
+
+        if all(credentials.values()):
+            user = authenticate(**credentials)
+
+            if user:
+                if user.status != User.INSERVICE:
+                    msg = u'禁用帐户,禁止登录'
+                    tenant_log(user, BizLog.INSERT,u'禁用帐户[%s]尝试登录系统,IP[%s]' % (user.username, get_remote_addr(self.request)))
+                    raise serializers.ValidationError(msg)
+
+                permissions = list(user.get_all_permissions())
+
+                payload = jwt_payload_handler(user)
+                tenant_log(user, BizLog.INSERT, u'[%s]登录系统,IP[%s]' % (user.username,get_remote_addr(self.request)))
+
+                return {
+                    'token': jwt_encode_handler(payload),
+                    'user_id': user.id,
+                    'username': user.username,
+                    'permissions': permissions
+                }
+            else:
+                msg = u'账号或者密码错误!'
+                raise serializers.ValidationError(msg)
+        else:
+            msg = u'必须包含“{username field}”和“password.'
+            msg = msg.format(username_field=self.username_field)
+            raise serializers.ValidationError(msg)

+ 12 - 0
apps/account/urls.py

@@ -0,0 +1,12 @@
+# coding=utf-8
+
+from django.conf.urls import url, include
+from rest_framework.routers import SimpleRouter
+
+from .views import *
+
+urlpatterns = [
+     url(r'^login/$', LoginView.as_view()),
+     url(r'^token_refresh/$', RefreshTokenView.as_view()),
+     url(r'^token_verify/$', VerifyTokenView.as_view()),
+]

+ 39 - 0
apps/account/views.py

@@ -0,0 +1,39 @@
+# coding=utf-8
+
+from rest_framework_jwt.views import ObtainJSONWebToken,VerifyJSONWebToken,RefreshJSONWebToken
+from rest_framework.serializers import ValidationError
+
+from utils import response_error, response_ok
+from .serializers import TenantJWTSerializer
+
+class LoginView(ObtainJSONWebToken):
+    serializer_class = TenantJWTSerializer
+
+    def post(self, request, *args, **kwargs):
+        try:
+            ser = self.serializer_class(data=request.data)
+            ser.request = request
+            if ser.is_valid(raise_exception=True):
+                return response_ok(ser.validated_data)
+        except ValidationError as e:
+            return response_error(e.detail['error'][0])
+
+
+class VerifyTokenView(VerifyJSONWebToken):
+    def post(self, request, *args, **kwargs):
+        try:
+            ser = self.serializer_class(data=request.data)
+            if ser.is_valid(raise_exception=True):
+                return response_ok({'token': ser.validated_data['token']})
+        except ValidationError as e:
+            return response_error(u'登录状态失效,请重新登录[' + e.detail['error'][0] + ']')
+
+
+class RefreshTokenView(RefreshJSONWebToken):
+    def post(self, request, *args, **kwargs):
+        try:
+            ser = self.serializer_class(data=request.data)
+            if ser.is_valid(raise_exception=True):
+                return response_ok({'token': ser.validated_data['token']})
+        except ValidationError as e:
+            return response_error(u'登录状态失效,请重新登录[' + e.detail['error'][0] + ']')

+ 0 - 0
apps/dashboard/__init__.py


+ 11 - 0
apps/dashboard/views.py

@@ -0,0 +1,11 @@
+#coding=utf-8
+
+from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect
+
+def index(request):
+    user_id = request.META.get('HTTP_USER_ID')
+    token = request.META.get('HTTP_ACCESS_TOKEN')
+    if not user_id or not token:
+        return HttpResponseRedirect('/views/account/login.html')
+    else:
+        return HttpResponsePermanentRedirect("/views/index.html")

+ 0 - 0
apps/foundation/__init__.py


+ 14 - 0
apps/foundation/consts.py

@@ -0,0 +1,14 @@
+#coding=utf-8
+
+CONTENT_TYPE_SORTING = (
+
+    'account-user',  # 人员管理
+
+)
+
+MENU_TO_MODEL = (
+    (u'基础数据', ('material-material', 'material-consumable', 'goods-goods','warehouse-warehouse'
+               , 'warehouse-warehouseadmin','warehouse-warehousestock','account-department'
+               ,'foundation-option','supplier-supplier', 'customer-customer', 'account-user','foundation-bizlog','config-config',)),
+    (u'其他',('product-productbase',))
+)

+ 110 - 0
apps/foundation/models.py

@@ -0,0 +1,110 @@
+#coding=utf-8
+import json
+import datetime
+
+from django.db import models
+from django.utils import timezone
+from django.conf import settings
+
+from utils.exceptions import CustomError
+from utils.format import strftime, strfdate
+
+class Option(models.Model):
+    PAYMENT_MODE = 0
+    MATERIAL_MODE = 1
+    CONSUMABLE_MODE = 2
+    GOODS_MODE = 3
+    QUALITY_REQUEST = 4
+
+    TYPE_CHOICES = (
+        (PAYMENT_MODE, u'付款方式'),
+        (MATERIAL_MODE, u'原料类别'),
+        (CONSUMABLE_MODE, u'耗材类别'),
+        (GOODS_MODE, u'成品类别'),
+        (QUALITY_REQUEST, u'质量要求'),
+    )
+
+    type = models.PositiveSmallIntegerField(choices=TYPE_CHOICES, verbose_name=u"类别")
+    name = models.CharField(max_length=100, verbose_name=u"名称")
+    notes = models.CharField(max_length=500, verbose_name=u"备注",blank=True,null=True)
+    enabled = models.BooleanField(verbose_name=u"在用", default=True)
+
+
+    @staticmethod
+    def getTypeText(type):
+        return Option.TYPE_CHOICES[type][1]
+
+    @staticmethod
+    def getById(id):
+        try:
+            id = int(id)
+        except:
+            raise CustomError(u'无效的自定义项ID')
+
+        instance = Option.objects.filter(pk=id).first()
+        if not instance:
+            raise CustomError(u'未找到相应的自定义项')
+        return instance
+
+    @staticmethod
+    def getByName(name, type):
+        option = Option.objects.filter(name=name,type=type).first()
+        if not option:
+            raise CustomError(u'未找到[%s]为[%s]的自定义项' % (Option.getTypeText(type),name))
+        return option
+
+    class Meta:
+        db_table = "system_option"
+        verbose_name = u"自定义项管理"
+        ordering = ('-id', )
+        index_together = (
+            'name',
+        )
+        default_permissions = ()
+
+
+class BizLogManager(models.Manager):
+    def addnew(self, user, type, description, data=None):
+        def default(o):
+            if isinstance(o, datetime.datetime):
+                return strftime(o)
+            elif isinstance(o, datetime.date):
+                return strfdate(o)
+
+        row = self.model(user=user, type=type, description=description, create_time=timezone.now())
+        if data:
+            row.data = json.dumps(data, default=default)
+        row.save()
+        return row
+
+class BizLog(models.Model):
+    INSERT = 1
+    UPDATE = 2
+    DELETE = 3
+    CHECK = 4
+    EXPORT = 5
+    PRINT = 6
+    IMPORT = 7
+    TYPE_CHOICES = (
+        (INSERT, u'添加'),
+        (UPDATE, u'修改'),
+        (DELETE, u'删除'),
+        (CHECK, u'审核'),
+        (EXPORT, u'导出'),
+        (PRINT, u'打印'),
+        (IMPORT, u'导入'),
+    )
+
+    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, null=True, blank=True)
+    type = models.PositiveSmallIntegerField(choices=TYPE_CHOICES, verbose_name=u"类别")
+    description = models.CharField(max_length=1000, verbose_name=u"内容")
+    data = models.TextField(verbose_name=u"数据", null=True, blank=True)
+    create_time = models.DateTimeField(verbose_name=u"添加时间", auto_now_add=True, editable=False)
+
+    objects = BizLogManager()
+
+    class Meta:
+        db_table = "system_log"
+        ordering = ['-id']
+        verbose_name = u"日志管理"
+        default_permissions = ()

+ 0 - 0
shop/__init__.py


+ 259 - 0
shop/settings.py

@@ -0,0 +1,259 @@
+# coding=utf-8
+"""
+Django settings for markwin project.
+
+Generated by 'django-admin startproject' using Django 2.2.5.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/2.2/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/2.2/ref/settings/
+"""
+
+import os, datetime
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = 'c+)5afbc(m&qe^8t7gc54w6f-*==b2(l0(8rqef-cbwtv*3n$w'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = ['*']
+
+
+# Application definition
+
+INSTALLED_APPS = [
+    #'django.contrib.admin',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+
+    'corsheaders',
+    'rest_framework',
+    'rest_framework_jwt',
+    'django_filters',
+
+    'apps.account',
+    'apps.foundation',
+    # 'apps.log',
+    # 'apps.images',
+    # 'apps.shop',
+    # 'apps.employee',
+    # 'apps.option',
+    # 'apps.package',
+    # 'apps.package_order',
+    # 'apps.pay',
+    # 'apps.poster',
+    # 'apps.activity',
+    # 'apps.message',
+    # 'apps.vehicle_model',
+    # 'apps.customer',
+    # 'apps.vehicle_order',
+    # 'apps.recommend',
+    # 'apps.config',
+    # 'apps.sales_advisor',
+    # 'apps.commission_order',
+    # 'apps.commission_log',
+    # 'apps.cashout_order',
+    # 'apps.sms',
+    # 'apps.collection',
+    # 'apps.WechatApplet',
+    # 'apps.WechatTp',
+    # 'apps.api',
+    # 'apps.xgjsms',
+    # 'apps.coupon',
+    # 'apps.statistics',
+    # 'apps.points_order',
+    # 'apps.points_log'
+
+]
+
+MIDDLEWARE = [
+    'django.middleware.security.SecurityMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.middleware.common.CommonMiddleware',
+    'django.middleware.csrf.CsrfViewMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
+    'django.middleware.clickjacking.XFrameOptionsMiddleware',
+    'corsheaders.middleware.CorsMiddleware',
+]
+
+CORS_ALLOW_CREDENTIALS = True  # 允许携带cookie
+CORS_ORIGIN_ALLOW_ALL = True
+CORS_ALLOW_METHODS = (
+    'DELETE',
+    'GET',
+    'OPTIONS',
+    'PATCH',
+    'POST',
+    'PUT',
+    'VIEW',
+)
+CORS_ALLOW_HEADERS = (
+    'XMLHttpRequest',
+    'X_FILENAME',
+    'accept-encoding',
+    'authorization',
+    'content-type',
+    'dnt',
+    'origin',
+    'token',
+    'user-agent',
+    'x-csrftoken',
+    'x-requested-with',
+    'Pragma',
+)
+
+ROOT_URLCONF = 'shop.urls'
+AUTH_USER_MODEL = "account.User"
+AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.AllowAllUsersModelBackend']
+
+TEMPLATES = [
+    {
+        'BACKEND': 'django.template.backends.django.DjangoTemplates',
+        'DIRS': [],
+        'APP_DIRS': True,
+        'OPTIONS': {
+            'context_processors': [
+                'django.template.context_processors.debug',
+                'django.template.context_processors.request',
+                'django.contrib.auth.context_processors.auth',
+                'django.contrib.messages.context_processors.messages',
+            ],
+        },
+    },
+]
+
+WSGI_APPLICATION = 'shop.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.mysql',
+        'NAME': 'lhyt_shop',
+        'USER': 'root',
+        'PASSWORD': 'lyh123',
+        'HOST': '127.0.0.1',
+        'PORT': 3306,
+    }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+    {
+        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+    },
+]
+
+UNKNOW = 0
+FEMALE = 1
+MALE = 2
+GENDER_CHOICES = (
+    (UNKNOW, u'未知'),
+    (FEMALE, u'女'),
+    (MALE, u'男'),
+)
+
+DEFAULT = 0
+PASS = 1
+REJECT = 2
+CHECK_STATE_CHOICES = (
+    (DEFAULT, u'未审核'),
+    (PASS, u'已通过'),
+    (REJECT, u'未通过'),
+)
+
+# Internationalization
+# https://docs.djangoproject.com/en/2.2/topics/i18n/
+
+LANGUAGE_CODE = 'zh-hans'
+
+TIME_ZONE = 'Asia/Shanghai'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = False
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/2.2/howto/static-files/
+
+STATIC_URL = '/static/'
+
+MEDIA_URL = '/up/'
+MEDIA_ROOT = os.path.join(BASE_DIR, "uis/up/")
+
+UIS_URL = '/'
+UIS_ROOT = os.path.join(BASE_DIR, "uis/")
+
+SERVER_DOMAIN = 'https://xapp.aiche360.cn'
+
+JWT_AUTH = {
+    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=30),
+    'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=360),
+    'JWT_ALLOW_REFRESH': True,
+}
+
+REST_FRAMEWORK = {
+    'DEFAULT_PERMISSION_CLASSES': (
+        'rest_framework.permissions.AllowAny',
+    ),
+    'DEFAULT_AUTHENTICATION_CLASSES': (
+        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
+        #'rest_framework.authentication.SessionAuthentication',
+        #'rest_framework.authentication.BasicAuthentication',
+    ),
+    'DEFAULT_PARSER_CLASSES': (
+        'rest_framework.parsers.JSONParser',
+        'rest_framework.parsers.FormParser',
+        'rest_framework.parsers.MultiPartParser'
+    ),
+    'DEFAULT_RENDERER_CLASSES': (
+        'rest_framework.renderers.JSONRenderer',
+        # 'rest_framework.renderers.BrowsableAPIRenderer',
+    ),
+    'DEFAULT_FILTER_BACKENDS': (
+        'django_filters.rest_framework.DjangoFilterBackend',
+    ),
+    #'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
+    'DEFAULT_PAGINATION_CLASS': 'utils.pagination.CustomPagination',
+    #'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler',
+    'EXCEPTION_HANDLER': 'utils.handler.custom_exception_handler',
+    'PAGE_SIZE': 10,
+    'DATETIME_FORMAT': "%Y-%m-%d %H:%M:%S",
+    'NON_FIELD_ERRORS_KEY': "error",  # 序列化器错误KEY名称
+}
+
+# 导入本地设置
+try:
+    from shop.local_settings import *
+except ImportError:
+    pass

+ 33 - 0
shop/urls.py

@@ -0,0 +1,33 @@
+# coding=utf-8
+"""hagrid URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+    https://docs.djangoproject.com/en/2.2/topics/http/urls/
+Examples:
+Function views
+    1. Add an import:  from my_app import views
+    2. Add a URL to urlpatterns:  path('', views.home, name='home')
+Class-based views
+    1. Add an import:  from other_app.views import Home
+    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
+Including another URLconf
+    1. Import the include() function: from django.urls import include, path
+    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
+"""
+
+from django.conf.urls import url, include
+from django.conf.urls.static import static
+from django.conf import settings
+
+from apps.dashboard.views import index
+
+urlpatterns = [
+    url(r'^$', index),
+    url(r'^account/', include('apps.account.urls')),
+    # url(r'^tenant/', include('apps.tenant.urls')),
+    # url(r'^customer/', include('apps.customer.urls')),
+    # url(r'^api/', include('apps.api.urls')),
+]
+
+urlpatterns += static(settings.UIS_URL, document_root=settings.UIS_ROOT)
+urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

+ 16 - 0
shop/wsgi.py

@@ -0,0 +1,16 @@
+"""
+WSGI config for hamarkwingrid project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shop.settings')
+
+application = get_wsgi_application()

+ 0 - 0
uis/__init__.py


+ 206 - 0
uis/views/account/employee.html

@@ -0,0 +1,206 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>人员管理</title>
+  <meta name="renderer" content="webkit">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=0">
+  <link rel="stylesheet" href="../../layuiadmin/layui/css/layui.css" media="all">
+  <link rel="stylesheet" href="../../layuiadmin/style/admin.css" media="all">
+</head>
+<body>
+
+  <div class="layui-fluid">
+    <div class="layui-card">
+        <div class="layui-card-body" pad15>
+        <div class="layui-row layui-col-space15">
+          <div class="layui-col-md12">
+
+            <div class="LAY-btns" style="margin-bottom: 10px;">
+              <button class="layui-btn layui-btn-sm" data-permission="account.add_user" id="btn_add"><i class="layui-icon layui-icon-add-circle"></i>添加</button>
+              <button class="layui-btn layui-btn-sm" id="btn_query"><i class="layui-icon layui-icon-search"></i>查询</button>
+            </div>
+
+            <table class="layui-hide" id="datagrid" lay-filter="datagrid-operate"></table>
+
+            <script type="text/html" id="datagrid-operate-bar">
+                <div class="layui-btn-group">
+                  <a class="layui-btn layui-btn-xs" data-permission="account.add_user" lay-event="edit">修改</a>
+                  <a class="layui-btn layui-btn-danger layui-btn-xs" data-permission="account.delete_user" lay-event="delete">删除</a>
+                  <a class="layui-btn layui-btn-xs layui-btn-warm" data-permission="account.add_user" lay-event="manage_range">管理范围</a>
+                </div>
+            </script>
+          </div>
+        </div>
+        </div>
+    </div>
+  </div>
+
+  <div id="dlg_query" style="display: none">
+    <div class="layui-card-body" pad15>
+        <form class="layui-form" lay-filter="query-form-element">
+          <div class="layui-row layui-col-space10 layui-form-item">
+              <div class="layui-col-xs12 layui-col-sm12">
+                <label class="layui-form-label">员工姓名:</label>
+                <div class="layui-input-block">
+                    <input type="text" name="name" autocomplete="off" class="layui-input">
+                </div>
+            </div>
+            <div class="layui-col-xs12 layui-col-sm12">
+                <label class="layui-form-label">员工账号:</label>
+                <div class="layui-input-block">
+                    <input type="text" name="username" autocomplete="off" class="layui-input">
+                </div>
+            </div>
+
+          </div>
+          <div class="layui-form-item" style="display: none">
+                <button id="query_search" class="layui-btn" lay-submit lay-filter="query-form-element">查询</button>
+          </div>
+        </form>
+    </div>
+   </div>
+
+  <script src="../../layuiadmin/layui/layui.js?t=1"></script>
+  <script>
+  var _params = '';
+  layui.config({
+    base: '../../../layuiadmin/' //静态资源所在路径
+  }).extend({
+    index: 'lib/index' //主入口模块
+  }).use(['index', 'table'], function(){
+    var $ = layui.$
+    ,form = layui.form;
+    var table = layui.table;
+
+    table.render({
+      elem: '#datagrid'
+      ,url: '/account/employee/data/'
+      ,cols: [[
+        {field:'name', title:'姓名', width:100}
+        ,{field:'username', title:'登录账号',width: 100}
+        ,{field:'groups', title:'权限组', width:200}
+        ,{field:'gender_text', title:'性别', width:80}
+        ,{field:'tel', title:'手机号码',width: 130}
+        ,{field:'department_text', title:'所属部门', width:150}
+        ,{field:'ID_card', title:'身份证号码',width: 180}
+        ,{field:'address', title:'家庭住址',minWidth: 200}
+        ,{field:'status_text', title:'是否在职', width:90}
+        ,{field:'title', title:'工作岗位', width:100}
+        ,{width:170, align:'left', fixed: 'right', toolbar: '#datagrid-operate-bar'}
+      ]]
+      ,page: true
+      ,height: 'full-104'
+      ,done: function () {
+        layui.index.removeNoPermButtons();
+      }
+    });
+
+    //监听工具条
+    table.on('tool(datagrid-operate)', function(obj){
+      var data = obj.data;
+        if(obj.event === 'edit'){
+        table.editdata = data;
+        layer.open({
+          type: 2,
+          title: '修改',
+          shadeClose: false,
+          area: ['550px', '85%'],
+          btn: ['保存', '取消'],
+          yes: function(index, dom){
+             layui.onSubmitChild = function (data) {
+              layer.close(index);
+              table.reload('datagrid',{});
+            };
+            layui.submitChild();
+          },
+          btn2: function(index, layero){
+            layer.close(index);//关闭当前按钮
+          },
+          content: 'employee_edit.html?id='+data.id
+        });
+      } else if(obj.event === 'manage_range'){
+         table.editdata = data;
+         layer.open({
+          type: 2,
+          title: '管理范围',
+          shadeClose: false,
+          area: ['650px', '80%'],
+          btn: ['保存', '取消'],
+          yes: function(index, dom){
+             layui.onSubmitChild = function (data) {
+              layer.close(index);
+              table.reload('datagrid',{});
+            };
+            layui.submitChild();
+          },
+          btn2: function(index, layero){
+            layer.close(index);//关闭当前按钮
+          },
+          content: 'manage_range.html?id='+data.id
+        });
+      }else if(obj.event === 'delete'){
+         layer.confirm('确定要删除吗?', function(index){
+             layer.close(index);
+            layui.admin.req({
+              url: '/account/employee/delete/?id='+data.id
+              ,done: function(res){
+                table.reload('datagrid',{});
+              }
+            });
+         });
+      }
+    });
+
+    $('#btn_add').on('click', function(){
+        layer.open({
+          type: 2,
+          title: '添加',
+          shadeClose: false,
+          area: ['550px', '85%'],
+          btn: ['保存', '取消'],
+          yes: function(index, dom){
+             layui.onSubmitChild = function (data) {
+              layer.close(index);
+              table.reload('datagrid',{});
+            };
+            layui.submitChild();
+          },
+          btn2: function(index, layero){
+            layer.close(index);//关闭当前按钮
+          },
+          content: 'employee_edit.html'
+        });
+    });
+
+    $('#btn_query').on('click', function(){
+        layer.open({
+          type: 1,
+          shadeClose: true,
+          area: ['500px', '250px'],
+          title: '查询',
+          btn: ['查询'],
+          yes: function(index, dom){
+             $('#query_search').click();
+          },
+          content: $('#dlg_query')
+        });
+    });
+
+    form.on('submit(query-form-element)', function(data){
+      //layer.msg(JSON.stringify(data.field));
+      _params = data.field;
+      table.reload('datagrid', {
+          where: data.field,
+          page:{curr:1}
+      });
+      layer.closeAll();
+      return false;
+    });
+
+  });
+  </script>
+</body>
+</html>
+

+ 211 - 0
uis/views/account/employee_edit.html

@@ -0,0 +1,211 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>添加员工信息</title>
+  <meta name="renderer" content="webkit">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=0">
+  <link rel="stylesheet" href="../../layuiadmin/layui/css/layui.css" media="all">
+  <link rel="stylesheet" href="../../layuiadmin/style/admin.css" media="all">
+  <link rel="stylesheet" type="text/css" href="../../layuiadmin/style/formSelects-v4.css"/>
+  <style type="text/css">
+        #department_selecter dl{max-height: 250px;}
+        #group_selecter dl{max-height: 200px;}
+    </style>
+</head>
+<body>
+
+<div class="layui-fluid">
+    <div class="layui-card">
+        <div class="layui-card-body" pad15>
+        <div class="layui-row layui-col-space15">
+          <div class="layui-col-md12">
+            <form class="layui-form" action="" lay-filter="component-form-element">
+              <div class="layui-row layui-col-space10 layui-form-item">
+
+                <div class="layui-col-lg6">
+                  <label class="layui-form-label"><font color='red' size="4">*</font>姓名:</label>
+                  <div class="layui-input-block">
+                    <input type="text" name="name" lay-verify="required" placeholder="请输入姓名" autocomplete="off" class="layui-input">
+                  </div>
+                </div>
+
+                <div class="layui-col-lg6">
+                  <label class="layui-form-label"><font color='red' size="4">*</font>账号:</label>
+                  <div class="layui-input-block">
+                    <input type="text" name="username" lay-verify="required" placeholder="请输入用户名" autocomplete="off" class="layui-input">
+                  </div>
+                </div>
+
+                  <div class="layui-col-lg6">
+                    <label class="layui-form-label"><font color='red' size="4">*</font>密码:</label>
+                    <div class="layui-input-block">
+                      <input type="password" name="password" placeholder="请输入密码" autocomplete="off" class="layui-input">
+                        <div class="layui-word-aux">默认密码:1111;<br>修改信息时如果留空,则不修改密码。</div>
+                    </div>
+                  </div>
+
+                <div class="layui-col-lg6">
+                    <label class="layui-form-label"><font color='red' size="4">*</font>性别:</label>
+                    <div class="layui-input-block">
+                      <select name="gender" lay-verify="required" >
+                         <option value="2">先生</option>
+                          <option value="1">女士</option>
+                      </select>
+                    </div>
+                </div>
+
+                <div class="layui-col-lg6">
+                  <label class="layui-form-label"><font color='red' size="4">*</font>手机号码:</label>
+                  <div class="layui-input-block">
+                    <input type="text" name="tel" placeholder="请输入手机号" lay-verify="required|tel" autocomplete="off" class="layui-input">
+                  </div>
+                </div>
+
+                <div class="layui-col-lg6">
+                    <label class="layui-form-label"><font color='red' size="4">*</font>所属部门:</label>
+                    <div class="layui-input-block" id="department_selecter">
+                      <select name="department" xm-select="selectDepartment" xm-select-radio lay-verify="required">
+                      </select>
+                    </div>
+                  </div>
+
+                <div class="layui-col-lg6">
+                  <label class="layui-form-label">权限组:</label>
+                  <div class="layui-input-block" id="group_selecter">
+                    <select name="groups" xm-select="selectGroup"></select>
+                  </div>
+                </div>
+
+                <div class="layui-col-lg6">
+                  <label class="layui-form-label">身份证号:</label>
+                  <div class="layui-input-block">
+                    <input type="text" name="ID_card" placeholder="" autocomplete="off" class="layui-input">
+                  </div>
+                </div>
+
+                <div class="layui-col-lg6">
+                  <label class="layui-form-label">家庭住址:</label>
+                  <div class="layui-input-block">
+                    <input type="text" name="address" placeholder="" autocomplete="off" class="layui-input">
+                  </div>
+                </div>
+
+                <div class="layui-col-lg6">
+                  <label class="layui-form-label">工作岗位:</label>
+                  <div class="layui-input-block">
+                    <input type="text" name="title" placeholder="" autocomplete="off" class="layui-input">
+                  </div>
+                </div>
+
+                <div class="layui-col-lg6">
+                  <label class="layui-form-label">是否在职:</label>
+                  <div class="layui-input-block">
+                    <input type="checkbox" name="status" lay-skin="switch" lay-text="是|否" checked="" value="1">
+                  </div>
+                </div>
+
+                <button class="layui-btn" id="submit_btn" lay-submit lay-filter="component-form-element" style="display: none">保存</button>
+
+              </div>
+            </form>
+          </div>
+        </div>
+        </div>
+    </div>
+</div>
+
+  <script src="../../layuiadmin/layui/layui.js"></script>
+  <script>
+  layui.config({
+    base: '../../../layuiadmin/' //静态资源所在路径
+  }).extend({
+    index: 'lib/index',
+    utils:'utils',
+    formSelects: 'formSelects-v4'
+  }).use(['index','utils', 'form', 'formSelects'], function(){
+    var $ = layui.$
+    ,admin = layui.admin
+    ,element = layui.element
+    ,form = layui.form
+    ,formSelects = layui.formSelects
+    ,dep_formSelects = layui.formSelects;
+
+    var editdata = null;
+    var department = '';
+    var id = layui.view.getParameterByName('id');
+
+    if(id){
+      department = parent.layui.table.editdata.department;
+      editdata = JSON.parse(JSON.stringify(parent.layui.table.editdata)); // 框架有Bug所以这么转换
+      editdata.password = '';
+      if (editdata.status == 1) {
+        editdata.status = true;
+      } else {
+        editdata.status = false;
+      }
+    }
+
+    form.val("component-form-element", {'password':'1111'});
+    if(editdata){
+        form.val("component-form-element", editdata);
+    }
+
+    dep_formSelects.value('selectDepartment', []);
+    admin.req({
+        url: '/account/department/tree/'
+        ,done: function(res){
+            dep_formSelects.data('selectDepartment', 'local', {
+            arr: res.data,
+            tree: {
+                //在点击节点的时候, 如果没有子级数据, 会触发此事件
+                nextClick: function(id, item, callback){
+                    return false;
+                    },
+                }
+            });
+            dep_formSelects.value('selectDepartment', [department]);
+        }
+    });
+
+    formSelects.config('selectGroup', {
+      beforeSuccess:function(eid, url, searchVal, result){
+          if(id){
+              var groupIds = parent.layui.table.editdata.group_ids;
+              for(var n in result.data){
+                  var item = result.data[n];
+                  if(groupIds.indexOf(parseInt(item.value)) > -1)
+                      item.selected = 'selected';
+              }
+          }
+
+          return result;
+      }
+    });
+    formSelects.data('selectGroup', 'server', {
+        url: '/account/group/combobox/data/'
+    });
+
+    form.on('submit(component-form-element)', function(data){
+      var submitData = data.field;
+      submitData.groups = formSelects.value('selectGroup', 'val');
+      admin.req({
+        url: '/account/employee/save/?id='+id
+        ,data: JSON.stringify(submitData)
+        ,type: 'post'
+        ,done: function(res){
+            parent.layui.onSubmitChild(res.data);
+        }
+      });
+
+      return false;
+    });
+    parent.layui.submitChild = function () {
+      $("#submit_btn").click();
+    };
+
+  });
+  </script>
+</body>
+</html>

+ 135 - 0
uis/views/account/login.html

@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>小程序商城</title>
+  <meta name="renderer" content="webkit">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=0">
+  <link rel="stylesheet" href="../../layuiadmin/layui/css/layui.css" media="all">
+  <link rel="stylesheet" href="../../layuiadmin/style/admin.css" media="all">
+  <link rel="stylesheet" href="../../layuiadmin/style/login.css" media="all">
+</head>
+<body>
+
+  <div class="layadmin-user-login layadmin-user-display-show" id="LAY-user-login" style="display: none;">
+
+    <div class="layadmin-user-login-main">
+      <div class="layadmin-user-login-box layadmin-user-login-header">
+        <h2>小程序商城系统</h2>
+      </div>
+      <form class="layui-form" action="" lay-filter="component-form-element">
+      <div class="layadmin-user-login-box layadmin-user-login-body layui-form">
+        <div class="layui-form-item">
+          <label class="layadmin-user-login-icon layui-icon layui-icon-username" for="LAY-user-login-username"></label>
+          <input type="text" name="username" id="LAY-user-login-username" lay-verify="required" placeholder="用户名" autocomplete="off" class="layui-input">
+        </div>
+        <div class="layui-form-item">
+          <label class="layadmin-user-login-icon layui-icon layui-icon-password" for="LAY-user-login-password"></label>
+          <input type="password" name="password" id="LAY-user-login-password" autocomplete="off" lay-verify="required" placeholder="密码" class="layui-input">
+        </div>
+        <div class="layui-form-item">
+          <button class="layui-btn layui-btn-fluid" lay-submit lay-filter="component-form-element">登 录</button>
+        </div>
+      </div>
+       </form>
+    </div>
+    
+    <div class="layui-trans layadmin-user-login-footer">
+      <p>© 2022 小程序商城系统 <a href="http://www.zzliaoyuan.com/" target="_blank">郑州燎原计算机技术有限公司</a></p>
+    </div>
+    
+    <!--<div class="ladmin-user-login-theme">
+      <script type="text/html" template>
+        <ul>
+          <li data-theme=""><img src="{{ layui.setter.base }}style/res/bg-none.jpg"></li>
+          <li data-theme="#03152A" style="background-color: #03152A;"></li>
+          <li data-theme="#2E241B" style="background-color: #2E241B;"></li>
+          <li data-theme="#50314F" style="background-color: #50314F;"></li>
+          <li data-theme="#344058" style="background-color: #344058;"></li>
+          <li data-theme="#20222A" style="background-color: #20222A;"></li>
+        </ul>
+      </script>
+    </div>-->
+    
+  </div>
+
+  <script src="../../layuiadmin/layui/layui.js"></script>
+  <script>
+  layui.config({
+    base: '../../layuiadmin/' //静态资源所在路径
+  }).extend({
+    index: 'lib/index' //主入口模块
+  }).use(['index', 'user'], function(){
+    var $ = layui.$
+    ,setter = layui.setter
+    ,admin = layui.admin
+    ,form = layui.form
+    ,router = layui.router()
+    ,search = router.search;
+
+    if (layui.data(setter.tableName)[setter.request.tokenName]) {
+      admin.req({
+        url: '/tenant/token_refresh/'
+        ,data: {token: layui.data(setter.tableName)[setter.request.tokenName].substr(4)}
+        ,type: 'post'
+        ,done: function(res){
+
+          //请求成功后,写入 access_token
+          layui.data(setter.tableName, {
+            key: setter.request.tokenName
+            ,value: 'JWT ' + res.data.token
+          });
+
+          location.href = '../index.html'; //后台主页
+        }
+      });
+    }
+
+    form.render();
+
+    //提交
+    form.on('submit(component-form-element)', function(obj){
+
+      //请求登入接口
+      admin.req({
+        url: '/account/login/'
+        ,data: obj.field
+        ,type: 'post'
+        ,done: function(res){
+
+          //请求成功后,写入 access_token
+          layui.data(setter.tableName, {
+            key: setter.request.tokenName
+            ,value: 'JWT ' + res.data.token
+          });
+          layui.data(setter.tableName, {
+            key: setter.request.userId
+            ,value: res.data.user_id
+          });
+          layui.data(setter.tableName, {
+            key: 'name'
+            ,value: res.data.username
+          });
+          layui.data(setter.tableName, {
+            key: 'permissions'
+            ,value: res.data.permissions
+          });
+
+          //登入成功的提示与跳转
+          layer.msg('登入成功', {
+            offset: '15px'
+            ,icon: 1
+            ,time: 1000
+          }, function(){
+            location.href = '../index.html'; //后台主页
+          });
+        }
+      });
+      return false;
+      
+    });
+  });
+  </script>
+</body>
+</html>

+ 115 - 0
uis/views/account/manage_range.html

@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>管理范围</title>
+  <meta name="renderer" content="webkit">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=0">
+  <link rel="stylesheet" href="../../layuiadmin/layui/css/layui.css" media="all">
+  <link rel="stylesheet" href="../../layuiadmin/style/admin.css" media="all">
+  <link rel="stylesheet" type="text/css" href="../../layuiadmin/style/formSelects-v4.css"/>
+  <style type="text/css">
+        #department_employee_selecter dl{max-height: 250px;}
+  </style>
+</head>
+<body>
+
+<div class="layui-fluid">
+  <div class="layui-card">
+    <div class="layui-card-body" pad15>
+      <div class="layui-row layui-col-space15">
+        <div class="layui-col-md12">
+
+          <form class="layui-form" action="" lay-filter="component-form-element">
+            <div class="layui-row layui-col-space10 layui-form-item">
+
+              <div class="layui-col-md12">
+                <label class="layui-form-label">部门/人员:</label>
+                <div class="layui-input-block" id="department_employee_selecter">
+                  <select name="managers" xm-select="manager_range">
+                  </select>
+                </div>
+              </div>
+              <button class="layui-btn" id="submit_btn" lay-submit lay-filter="component-form-element" style="display: none">保存</button>
+
+            </div>
+          </form>
+
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+
+  <script src="../../layuiadmin/layui/layui.js"></script>
+  <script>
+  layui.config({
+    base: '../../../layuiadmin/' //静态资源所在路径
+  }).extend({
+    index: 'lib/index' //主入口模块
+    ,formSelects: 'formSelects-v4'
+  }).use(['index', 'form','formSelects'], function(){
+    var $ = layui.$
+    ,admin = layui.admin
+    ,element = layui.element
+    ,form = layui.form
+    ,formSelects = layui.formSelects;
+
+    formSelects.render('manager_range',{
+      template:function(id, item) {
+        if (item.value.split('_')[0] == 'employee') {
+          return "<span style='color:rgb(0,150,136);'>"+item.name+"</span>";
+        }
+        return item.name;
+      }
+    });
+
+    var id = layui.view.getParameterByName('id');
+    var editdata = parent.layui.table.editdata;
+    var manages = [];
+    for (var n in editdata.sub_department_ids) {
+        manages.push('department_'+editdata.sub_department_ids[n]);
+    }
+    for (var n in editdata.sub_employee_ids) {
+        manages.push('employee_'+editdata.sub_employee_ids[n]);
+    }
+
+    formSelects.value('manager_range', []);
+    admin.req({
+        url: '/account/employee/tree/'
+        ,done: function(res){
+            formSelects.data('manager_range', 'local', {
+            arr: res.data,
+            tree: {
+                //在点击节点的时候, 如果没有子级数据, 会触发此事件
+                nextClick: function(id, item, callback){
+                    return false;
+                    },
+                }
+            });
+            formSelects.value('manager_range', manages);
+        }
+    });
+
+    form.on('submit(component-form-element)', function(data){
+        data.field.managers = formSelects.value('manager_range', 'val');
+        admin.req({
+        url: '/account/employee/manager/save/?id='+id
+        ,data: JSON.stringify(data.field)
+        ,type: 'post'
+        ,done: function(res){
+            parent.layui.onSubmitChild();
+        }
+      });
+        return false;
+    });
+
+    parent.layui.submitChild = function () {
+      $("#submit_btn").click();
+    };
+
+  });
+  </script>
+</body>
+</html>

+ 92 - 0
uis/views/account/user_password.html

@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>修改密码</title>
+  <meta name="renderer" content="webkit">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=0">
+  <link rel="stylesheet" href="../../layuiadmin/layui/css/layui.css" media="all">
+  <link rel="stylesheet" href="../../layuiadmin/style/admin.css" media="all">
+</head>
+<body>
+
+  <div class="layui-fluid">
+    <div class="layui-card">
+        <div class="layui-card-body" pad15>
+        <div class="layui-row layui-col-space15">
+          <div class="layui-col-md12">
+            <form class="layui-form" action="" lay-filter="component-form-element">
+              <div class="layui-row layui-col-space10 layui-form-item">
+                <div class="layui-col-lg12">
+                  <label class="layui-form-label">原密码:</label>
+                  <div class="layui-input-block">
+                    <input type="text" name="old_password" lay-verify="required" placeholder="" autocomplete="off" class="layui-input">
+                  </div>
+                </div>
+                <div class="layui-col-lg12">
+                  <label class="layui-form-label">新密码:</label>
+                  <div class="layui-input-block">
+                    <input type="text" name="new_password" placeholder="" autocomplete="off" class="layui-input" lay-verify="required">
+                  </div>
+                </div>
+                 <div class="layui-col-lg12">
+                  <label class="layui-form-label">密码确认:</label>
+                  <div class="layui-input-block">
+                    <input type="text" name="confirm_password" placeholder="" autocomplete="off" class="layui-input" lay-verify="required">
+                  </div>
+                </div>
+              <div class="layui-form-item">
+                <div class="layui-input-block">
+                  <button class="layui-btn" lay-submit lay-filter="component-form-element">保存</button>
+                </div>
+              </div>
+              </div>
+            </form>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+
+  <script src="../../layuiadmin/layui/layui.js"></script>
+  <script>
+  layui.config({
+    base: '../../../layuiadmin/' //静态资源所在路径
+  }).extend({
+    index: 'lib/index' //主入口模块
+  }).use(['index', 'form'], function(){
+    var $ = layui.$
+    ,admin = layui.admin
+    ,element = layui.element
+    ,form = layui.form;
+    form.render(null, 'component-form-element');
+    element.render('breadcrumb', 'breadcrumb');
+
+    form.on('submit(component-form-element)', function(data){
+      admin.req({
+        url: '/account/password/save/'
+        ,data: JSON.stringify(data.field)
+        ,type: 'post'
+        ,done: function(res){
+            layer.open({
+                 type: 1
+                ,content: '<div style="padding: 20px 100px;">保存成功</div>'
+                ,btn: '关闭'
+                ,btnAlign: 'c' //按钮居中
+                ,shade: 0 //不显示遮罩
+                ,yes: function(){
+                  layer.closeAll();
+                }
+            });
+        }
+      });
+
+      return false;
+    });
+
+  });
+  </script>
+</body>
+</html>

+ 21 - 0
uis/views/dashboard/home.html

@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html style="height: 100%;">
+<head>
+  <meta charset="utf-8">
+  <title>首页</title>
+  <meta name="renderer" content="webkit">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=0">
+  <link rel="stylesheet" href="../../layuiadmin/layui/css/layui.css" media="all">
+  <link rel="stylesheet" href="../../layuiadmin/style/admin.css" media="all">
+</head>
+<body style="height: 100%;">
+
+<div class="layui-card" style="height: 100%;display:flex;">
+  <div style="margin:auto;font-weight:bold;font-size: 32px;font-style: normal;color:#868686;">
+    欢迎使用小程序商城后台系统
+  </div>
+</div>
+
+</body>
+</html>

+ 375 - 0
uis/views/index.html

@@ -0,0 +1,375 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>小程序商城系统</title>
+  <meta name="renderer" content="webkit">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=0">
+  <link rel="stylesheet" href="../layuiadmin/layui/css/layui.css" media="all">
+  <link rel="stylesheet" href="../layuiadmin/style/admin.css" media="all">
+</head>
+<body class="layui-layout-body">
+  
+  <div id="LAY_app">
+    <div class="layui-layout layui-layout-admin">
+      <div class="layui-header">
+        <!-- 头部区域 -->
+        <ul class="layui-nav layui-layout-left">
+          <li class="layui-nav-item layadmin-flexible" lay-unselect>
+            <a href="javascript:;" layadmin-event="flexible" title="侧边伸缩">
+              <i class="layui-icon layui-icon-shrink-right" id="LAY_app_flexible"></i>
+            </a>
+          </li>
+          <li class="layui-nav-item" lay-unselect>
+            <a href="javascript:;" layadmin-event="refresh" title="刷新">
+              <i class="layui-icon layui-icon-refresh-3"></i>
+            </a>
+          </li>
+        </ul>
+        <ul class="layui-nav layui-layout-right" lay-filter="layadmin-layout-right">
+          <li class="layui-nav-item layui-hide-xs" lay-unselect>
+            <a href="javascript:;" layadmin-event="theme">
+              <i class="layui-icon layui-icon-theme"></i>
+            </a>
+          </li>
+          <li class="layui-nav-item" lay-unselect>
+            <a href="javascript:;">
+              <cite id="id_username"></cite>
+            </a>
+            <dl class="layui-nav-child">
+              <dd><a lay-href="account/user_password.html">修改密码</a></dd>
+              <hr>
+              <dd layadmin-event="logout" style="text-align: center;"><a>退出</a></dd>
+            </dl>
+          </li>
+
+          <li class="layui-nav-item layui-hide-xs" lay-unselect>
+            <a href="javascript:;" layadmin-event="about"><i class="layui-icon layui-icon-more-vertical"></i></a>
+          </li>
+          <li class="layui-nav-item layui-show-xs-inline-block layui-hide-sm" lay-unselect>
+            <a href="javascript:;" layadmin-event="more"><i class="layui-icon layui-icon-more-vertical"></i></a>
+          </li>
+        </ul>
+      </div>
+      
+      <!-- 侧边菜单 -->
+      <div class="layui-side layui-side-menu">
+        <div class="layui-side-scroll">
+          <div class="layui-logo">
+            <span>小程序商城<span style="font-size: 12px;">erp</span></span>
+          </div>
+
+          <ul class="layui-nav layui-nav-tree" lay-shrink="all" id="LAY-system-side-muen" lay-filter="layadmin-system-side-menu">
+            <li data-name="set" class="layui-nav-item">
+              <a href="javascript:;" lay-tips="公告管理" lay-direction="2">
+                <i class="layui-icon layui-icon-notice"></i>
+                <cite>公告管理</cite>
+              </a>
+              <dl class="layui-nav-child">
+                <dd data-name="button" data-permission="office.view_notice">
+                  <a lay-href="notice/notice.html">公告管理</a>
+                </dd>
+              </dl>
+            </li>
+            <li data-name="set" class="layui-nav-item">
+              <a href="javascript:;" lay-tips="销售管理" lay-direction="2">
+                <i class="layui-icon layui-icon-form"></i>
+                <cite>销售管理</cite>
+              </a>
+              <dl class="layui-nav-child">
+                <dd data-name="button" data-permission="plan.view_sale_plan">
+                  <a lay-href="plan/sale.html">销售计划管理</a>
+                </dd>
+                <dd data-name="button" data-permission="order.view_sale_order">
+                  <a lay-href="order/sale_order.html">销售订单管理</a>
+                </dd>
+                <dd data-name="button" data-permission="product.view_goods_product">
+                  <a lay-href="goods/goods_stock_query.html">成品库存查询</a>
+                </dd>
+              </dl>
+            </li>
+            <li data-name="component" class="layui-nav-item">
+              <a href="javascript:;" lay-tips="生产管理" lay-direction="2">
+                <i class="layui-icon layui-icon-read"></i>
+                <cite>生产管理</cite>
+              </a>
+              <dl class="layui-nav-child">
+                <dd data-name="nav" data-permission="plan.view_production_plan">
+                  <a lay-href="plan/production.html">生产计划管理</a>
+                </dd>
+              </dl>
+            </li>
+            <li data-name="set" class="layui-nav-item">
+              <a href="javascript:;" lay-tips="采购管理" lay-direction="2">
+                <i class="layui-icon layui-icon-cart-simple"></i>
+                <cite>采购管理</cite>
+              </a>
+              <dl class="layui-nav-child">
+                <dd data-name="button" data-permission="purchase.view_purchase_plan">
+                  <a lay-href="purchase/purchase.html">计划管理</a>
+                </dd>
+                <dd data-name="button" data-permission="purchase.view_purchase_user_plan">
+                  <a lay-href="purchase/purchase_price.html">询价管理</a>
+                </dd>
+                <dd data-name="button" data-permission="purchase.view_purchase_order">
+                    <a lay-href="purchase/purchase_order.html">合同管理</a>
+                  </dd>
+                <dd data-name="button" data-permission="purchase.view_purchase_payment">
+                    <a lay-href="purchase/purchase_payment.html">付款管理</a>
+                  </dd>
+                <dd data-name="button" data-permission="purchase.view_purchase_invoice">
+                    <a lay-href="purchase/purchase_invoice.html">发票管理</a>
+                  </dd>
+                <dd data-name="button" data-permission="purchase.view_purchase_price_query">
+                    <a lay-href="purchase/purchase_price_query.html">询价历史</a>
+                  </dd>
+              </dl>
+            </li>
+            <li data-name="component" class="layui-nav-item">
+              <a href="javascript:;" lay-tips="原料管理" lay-direction="2">
+                <i class="layui-icon layui-icon-read"></i>
+                <cite>原料管理</cite>
+              </a>
+              <dl class="layui-nav-child" >
+                <dd data-name="nav" data-permission="purchase.view_material_godown_entry">
+                  <a lay-href="purchase/material_godownentry.html">入库管理</a>
+                </dd>
+                  <dd data-name="nav" data-permission="purchase.view_material_godownentry_query">
+                  <a lay-href="purchase/material_godownentry_query.html">入库查询</a>
+                </dd>
+                  <dd data-name="nav" data-permission="material.view_material_deliver">
+                  <a lay-href="material/deliver_material.html">出库管理</a>
+                </dd>
+                  <dd data-name="nav" data-permission="plan.view_material_deliver_query">
+                  <a lay-href="material/deliver_material_query.html">出库查询</a>
+                </dd>
+                  <dd data-name="nav" data-permission="purchase.view_material_godown_entry_return">
+                  <a lay-href="purchase/material_godownentry_return.html">退货管理</a>
+                </dd>
+                  <dd data-name="nav" data-permission="account.view_material_godownentry_return_query">
+                  <a lay-href="purchase/material_godownentry_return_query.html">退货查询</a>
+                </dd>
+                <dd data-name="nav" data-permission="material.view_material_deliver_return">
+                  <a lay-href="material/deliver_material_return.html">退料管理</a>
+                </dd>
+                  <dd data-name="nav" data-permission="warehouse.view_material_deliver_return_query">
+                  <a lay-href="material/deliver_material_return_query.html">退料查询</a>
+                </dd>
+                  <dd data-name="nav" data-permission="warehouse.view_material_inventory">
+                  <a lay-href="material/material_inventory.html">盘存管理</a>
+                </dd>
+                <dd data-name="nav" data-permission="warehouse.view_material_stock">
+                  <a lay-href="material/material_stock.html">库存查询</a>
+                </dd>
+                <dd data-name="nav" data-permission="product.view_material_stock_ledger">
+                  <a lay-href="material/material_stock_ledger.html">库存总账</a>
+                </dd>
+              </dl>
+            </li>
+              <li data-name="component" class="layui-nav-item">
+              <a href="javascript:;" lay-tips="耗材管理" lay-direction="2">
+                <i class="layui-icon layui-icon-read"></i>
+                <cite>耗材管理</cite>
+              </a>
+              <dl class="layui-nav-child" >
+                <dd data-name="nav" data-permission="purchase.view_consumable_godown_entry">
+                  <a lay-href="purchase/consumable_godownentry.html">入库管理</a>
+                </dd>
+                  <dd data-name="nav" data-permission="purchase.view_consumable_godownentry_query">
+                  <a lay-href="purchase/consumable_godownentry_query.html">入库查询</a>
+                </dd>
+                  <dd data-name="nav" data-permission="material.view_consumable_deliver">
+                  <a lay-href="material/deliver_consumable.html">出库管理</a>
+                </dd>
+                  <dd data-name="nav" data-permission="plan.view_consumable_deliver_query">
+                  <a lay-href="material/deliver_consumable_query.html">出库查询</a>
+                </dd>
+                  <dd data-name="nav" data-permission="purchase.view_consumable_godown_entry_return">
+                  <a lay-href="purchase/consumable_godownentry_return.html">退货管理</a>
+                </dd>
+                  <dd data-name="nav" data-permission="account.view_consumable_godownentry_return_query">
+                  <a lay-href="purchase/consumable_godownentry_return_query.html">退货查询</a>
+                </dd>
+                <dd data-name="nav" data-permission="material.view_consumable_deliver_return">
+                  <a lay-href="material/deliver_consumable_return.html">退料管理</a>
+                </dd>
+                  <dd data-name="nav" data-permission="purchase.view_consumable_deliver_return_query">
+                  <a lay-href="material/deliver_consumable_return_query.html">退料查询</a>
+                </dd>
+                  <dd data-name="nav" data-permission="warehouse.view_consumable_inventory">
+                  <a lay-href="material/consumable_inventory.html">盘存管理</a>
+                </dd>
+                <dd data-name="nav" data-permission="warehouse.view_consumable_stock">
+                  <a lay-href="material/consumable_stock.html">库存查询</a>
+                </dd>
+                <dd data-name="nav" data-permission="product.view_consumable_stock_ledger">
+                  <a lay-href="material/consumable_stock_ledger.html">库存总账</a>
+                </dd>
+              </dl>
+            </li>
+              <li data-name="component" class="layui-nav-item">
+              <a href="javascript:;" lay-tips="成品管理" lay-direction="2">
+                <i class="layui-icon layui-icon-read"></i>
+                <cite>成品管理</cite>
+              </a>
+              <dl class="layui-nav-child" >
+                <dd data-name="nav" data-permission="goods.view_goods_godown_entry">
+                  <a lay-href="goods/goods_godownentry.html">入库管理</a>
+                </dd>
+                  <dd data-name="nav" data-permission="order.view_goods_godownentry_query">
+                  <a lay-href="goods/goods_godownentry_query.html">入库查询</a>
+                </dd>
+                  <dd data-name="nav" data-permission="order.view_goods_deliver">
+                  <a lay-href="order/goods_deliver.html">出库管理</a>
+                </dd>
+                  <dd data-name="nav" data-permission="order.view_goods_deliver_query">
+                  <a lay-href="order/goods_deliver_query.html">出库查询</a>
+                </dd>
+                  <dd data-name="nav" data-permission="order.view_goods_deliver_return">
+                  <a lay-href="order/goods_deliver_return.html">退库管理</a>
+                </dd>
+                  <dd data-name="nav" data-permission="order.view_goods_deliver_return_query">
+                  <a lay-href="order/goods_deliver_return_query.html">退库查询</a>
+                </dd>
+                </dd>
+                  <dd data-name="nav" data-permission="goods.view_goods_inventory">
+                  <a lay-href="goods/goods_inventory.html">盘存管理</a>
+                </dd>
+                <dd data-name="nav" data-permission="warehouse.view_goods_stock">
+                  <a lay-href="goods/goods_stock.html">库存查询</a>
+                </dd>
+                <dd data-name="nav" data-permission="product.view_goods_stock_ledger">
+                  <a lay-href="goods/goods_stock_ledger.html">库存总账</a>
+                </dd>
+              </dl>
+            </li>
+            <li data-name="component" class="layui-nav-item">
+              <a href="javascript:;" lay-tips="统计报表" lay-direction="2">
+                <i class="layui-icon layui-icon-read"></i>
+                <cite>统计报表</cite>
+              </a>
+              <dl class="layui-nav-child">
+                <dd data-name="nav" data-permission="product.view_department_ledger">
+                  <a lay-href="statistics/department_ledger.html">部门总账</a>
+                </dd>
+              </dl>
+            </li>
+            <li data-name="set" class="layui-nav-item">
+              <a href="javascript:;" lay-tips="基础数据" lay-direction="2">
+                <i class="layui-icon layui-icon-set"></i>
+                <cite>基础数据</cite>
+              </a>
+              <dl class="layui-nav-child">
+                <dd data-name="nav" data-permission="material.view_material">
+                  <a lay-href="material/material.html">原料产品管理</a>
+                </dd>
+                <dd data-name="nav" data-permission="material.view_consumable">
+                  <a lay-href="material/consumable.html">耗材产品管理</a>
+                </dd>
+                <dd data-name="nav" data-permission="goods.view_goods">
+                  <a lay-href="goods/goods.html">成品产品管理</a>
+                </dd>
+                <dd data-name="nav" data-permission="warehouse.view_material_warehouse">
+                  <a lay-href="warehouse/material_warehouse.html">原料仓别管理</a>
+                </dd>
+                <dd data-name="nav" data-permission="warehouse.view_consumable_warehouse">
+                  <a lay-href="warehouse/consumable_warehouse.html">耗材仓别管理</a>
+                </dd>
+                <dd data-name="nav" data-permission="warehouse.view_goods_warehouse">
+                  <a lay-href="warehouse/goods_warehouse.html">成品仓别管理</a>
+                </dd>
+                <dd data-name="nav" data-permission="account.view_department">
+                  <a lay-href="account/department.html">组织结构管理</a>
+                </dd>
+                <dd data-name="nav" data-permission="foundation.view_option">
+                  <a lay-href="foundation/option.html">自定义项管理</a>
+                </dd>
+                <dd data-name="nav" data-permission="supplier.view_supplier">
+                  <a lay-href="supplier/supplier.html">供应商管理</a>
+                </dd>
+                <dd data-name="nav" data-permission="customer.view_customer">
+                  <a lay-href="customer/customer.html">客户管理</a>
+                </dd>
+                <dd data-name="nav" data-permission="account.view_user">
+                  <a lay-href="account/employee.html">人员管理</a>
+                </dd>
+                <dd data-name="nav" data-permission="foundation.view_group">
+                  <a lay-href="account/group.html">权限管理</a>
+                </dd>
+                <dd data-name="nav" data-permission="config.edit_config">
+                  <a lay-href="config/index.html">基础设置</a>
+                </dd>
+              </dl>
+            </li>
+          </ul>
+        </div>
+      </div>
+
+      <!-- 页面标签 -->
+      <div class="layadmin-pagetabs" id="LAY_app_tabs">
+        <div class="layui-icon layadmin-tabs-control layui-icon-prev" layadmin-event="leftPage"></div>
+        <div class="layui-icon layadmin-tabs-control layui-icon-next" layadmin-event="rightPage"></div>
+        <div class="layui-icon layadmin-tabs-control layui-icon-down">
+          <ul class="layui-nav layadmin-tabs-select" lay-filter="layadmin-pagetabs-nav">
+            <li class="layui-nav-item" lay-unselect>
+              <a href="javascript:;"></a>
+              <dl class="layui-nav-child layui-anim-fadein">
+                <dd layadmin-event="closeThisTabs"><a href="javascript:;">关闭当前标签页</a></dd>
+                <dd layadmin-event="closeOtherTabs"><a href="javascript:;">关闭其它标签页</a></dd>
+                <dd layadmin-event="closeAllTabs"><a href="javascript:;">关闭全部标签页</a></dd>
+              </dl>
+            </li>
+          </ul>
+        </div>
+        <div class="layui-tab" lay-unauto lay-allowClose="true" lay-filter="layadmin-layout-tabs">
+          <ul class="layui-tab-title" id="LAY_app_tabsheader">
+            <li lay-id="home/console.html" class="layui-this"><i class="layui-icon layui-icon-home"></i></li>
+          </ul>
+        </div>
+      </div>
+      
+      
+      <!-- 主体内容 -->
+      <div class="layui-body" id="LAY_app_body">
+        <div class="layadmin-tabsbody-item layui-show">
+          <iframe src="dashboard/home.html" frameborder="0" class="layadmin-iframe"></iframe>
+        </div>
+      </div>
+      
+      <!-- 辅助元素,一般用于移动设备下遮罩 -->
+      <div class="layadmin-body-shade" layadmin-event="shade"></div>
+    </div>
+  </div>
+
+  <script src="../layuiadmin/layui/layui.js"></script>
+  <script>
+  layui.config({
+    base: '../layuiadmin/' //静态资源所在路径
+  }).extend({
+    index: 'lib/index' //主入口模块
+  }).use('index', function () {
+      var $ = layui.$;
+      var name = layui.data(layui.setter.tableName)['name'];
+      $('#id_username').html(name);
+
+      var permissions = layui.data(layui.setter.tableName)['permissions'];
+      $('#LAY-system-side-muen .layui-nav-child dd').each(function () {
+          var perm = $(this).data("permission");
+          //console.log(perm, permissions.indexOf(perm))
+          if(permissions.indexOf(perm) == -1){
+            $(this).remove();
+          }
+      });
+
+      $('#LAY-system-side-muen .layui-nav-item dl').each(function () {
+          if($(this).children('dd').length == 0)
+              $(this).parent().remove();
+      });
+
+  });
+  </script>
+</body>
+</html>
+
+

+ 24 - 0
utils/__init__.py

@@ -0,0 +1,24 @@
+#coding=utf-8
+
+from rest_framework import status
+from rest_framework.response import Response
+
+def response_error(msg, errcode=None):
+    code = 1
+    if errcode:
+        code = errcode
+    return Response({"code":code, "msg": msg}, status=status.HTTP_200_OK)
+
+def response_ok(data=None):
+    if data != None:
+        return Response({"code":0, 'data': data})
+    else:
+        return Response({"code":0})
+
+def get_remote_addr(request):
+    x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
+    if x_forwarded_for:
+        ip = x_forwarded_for.split(',')[0]
+    else:
+        ip = request.META.get('REMOTE_ADDR')
+    return ip

+ 39 - 0
utils/custom_modelviewset.py

@@ -0,0 +1,39 @@
+# coding=utf-8
+
+from rest_framework.viewsets import ModelViewSet
+
+from utils import response_error, response_ok
+from utils.exceptions import CustomError
+
+from django.db import transaction
+
+class CustomModelViewSet(ModelViewSet):
+    def create(self, request, *args, **kwargs):
+        try:
+            with transaction.atomic():
+                super(CustomModelViewSet, self).create(request, *args, **kwargs)
+        except CustomError as e:
+            return response_error(e.get_error_msg())
+        except Exception as e:
+            return response_error(str(e))
+        return response_ok()      
+
+    def update(self, request, *args, **kwargs):
+        try:
+            with transaction.atomic():
+                super(CustomModelViewSet, self).update(request, *args, **kwargs)
+        except CustomError as e:
+            return response_error(e.get_error_msg())
+        except Exception as e:
+            return response_error(str(e))
+        return response_ok()        
+
+    def destroy(self, request, *args, **kwargs):
+        try:
+            with transaction.atomic():
+                super(CustomModelViewSet, self).destroy(request, *args, **kwargs)
+        except CustomError as e:
+            return response_error(e.get_error_msg())
+        except Exception as e:
+            return response_error(str(e))
+        return response_ok()   

+ 31 - 0
utils/exceptions.py

@@ -0,0 +1,31 @@
+#coding=utf-8
+
+class CustomError(Exception):
+    def __init__(self, msg, code=''):
+        """
+
+        :param code: error code
+        :param message: error message
+        :return:
+        """
+        Exception.__init__(self)
+        self.message = msg
+        self.error_code = code
+
+    def __str__(self):
+        return "%s %s" % (
+            self.error_code,
+            self.message,
+        )
+
+    def set_error_code(self, code):
+        self.error_code = code
+
+    def set_error_msg(self, msg):
+        self.message = msg
+
+    def get_error_code(self):
+        return self.error_code
+
+    def get_error_msg(self):
+        return self.message

+ 68 - 0
utils/file_operation.py

@@ -0,0 +1,68 @@
+# coding=utf-8
+
+import os
+import requests
+from django.conf import settings
+from django.utils import timezone
+from django.utils.deconstruct import deconstructible
+
+
+def UploadFile(file, upload_path):
+    upload_path = PathAndRename(upload_path)
+    filename = "%s%s.%s" % (
+        upload_path.path,
+        timezone.now().strftime('%Y%m%d%H%M%S%f'),
+        file.name.split('.')[-1]
+    )
+    filename = filename.lower()
+    full_filename = "%s/%s" % (settings.MEDIA_ROOT, filename)
+    with open(full_filename, 'wb+') as destination:
+        for chunk in file.chunks():
+            destination.write(chunk)
+    return filename
+
+
+def DownloadFace(url, save_path, ext):
+    upload_path = PathAndRename(save_path)
+    filename = "%s%s.%s" % (
+        upload_path.path,
+        timezone.now().strftime('%Y%m%d%H%M%S%f'),
+        ext
+    )
+    filename = filename.lower()
+    full_filename = "%s/%s" % (settings.MEDIA_ROOT, filename)
+    response = requests.get(url)
+    img = response.content
+    with open(full_filename, 'wb+') as destination:
+        destination.write(img)
+    return filename
+
+
+def DeleteFile(filename):
+    img_path = '%s/%s' % (settings.MEDIA_ROOT, filename)
+    try:
+        if os.path.exists(img_path):
+            os.remove(img_path)
+    except:
+        pass
+
+
+@deconstructible
+class PathAndRename(object):
+
+    def __init__(self, sub_path):
+        self.path = sub_path
+
+        self.full_path = "%s/%s" % (settings.MEDIA_ROOT, sub_path)
+        if not os.path.exists(self.full_path):
+            os.makedirs(self.full_path)
+
+    def __call__(self, instance, filename):
+        ext = filename.split('.')[-1]
+        t = timezone.now().strftime('%Y%m%d%H%M%S%f')
+
+        if instance.pk:
+            filename = '{}-{}.{}'.format(instance.pk, t, ext)
+        else:
+            filename = '{}.{}'.format(t, ext)
+        return os.path.join(self.path, filename)

+ 38 - 0
utils/format.py

@@ -0,0 +1,38 @@
+#coding=utf-8
+
+import datetime
+from datetime import timedelta
+
+def strfdate(d):
+    if d:
+        return d.strftime('%Y-%m-%d')
+    else:
+        return ''
+
+def strftime(t):
+    if t:
+        return t.strftime('%Y-%m-%d %H:%M')
+    else:
+        return ''
+
+def strfsecond(second):
+    sec = timedelta(seconds=second)
+    d = datetime.datetime(1,1,1) + sec
+    if d.hour > 0:
+        if d.minute > 0:
+            retval = "%d小时%d分钟" % (d.hour, d.minute)
+        else:
+            retval = "%d小时" % (d.hour)
+    else:
+        retval = "%d分钟" % (d.minute)
+    return retval
+
+
+def clean_datetime_range(data, fieldname):
+    if data is not None and fieldname in data and data[fieldname] != '':
+        t = data[fieldname].split(' - ')
+        data = data.copy()
+        data[fieldname+'_after'] = t[0]
+        data[fieldname+'_before'] = t[1] + ' 23:59:59'
+        data.pop(fieldname)
+    return data

+ 39 - 0
utils/handler.py

@@ -0,0 +1,39 @@
+#coding=utf-8
+
+from rest_framework.views import exception_handler
+from rest_framework import serializers
+from rest_framework.exceptions import AuthenticationFailed,NotAuthenticated
+from jwt.exceptions import ExpiredSignatureError
+
+from .exceptions import CustomError
+from utils import response_error
+
+import traceback
+
+def custom_exception_handler(exc, context):
+    message = ''
+    errcode = None
+    if isinstance(exc, CustomError):
+        message = exc.get_error_msg()
+    elif isinstance(exc, serializers.ValidationError):
+        if isinstance(exc.detail, list):
+            message = exc.detail[0]
+        elif isinstance(exc.detail, dict):
+            for key, value in exc.detail.items():
+                if key != "error":
+                    message += key+"错误:"
+                message += value[0]
+        else:
+            message = list(exc.detail)[0]
+    elif isinstance(exc, AuthenticationFailed) or isinstance(exc, ExpiredSignatureError):
+        message = str(exc)
+        errcode = 460
+    elif isinstance(exc, NotAuthenticated):
+        message = str(exc)
+        errcode = 460
+    else:
+        traceback.print_exc()
+        message = str(exc)
+
+    return response_error(message, errcode=errcode)
+

+ 23 - 0
utils/pagination.py

@@ -0,0 +1,23 @@
+#coding=utf-8
+
+from rest_framework import pagination
+from rest_framework.response import Response
+
+import math
+
+class CustomPagination(pagination.PageNumberPagination):
+    page_query_param = 'page'
+    page_size_query_param = 'limit'
+    page_size = 10
+
+    def get_paginated_response(self, data):
+        ps = self.get_page_size(self.request)
+        return Response({
+            'code':0,
+            'showCount': ps,
+            'totalPage': math.ceil(self.page.paginator.count * 1.0 / ps),
+            'totalResult': self.page.paginator.count,
+            'currentPage': self.page.number,
+            'count': self.page.paginator.count,
+            'data': data
+        })

+ 74 - 0
utils/permission.py

@@ -0,0 +1,74 @@
+# coding=utf-8
+
+from rest_framework import permissions
+
+from apps.customer.models import Customer
+from apps.WechatApplet.models import WechatApplet
+from utils.exceptions import CustomError
+from utils import response_error
+
+class IsCustomerUser(permissions.BasePermission):
+    def has_permission(self, request, view):
+        if not request.user or not request.user.is_authenticated:
+            return False
+        if not request.user.is_customer():
+            return False
+
+        appid = request.GET.get('appid', None)
+        if not appid:
+            appid = request.POST.get('appid')
+        try:
+            app = WechatApplet.getByAppid(appid)
+        except:
+            return False
+        customer = Customer.objects.filter(tenant_id=app.tenant_id, user_id=request.user.id).first()
+        if not customer:
+            return False
+        request.customer = customer
+        return True
+
+class IsTenantUser(permissions.BasePermission):
+    def has_permission(self, request, view):
+        if not request.user or not request.user.is_authenticated:
+            return False
+        return request.user.is_tenant()
+
+class IsAdministratorUser(permissions.BasePermission):
+    def has_permission(self, request, view):
+        if not request.user or not request.user.is_authenticated:
+            return False
+        return request.user.is_administrator()
+
+
+decorator_with_arguments = lambda decorator: lambda *args, **kwargs: lambda func: decorator(func, *args, **kwargs)
+
+@decorator_with_arguments
+def permission_required(function, perm):
+    def _function(viewset, *args, **kwargs):
+        # user_id = viewset.request.META.get('HTTP_USER_ID')
+        # token = viewset.request.META.get('HTTP_Authorization')
+        # if user_id and token:
+        #     try:
+        #         user = User.objects.get(pk=user_id)
+        #     except:
+        #         return ForbiddenJSONResponse()
+
+            # valid = token_generator.check_token(user, token)
+            # if valid:
+            #     request.user = user
+            # viewset.request.user = user
+        # else:
+        #     return ForbiddenJSONResponse()
+
+        if viewset.request.user.has_perm(perm):
+            return function(viewset, *args, **kwargs)
+        else:
+            from django.contrib.auth.models import Permission
+            ps = perm.split('.')
+            try:
+                p = Permission.objects.get(codename=ps[1], content_type__app_label=ps[0])
+            except:
+                raise CustomError(u"权限配置错误!")
+
+            raise CustomError(u"您没有[%s-%s]权限,无法执行该操作,请联系管理员分配权限!" % (p.content_type.name, p.name))
+    return _function

+ 45 - 0
utils/serializersfield.py

@@ -0,0 +1,45 @@
+#coding=utf-8
+from django.utils import timezone
+from rest_framework import serializers
+
+def getAttributeValue(instance, obj):
+    if '.' in instance.source:
+        k = instance.source.split('.')
+        for i in range(len(k)):
+            if hasattr(obj, k[i]):
+                obj = getattr(obj, k[i])
+        val = obj
+    else:
+        val = getattr(obj, instance.source)
+    return val
+
+class BooleanCharField(serializers.BooleanField):
+    def get_attribute(self, obj):
+        return obj
+
+    def to_representation(self, obj):
+        val = getAttributeValue(self, obj)
+        return u'是' if val else u'否'
+
+class DelayTimeCharField(serializers.CharField):
+    def get_attribute(self, obj):
+        return obj
+
+    def to_representation(self, obj):
+        val = getAttributeValue(self, obj)
+        delay = timezone.now() - val
+
+        if delay:
+            if delay.days:
+                delay = u'{}天前'.format(delay.days)
+            elif delay.seconds:
+                delay = delay.seconds / 60
+                if delay < 60:
+                    delay =u'{}分钟前'.format(int(delay))
+                elif delay < 60 * 24:
+                    delay = u'{}小时前'.format(int(delay / 60))
+            else:
+                delay = u''
+        else:
+            delay = u''
+        return delay

+ 144 - 0
utils/wechatpay.py

@@ -0,0 +1,144 @@
+# coding=utf-8
+
+import uuid
+import requests
+import json
+import xmltodict
+import time
+from hashlib import md5
+from django.conf import settings
+
+from utils.exceptions import CustomError
+
+# 微信支付sign_type
+WEIXIN_SIGN_TYPE = 'MD5'
+# 服务器IP地址
+WEIXIN_SPBILL_CREATE_IP = '139.9.148.181'
+# 微信支付用途
+WEIXIN_BODY = u'小程序支付'
+# 微信统一下单URL
+WEIXIN_UNIFIED_ORDER_URL = 'https://api.mch.weixin.qq.com/pay/unifiedorder'
+# 微信查询订单URL
+WEIXIN_QUERY_ORDER_URL = 'https://api.mch.weixin.qq.com/pay/orderquery'
+# 微信支付回调API
+WEIXIN_CALLBACK_API = 'https://xapp.aiche360.cn/api/wechat_notify/'
+
+class WechatPay():
+
+    def __init__(self, appid, mch_id, merchant_key):
+        self.params = {
+            'appid': appid,
+            'mch_id': mch_id,
+            'nonce_str': '',
+            'sign_type': WEIXIN_SIGN_TYPE,
+            'body': WEIXIN_BODY,
+            'out_trade_no': '',
+            'total_fee': '',
+            'spbill_create_ip': WEIXIN_SPBILL_CREATE_IP,
+            'notify_url': WEIXIN_CALLBACK_API + appid + '/',
+            'trade_type': 'JSAPI'
+        }
+        self.prepay_id = None
+        self.merchant_key = merchant_key
+
+    def getAppString(self):
+        data = {
+            'appId': self.params['appid'],
+            'signType': WEIXIN_SIGN_TYPE,
+            'package': "prepay_id={}".format(self.prepay_id),
+            'nonceStr': generate_nonce_str(),
+            'timeStamp': str(int(time.time()))
+        }
+        data['paySign'] = generate_sign(data, self.merchant_key)
+        data.pop('appId')
+        return data
+
+    def unifiedOrder(self,out_trade_no,total_fee, openid):
+        self.params['out_trade_no'] = out_trade_no
+        self.params['total_fee'] = int(round(total_fee / 100,0))
+        self.params['openid'] = openid
+        self.params['nonce_str'] = generate_nonce_str()
+        self.params['sign'] = generate_sign(self.params, self.merchant_key)
+        data = xmltodict.unparse({'xml': self.params}, pretty=True, full_document=False).encode('utf-8')
+
+        headers = {'Content-Type': 'application/xml'}
+        res = requests.post(WEIXIN_UNIFIED_ORDER_URL, data=data, headers=headers)
+
+        if res.status_code != 200:
+            raise CustomError(u'微信请求失败!')
+
+        result = json.loads(json.dumps(xmltodict.parse(res.content)))
+        if result['xml']['return_code'] != 'SUCCESS':
+            raise CustomError(u'微信通信失败![%s]' % result['xml']['return_msg'])
+        if result['xml']['result_code'] != 'SUCCESS':
+            raise CustomError(u'微信交易失败![%s:%s]' % (result['xml']['err_code'],result['xml']['err_code_des']))
+
+        self.prepay_id = result['xml']['prepay_id']
+
+        return result['xml']
+
+class WechatPayNotify():
+    def __init__(self,params, merchant_key):
+        self.params = params
+        self.merchant_key = merchant_key
+
+    def handle(self):
+        resp_dict = json.loads(json.dumps(xmltodict.parse(self.params)))['xml']
+        return_code = resp_dict['return_code']
+
+        if return_code != 'SUCCESS':
+            return None
+
+        if not validate_sign(resp_dict, self.merchant_key):
+            return None
+
+        return resp_dict
+
+    @staticmethod
+    def response_ok():
+        return_info = {
+            'return_code': 'SUCCESS',
+            'return_msg': 'OK'
+        }
+        return generate_response_data(return_info)
+
+    @staticmethod
+    def response_fail():
+        return_info = {
+            'return_code': 'FAIL',
+            'return_msg': 'FAIL'
+        }
+        return generate_response_data(return_info)
+
+def generate_nonce_str():
+    """
+    生成随机字符串
+    """
+    return str(uuid.uuid4()).replace('-', '')
+
+def generate_sign(params, merchant_key):
+    """
+    生成md5签名的参数
+    """
+    if 'sign' in params:
+        params.pop('sign')
+    src = '&'.join(['%s=%s' % (k, v) for k, v in sorted(params.items())]) + '&key=%s' % merchant_key
+    return md5(src.encode('utf-8')).hexdigest().upper()
+
+def validate_sign(resp_dict, merchant_key):
+    """
+    验证微信返回的签名
+    """
+    if 'sign' not in resp_dict:
+        return False
+    wx_sign = resp_dict['sign']
+    sign = generate_sign(resp_dict, merchant_key)
+    if sign == wx_sign:
+        return True
+    return False
+
+def generate_response_data(resp_dict):
+    """
+    字典转xml
+    """
+    return xmltodict.unparse({'xml': resp_dict}, pretty=True, full_document=False).encode('utf-8')

+ 27 - 0
utils/wx/WXBizDataCrypt.py

@@ -0,0 +1,27 @@
+import base64
+import json
+from Crypto.Cipher import AES
+
+
+class WXBizDataCrypt:
+    def __init__(self, appId, sessionKey):
+        self.appId = appId
+        self.sessionKey = sessionKey
+
+    def decrypt(self, encryptedData, iv):
+        # base64 decode
+        sessionKey = base64.b64decode(self.sessionKey)
+        encryptedData = base64.b64decode(encryptedData)
+        iv = base64.b64decode(iv)
+
+        cipher = AES.new(sessionKey, AES.MODE_CBC, iv)
+
+        decrypted = json.loads(self._unpad(cipher.decrypt(encryptedData)))
+
+        if decrypted['watermark']['appid'] != self.appId:
+            raise Exception('Invalid Buffer')
+
+        return decrypted
+
+    def _unpad(self, s):
+        return (s[:-ord(s[len(s)-1:])]).decode('utf-8')

+ 255 - 0
utils/wx/WXBizMsgCrypt.py

@@ -0,0 +1,255 @@
+#!/usr/bin/env python
+#-*- encoding:utf-8 -*-
+
+""" 对公众平台发送给公众账号的消息加解密示例代码.
+@copyright: Copyright (c) 1998-2014 Tencent Inc.
+
+"""
+# ------------------------------------------------------------------------
+
+import base64
+import string
+import random
+import hashlib
+import time
+import struct
+from Crypto.Cipher import AES
+import xml.etree.cElementTree as ET
+import socket
+import utils.wx.ierror as ierror
+
+""" AES加解密用 pycrypto """
+
+class FormatException(Exception):
+    pass
+
+def throw_exception(message, exception_class=FormatException):
+    """my define raise exception function"""
+    raise exception_class(message)
+
+class SHA1:
+    """计算公众平台的消息签名接口"""
+    def getSHA1(self, token, timestamp, nonce, encrypt):
+        """用SHA1算法生成安全签名
+        @param token:  票据
+        @param timestamp: 时间戳
+        @param encrypt: 密文
+        @param nonce: 随机字符串
+        @return: 安全签名
+        """
+        try:
+            token = token.decode()
+            sortlist = [token, timestamp, nonce, encrypt]
+            sortlist.sort()
+            sha = hashlib.sha1()
+            sha.update("".join(sortlist).encode("utf8"))
+            return ierror.WXBizMsgCrypt_OK, sha.hexdigest()
+        except Exception as e:
+            print(e)
+            return ierror.WXBizMsgCrypt_ComputeSignature_Error, None
+
+
+class XMLParse(object):
+    """提供提取消息格式中的密文及生成回复消息格式的接口"""
+
+    # xml消息模板
+    AES_TEXT_RESPONSE_TEMPLATE = """<xml><Encrypt><![CDATA[%(msg_encrypt)s]]></Encrypt><MsgSignature><![CDATA[%(msg_signaturet)s]]></MsgSignature><TimeStamp>%(timestamp)s</TimeStamp><Nonce><![CDATA[%(nonce)s]]></Nonce></xml>"""
+    def extract(self, xmltext):
+        """提取出xml数据包中的加密消息
+        @param xmltext: 待提取的xml字符串
+        @return: 提取出的加密消息字符串
+        """
+        try:
+            xml_tree = ET.fromstring(xmltext)
+            encrypt = xml_tree.find("Encrypt")
+            # touser_name = xml_tree.find("ToUserName")
+            return ierror.WXBizMsgCrypt_OK, encrypt.text
+        except Exception as e:
+            print(e)
+            return ierror.WXBizMsgCrypt_ParseXml_Error, None, None
+
+    def generate(self, encrypt, signature, timestamp, nonce):
+        """生成xml消息
+        @param encrypt: 加密后的消息密文
+        @param signature: 安全签名
+        @param timestamp: 时间戳
+        @param nonce: 随机字符串
+        @return: 生成的xml字符串
+        """
+        resp_dict = {
+            'msg_encrypt': encrypt,
+            'msg_signaturet': signature,
+            'timestamp': timestamp,
+            'nonce': nonce,
+        }
+        resp_xml = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict
+        return resp_xml
+
+
+class PKCS7Encoder(object):
+    """提供基于PKCS7算法的加解密接口"""
+
+    block_size = 32
+
+    def encode(self, text):
+        """ 对需要加密的明文进行填充补位
+        @param text: 需要进行填充补位操作的明文
+        @return: 补齐明文字符串
+        """
+        text_length = len(text)
+        # 计算需要填充的位数
+        amount_to_pad = self.block_size - (text_length % self.block_size)
+        if amount_to_pad == 0:
+            amount_to_pad = self.block_size
+        # 获得补位所用的字符
+        pad = chr(amount_to_pad).encode()
+        return text + pad * amount_to_pad
+
+    def decode(self, decrypted):
+        """删除解密后明文的补位字符
+        @param decrypted: 解密后的明文
+        @return: 删除补位字符后的明文
+        """
+        pad = ord(decrypted[-1])
+        if pad < 1 or pad > 32:
+            pad = 0
+        return decrypted[:-pad]
+
+
+class Prpcrypt(object):
+    """提供接收和推送给公众平台消息的加解密接口"""
+
+    def __init__(self, key):
+        # self.key = base64.b64decode(key+"=")
+        self.key = key
+        # 设置加解密模式为AES的CBC模式
+        self.mode = AES.MODE_CBC
+
+    def encrypt(self, text, appid):
+        """对明文进行加密
+        @param text: 需要加密的明文
+        @return: 加密得到的字符串
+        """
+        # 16位随机字符串添加到明文开头
+        len_str = struct.pack("I", socket.htonl(len(text.encode())))
+        # text = self.get_random_str() + binascii.b2a_hex(len_str).decode() + text + appid
+        text = self.get_random_str() + len_str + text.encode() + appid
+        # 使用自定义的填充方式对明文进行补位填充
+        pkcs7 = PKCS7Encoder()
+        text = pkcs7.encode(text)
+        # 加密
+        cryptor = AES.new(self.key, self.mode, self.key[:16])
+        try:
+            ciphertext = cryptor.encrypt(text)
+            # 使用BASE64对加密后的字符串进行编码
+            return ierror.WXBizMsgCrypt_OK, base64.b64encode(ciphertext).decode('utf8')
+        except Exception as e:
+            return ierror.WXBizMsgCrypt_EncryptAES_Error, None
+
+    def decrypt(self, text, appid):
+        """对解密后的明文进行补位删除
+        @param text: 密文
+        @return: 删除填充补位后的明文
+        """
+        try:
+            cryptor = AES.new(self.key, self.mode, self.key[:16])
+            # 使用BASE64对密文进行解码,然后AES-CBC解密
+            plain_text = cryptor.decrypt(base64.b64decode(text))
+        except Exception as e:
+            print(e)
+            return ierror.WXBizMsgCrypt_DecryptAES_Error, None
+        try:
+            # pad = ord(plain_text[-1])
+            pad = plain_text[-1]
+            # 去掉补位字符串
+            # pkcs7 = PKCS7Encoder()
+            # plain_text = pkcs7.encode(plain_text)
+            # 去除16位随机字符串
+            content = plain_text[16:-pad]
+            xml_len = socket.ntohl(struct.unpack("I", content[: 4])[0])
+            xml_content = content[4: xml_len + 4]
+            from_appid = content[xml_len + 4:]
+        except Exception as e:
+            return ierror.WXBizMsgCrypt_IllegalBuffer, None
+        if from_appid != appid:
+            return ierror.WXBizMsgCrypt_ValidateAppid_Error, None
+        return 0, xml_content.decode()
+
+    def get_random_str(self):
+        """ 随机生成16位字符串
+        @return: 16位字符串
+        """
+        rule = string.ascii_letters + string.digits
+        str = random.sample(rule, 16)
+        return "".join(str).encode()
+
+
+class WXBizMsgCrypt(object):
+    # 构造函数
+    # @param sToken: 公众平台上,开发者设置的Token
+    # @param sEncodingAESKey: 公众平台上,开发者设置的EncodingAESKey
+    # @param sAppId: 企业号的AppId
+    def __init__(self, sToken, sEncodingAESKey, sAppId):
+        try:
+            self.key = base64.b64decode(sEncodingAESKey + "=")
+            assert len(self.key) == 32
+        except Exception:
+            throw_exception("[error]: EncodingAESKey unvalid !", FormatException)
+            # return ierror.WXBizMsgCrypt_IllegalAesKey)
+        self.token = sToken.encode()
+        self.appid = sAppId.encode()
+
+    def EncryptMsg(self, sReplyMsg, sNonce, timestamp=None):
+        # 将公众号回复用户的消息加密打包
+        # @param sReplyMsg: 企业号待回复用户的消息,xml格式的字符串
+        # @param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间
+        # @param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce
+        # sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串,
+        # return:成功0,sEncryptMsg,失败返回对应的错误码None
+        pc = Prpcrypt(self.key)
+        ret, encrypt = pc.encrypt(sReplyMsg, self.appid)
+        if ret != 0:
+            return ret, None
+        if timestamp is None:
+            timestamp = str(int(time.time()))
+        # 生成安全签名
+        sha1 = SHA1()
+        ret, signature = sha1.getSHA1(self.token, timestamp, sNonce, encrypt)
+
+        if ret != 0:
+            return ret, None
+        xmlParse = XMLParse()
+        return ret, xmlParse.generate(encrypt, signature, timestamp, sNonce)
+    def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr):
+        sha1 = SHA1()
+        ret,signature = sha1.getSHA1(self.token, sTimeStamp, sNonce, sEchoStr)
+        if ret  != 0:
+            return ret, None
+        if not signature == sMsgSignature:
+            return ierror.WXBizMsgCrypt_ValidateSignature_Error, None
+        pc = Prpcrypt(self.key)
+        ret,sReplyEchoStr = pc.decrypt(sEchoStr,self.appid)
+        return ret,sReplyEchoStr
+
+    def DecryptMsg(self, sPostData, sMsgSignature, sTimeStamp, sNonce):
+        # 检验消息的真实性,并且获取解密后的明文
+        # @param sMsgSignature: 签名串,对应URL参数的msg_signature
+        # @param sTimeStamp: 时间戳,对应URL参数的timestamp
+        # @param sNonce: 随机串,对应URL参数的nonce
+        # @param sPostData: 密文,对应POST请求的数据
+        #  xml_content: 解密后的原文,当return返回0时有效
+        # @return: 成功0,失败返回对应的错误码
+        # 验证安全签名
+        xmlParse = XMLParse()
+        ret, encrypt = xmlParse.extract(sPostData)
+        if ret != 0:
+            return ret, None
+        sha1 = SHA1()
+        ret, signature = sha1.getSHA1(self.token, sTimeStamp, sNonce, encrypt)
+        if ret != 0:
+            return ret, None
+        if not signature == sMsgSignature:
+            return ierror.WXBizMsgCrypt_ValidateSignature_Error, None
+        pc = Prpcrypt(self.key)
+        ret, xml_content = pc.decrypt(encrypt, self.appid)
+        return ret, xml_content

+ 0 - 0
utils/wx/__init__.py


+ 20 - 0
utils/wx/ierror.py

@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#########################################################################
+# Author: jonyqin
+# Created Time: Thu 11 Sep 2014 01:53:58 PM CST
+# File Name: ierror.py
+# Description:定义错误码含义 
+#########################################################################
+WXBizMsgCrypt_OK = 0
+WXBizMsgCrypt_ValidateSignature_Error = -40001
+WXBizMsgCrypt_ParseXml_Error = -40002
+WXBizMsgCrypt_ComputeSignature_Error = -40003
+WXBizMsgCrypt_IllegalAesKey = -40004
+WXBizMsgCrypt_ValidateAppid_Error = -40005
+WXBizMsgCrypt_EncryptAES_Error = -40006
+WXBizMsgCrypt_DecryptAES_Error = -40007
+WXBizMsgCrypt_IllegalBuffer = -40008
+WXBizMsgCrypt_EncodeBase64_Error = -40009
+WXBizMsgCrypt_DecodeBase64_Error = -40010
+WXBizMsgCrypt_GenReturnXml_Error = -40011

+ 252 - 0
utils/wx/wechat.py

@@ -0,0 +1,252 @@
+# coding=utf-8
+
+import requests
+import json
+from django.conf import settings
+from utils.exceptions import CustomError
+from utils.file_operation import PathAndRename
+
+
+class WeChat(object):
+
+    @staticmethod
+    def getPreAuthCode(component_appid, component_access_token):
+        """
+        第三方平台  获得预授权码
+        结果{"pre_auth_code": "Cx_Dk6qiBE0Dmx4EmlT3oRfArPvwSQ-oa3NL_fwHM7VI08r52wazoZX2Rhpz1dEw", "expires_in": 600}
+        """
+
+        url = 'https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode?component_access_token=' + component_access_token
+        param = {
+            'component_appid': component_appid,
+        }
+        result = requests.post(url, data=json.dumps(param))
+        result = result.json()
+        if 'errcode' in result and result['errcode']:
+            raise CustomError(u'微信请求失败' + str(result['errcode']) + ' :' + result['errmsg'])
+        return result
+
+    @staticmethod
+    def getComponentAccessToken(component_appid, component_appsecret, component_verify_ticket):
+        """第三方平台  获得access_token"""
+
+        url = 'https://api.weixin.qq.com/cgi-bin/component/api_component_token'
+        param = {
+            'component_appid': component_appid,
+            'component_appsecret': component_appsecret,
+            'component_verify_ticket': component_verify_ticket
+        }
+        result = requests.post(url, data=json.dumps(param))
+        result = result.json()
+        if 'errcode' in result and result['errcode']:
+            raise CustomError(u'微信请求失败'+ str(result['errcode']) + ' :' + result['errmsg'])
+        return result
+
+    @staticmethod
+    def getAuthorizationInfo(component_appid, authorization_code, component_access_token):
+        """第三方平台  获得授权信息"""
+
+        url = 'https://api.weixin.qq.com/cgi-bin/component/api_query_auth?component_access_token=' + component_access_token
+        param = {
+            'component_appid': component_appid,
+            'authorization_code': authorization_code,
+        }
+        result = requests.post(url, data=json.dumps(param))
+        result = result.json()
+        if 'errcode' in result and result['errcode']:
+            raise CustomError(u'微信请求失败'+ str(result['errcode']) + ' :' + result['errmsg'])
+        return result
+
+    @staticmethod
+    def getAuthorizerInfo(component_appid, authorizer_appid, component_access_token):
+        """第三方平台 获得授权方信息"""
+
+        url = 'https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_info?component_access_token=' + component_access_token
+        param = {
+            'component_appid': component_appid,
+            'authorizer_appid': authorizer_appid,
+        }
+        result = requests.post(url, data=json.dumps(param))
+        result = result.json()
+        if 'errcode' in result and result['errcode']:
+            raise CustomError(u'微信请求失败'+ str(result['errcode']) + ' :' + result['errmsg'])
+        return result
+
+    @staticmethod
+    def getAuthorizerAccessToken(component_appid, authorizer_appid, authorizer_refresh_token, component_access_token):
+        """第三方平台 获得授权方 access_token"""
+
+        url = 'https://api.weixin.qq.com/cgi-bin/component/api_authorizer_token?component_access_token=' + component_access_token
+        param = {
+            'component_appid': component_appid,
+            'authorizer_appid': authorizer_appid,
+            'authorizer_refresh_token': authorizer_refresh_token
+        }
+        result = requests.post(url, data=json.dumps(param))
+        result = result.json()
+        if 'errcode' in result and result['errcode']:
+            raise CustomError(u'微信请求失败'+ str(result['errcode']) + ' :' + result['errmsg'])
+        return result
+
+    @staticmethod
+    def getCodeTemplateList(component_access_token):
+        '''获取代码模版列表'''
+
+        url = 'https://api.weixin.qq.com/wxa/gettemplatelist?access_token=' + component_access_token
+        result = requests.get(url)
+        result = result.json()
+        if 'errcode' in result and result['errcode']:
+            raise CustomError(u'微信请求失败'+ str(result['errcode']) + ' :' + result['errmsg'])
+        return result['template_list']
+
+    @staticmethod
+    def commitCode(access_token, template_id, user_version, user_desc):
+        '''小程序提交代码'''
+        url = 'https://api.weixin.qq.com/wxa/commit?access_token=' + access_token
+
+        param = {
+            'template_id': template_id,
+            'ext_json': "{\"extEnable\": false,\"extAppid\": \"\"}",
+            'user_version': user_version,
+            'user_desc': 'test'
+        }
+
+        result = requests.post(url, data=json.dumps(param))
+        result = result.json()
+        if 'errcode' in result and result['errcode']:
+            raise CustomError(u'微信请求失败'+ str(result['errcode']) + ' :' + result['errmsg'])
+        return result
+
+    @staticmethod
+    def submitAuditCode(access_token):
+        '''将上传的代码提交审核'''
+
+        url = 'https://api.weixin.qq.com/wxa/submit_audit?access_token=' + access_token
+
+        result = requests.post(url)
+        result = result.json()
+        if 'errcode' in result and result['errcode']:
+            raise CustomError(u'微信请求失败' + str(result['errcode']) + ' :' + result['errmsg'])
+        return result
+
+    @staticmethod
+    def getLastSubmitAuditCodeStatus(access_token):
+        '''查询最新一次提交审核代码的审核状态'''
+
+        url = 'https://api.weixin.qq.com/wxa/get_latest_auditstatus?access_token=' + access_token
+        result = requests.get(url)
+        result = result.json()
+        if 'errcode' in result and result['errcode']:
+            raise CustomError(u'微信请求失败' + str(result['errcode']) + ' :' + result['errmsg'])
+        return result
+
+    @staticmethod
+    def modify_domain(access_token, action, requestdomain, wsrequestdomain, uploaddomain, downloaddomain):
+        '''设置小程序服务器域名'''
+        url = 'https://api.weixin.qq.com/wxa/modify_domain?access_token=' + access_token
+
+        param = {
+            "action": action,
+            "requestdomain": requestdomain,
+            "wsrequestdomain": wsrequestdomain,
+            "uploaddomain": uploaddomain,
+            "downloaddomain": downloaddomain
+        }
+        result = requests.post(url, data=json.dumps(param))
+        result = result.json()
+        if 'errcode' in result and result['errcode']:
+            raise CustomError(u'微信请求失败' + str(result['errcode']) + ' :' + result['errmsg'])
+        return result
+
+    @staticmethod
+    def code2Session(appid, secret, code):
+        '''登录凭证校验'''
+        url = 'https://api.weixin.qq.com/sns/jscode2session?appid='+ appid + '&secret=' + secret + '&js_code=' + code + '&grant_type=authorization_code'
+        result = requests.get(url)
+        result = result.json()
+        if 'errcode' in result and result['errcode']:
+            raise CustomError(u'微信请求失败' + str(result['errcode']) + ':' + result['errmsg'])
+        return result
+
+    @staticmethod
+    def addPlugin(access_token):
+        '''小程序插件管理'''
+        url = 'https://api.weixin.qq.com/wxa/plugin?access_token=' + access_token
+
+        param = {
+            'action': 'apply',
+            'plugin_appid': 'wxfa43a4a7041a84de'
+        }
+
+        result = requests.post(url, data=json.dumps(param))
+        result = result.json()
+        if 'errcode' in result and result['errcode'] and str(result['errcode']) != '89237':
+            raise CustomError(u'微信请求失败' + str(result['errcode']) + ':' + result['errmsg'])
+        return result
+
+    @staticmethod
+    def getTemplateList(access_token):
+        '''获取当前帐号下的个人模板列表'''
+        url = 'https://api.weixin.qq.com/wxaapi/newtmpl/gettemplate?access_token=' + access_token
+        result = requests.get(url)
+        result = result.json()
+        if 'errcode' in result and result['errcode']:
+            raise CustomError(u'微信请求失败' + str(result['errcode']) + ':' + result['errmsg'])
+        return result['data']
+
+    @staticmethod
+    def sendSubscribeMessage(access_token, openid, template_id, page, data):
+        '''发送订阅消息'''
+        url = 'https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=' + access_token
+
+        param = {
+            'touser': openid,
+            'template_id': template_id,
+            'page': page,
+            'data': data
+        }
+        result = requests.post(url, data=json.dumps(param))
+        #print('-------------------------')
+        #result = result.json()
+        #print(result)
+
+        #result = requests.post(url, data=json.dumps(param))
+        #result = result.json()
+        #if 'errcode' in result and result['errcode'] and str(result['errcode']) != '89237':
+        #    raise CustomError(u'微信请求失败' + str(result['errcode']) + ':' + result['errmsg'])
+        #return result
+
+    @staticmethod
+    def releaseCode(access_token):
+        '''已审核代码发布'''
+
+        url = 'https://api.weixin.qq.com/wxa/release?access_token=' + access_token
+
+        result = requests.post(url, data=json.dumps({}))
+        result = result.json()
+        if 'errcode' in result and result['errcode']:
+            raise CustomError(u'微信请求失败' + str(result['errcode']) + ' :' + result['errmsg'])
+        return result
+
+    @staticmethod
+    def getWXAppCode(access_token, page, activity_id):
+        '''获取小程序二维码'''
+        url = 'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={}'.format(access_token)
+        data = {"scene": "id=%s" % activity_id,
+                # "width": 1280, # 默认430
+                "page": page,
+                "line_color": {"r": 43, "g": 162, "b": 69},  # 自定义颜色
+                "is_hyaline": True}
+        result = requests.post(url, json=data)
+        # print('-------------------------',result)
+        upload_path = PathAndRename("activity/")
+        filename = "%s%s.png" % (
+            upload_path.path,
+            activity_id,
+        )
+        filename = filename.lower()
+        full_filename = "%s%s" % (settings.MEDIA_ROOT, filename)
+        with open(full_filename, 'wb') as destination:
+            destination.write(result.content)
+
+        return filename

+ 0 - 0
utils/xgj/__init__.py


+ 155 - 0
utils/xgj/xgj.py

@@ -0,0 +1,155 @@
+# coding=utf-8
+
+import requests
+import json
+from django.utils import timezone
+from django.conf import settings
+from hashlib import md5
+from utils.exceptions import CustomError
+from apps.WechatApplet.models import WechatApplet
+
+def gender_sign(gateway_key):
+    ts = timezone.now().strftime('%Y%m%d%H%M%S')
+    token = gateway_key + ts
+
+    m = md5()
+    m.update(token.encode("utf8"))
+    sign = m.hexdigest()
+    return ts, sign
+
+
+def get_xgj_params(appid):
+    applet = WechatApplet.getByAppid(appid)
+    tenant = applet.tenant
+    if not tenant.xgj_address:
+        raise CustomError(u'小程序设置错误!')
+    if not tenant.xgj_gateway_key:
+        raise CustomError(u'小程序设置错误!')
+
+    ts, sign = gender_sign(tenant.xgj_gateway_key)
+
+    return tenant.xgj_address, ts, sign
+
+
+class XGJ(object):
+
+    @staticmethod
+    def get_company(appid):
+        xgj_addr, ts, sign = get_xgj_params(appid)
+        url = xgj_addr + 'wechat/applet/member/company/?ts=' + ts + '&sign=' + sign
+        param = {}
+        result = requests.post(url=url, data=json.dumps(param))
+        result = result.json()
+
+        if not result['success']:
+            if 'errors' in result:
+                raise CustomError(u'请求失败--' + str(result['errors']))
+            else:
+                raise CustomError(u'请求失败')
+        return result
+
+    @staticmethod
+    def get_member_card(appid, company_id, tel, number):
+        xgj_addr, ts, sign = get_xgj_params(appid)
+        url = xgj_addr + 'wechat/applet/member/select/?ts=' + ts + '&sign=' + sign
+        param = {
+            'company_id': company_id,
+            'tel': tel,
+            'number': number
+        }
+        result = requests.post(url=url, data=json.dumps(param))
+        result = result.json()
+
+        if not result['success']:
+            if 'errors' in result:
+                raise CustomError(u'请求失败--' + str(result['errors']))
+            else:
+                raise CustomError(u'请求失败')
+        return result
+
+    @staticmethod
+    def get_member_info(appid, member_id):
+        xgj_addr, ts, sign = get_xgj_params(appid)
+        url = xgj_addr + 'wechat/applet/member/seacrh/?ts=' + ts + '&sign=' + sign + '&id=' + member_id
+
+        result = requests.get(url)
+        result = result.json()
+
+        if not result['success']:
+            if 'errors' in result:
+                raise CustomError(u'请求失败--' + str(result['errors']))
+            else:
+                raise CustomError(u'请求失败')
+        return result
+
+    @staticmethod
+    def get_member_points(appid, member_id, pageNum, pageSize):
+        xgj_addr, ts, sign = get_xgj_params(appid)
+        url = xgj_addr + 'wechat/applet/member/points/?ts=' + ts + '&sign=' + sign + '&id=' + member_id + '&page=' + pageNum + '&rows=' + pageSize
+
+        result = requests.get(url)
+        result = result.json()
+
+        if not result['success']:
+            if 'errors' in result:
+                raise CustomError(u'请求失败--' + str(result['errors']))
+            else:
+                raise CustomError(u'请求失败')
+        return result
+
+    @staticmethod
+    def get_member_record(appid, member_id, pageNum, pageSize):
+        xgj_addr, ts, sign = get_xgj_params(appid)
+        url = xgj_addr + 'wechat/applet/member/consume/?ts=' + ts + '&sign=' + sign + '&id=' + member_id + '&page=' + pageNum + '&rows=' + pageSize
+
+        result = requests.get(url)
+        result = result.json()
+
+        if not result['success']:
+            if 'errors' in result:
+                raise CustomError(u'请求失败--' + str(result['errors']))
+            else:
+                raise CustomError(u'请求失败')
+        return result
+
+    @staticmethod
+    def get_member_balance(appid, member_id):
+        xgj_addr, ts, sign = get_xgj_params(appid)
+        url = xgj_addr + 'wechat/applet/member/balance/?ts=' + ts + '&sign=' + sign + '&member_id=' + member_id
+
+        result = requests.get(url)
+        result = result.json()
+
+        if not result['success']:
+            if 'errors' in result:
+                raise CustomError(u'请求失败--' + str(result['errors']))
+            else:
+                raise CustomError(u'请求失败')
+        return result
+
+    @staticmethod
+    def refresh_member_card(tenant, member_id, amount, points, discount_amount):
+        if not tenant.xgj_gateway_key:
+            raise CustomError(u'小程序设置错误')
+        if not tenant.xgj_address:
+            raise CustomError(u'小程序设置错误')
+
+        ts, sign = gender_sign(tenant.xgj_gateway_key)
+        url = tenant.xgj_address + 'wechat/applet/member/use/?ts=' + ts + '&sign=' + sign + '&member_id=' + member_id
+
+        param = {
+            'amount': amount,
+            'points': points,
+            'discount_amount': discount_amount
+        }
+        result = requests.post(url=url, data=json.dumps(param))
+        result = result.json()
+
+        res = ''
+
+        if not result['success']:
+            if 'errors' in result:
+                res = u'请求失败--' + str(result['errors'])
+            else:
+                res = u'请求失败'
+        return res