ine="0">0   utils/redis/connect.py
  • 68 0
      utils/redis/rkeys.py
  • 7 0
      utils/url_utils.py
  • + 29 - 0
    .editorconfig

    @@ -0,0 +1,29 @@
    1
    +# EditorConfig is awesome: http://EditorConfig.org
    2
    +
    3
    +# top-most EditorConfig file
    4
    +root = true
    5
    +
    6
    +# Unix-style newlines with a newline ending every file
    7
    +[*]
    8
    +end_of_line = lf
    9
    +insert_final_newline = false
    10
    +
    11
    +# 4 space indentation
    12
    +[*.py]
    13
    +indent_style = space
    14
    +indent_size = 4
    15
    +
    16
    +# Tab indentation (no size specified)
    17
    +[*.js]
    18
    +indent_style = space
    19
    +indent_size = 4
    20
    +
    21
    +# Tab indentation (no size specified)
    22
    +[*.html]
    23
    +indent_style = space
    24
    +indent_size = 4
    25
    +
    26
    +# Matches the exact files either package.json or .travis.yml
    27
    +[{package.json,.travis.yml}]
    28
    +indent_style = space
    29
    +indent_size = 2

    + 66 - 0
    .gitignore

    @@ -0,0 +1,66 @@
    1
    +# Byte-compiled / optimized / DLL files
    2
    +__pycache__/
    3
    +*.py[cod]
    4
    +*.swp
    5
    +# C extensions
    6
    +*.so
    7
    +
    8
    +# Distribution / packaging
    9
    +bin/
    10
    +build/
    11
    +develop-eggs/
    12
    +dist/
    13
    +eggs/
    14
    +lib/
    15
    +lib64/
    16
    +parts/
    17
    +sdist/
    18
    +venv/
    19
    +var/
    20
    +static/upload/
    21
    +*.egg-info/
    22
    +.installed.cfg
    23
    +*.egg
    24
    +*.sublime*
    25
    +
    26
    +# Installer logs
    27
    +pip-log.txt
    28
    +pip-delete-this-directory.txt
    29
    +
    30
    +# Unit test / coverage reports
    31
    +.tox/
    32
    +.coverage
    33
    +.cache
    34
    +nosetests.xml
    35
    +coverage.xml
    36
    +
    37
    +# Translations
    38
    +# *.mo
    39
    +
    40
    +# Mr Developer
    41
    +.mr.developer.cfg
    42
    +.project
    43
    +.pydevproject
    44
    +.settings
    45
    +# Rope
    46
    +.ropeproject
    47
    +
    48
    +# Django stuff:
    49
    +*.log
    50
    +*.pot
    51
    +
    52
    +# Sphinx documentation
    53
    +docs/_build/
    54
    +
    55
    +
    56
    +# Ignore For zhTimer
    57
    +.DS_Store
    58
    +db.sqlite3
    59
    +local_settings.py
    60
    +
    61
    +.idea/
    62
    +media/
    63
    +collect_static/
    64
    +
    65
    +# Special File
    66
    +*download.html

    + 8 - 0
    .isort.cfg

    @@ -0,0 +1,8 @@
    1
    +# See the menu of settings available here:
    2
    +#   https://github.com/timothycrosley/isort/wiki/isort-Settings
    3
    +
    4
    +[settings]
    5
    +indent='    '
    6
    +line_length=120
    7
    +lines_after_imports=2
    8
    +skip=migrations

    + 0 - 0
    account/__init__.py


    + 15 - 0
    account/admin.py

    @@ -0,0 +1,15 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +
    3
    +from djadmin import ReadonlyModelAdmin
    4
    +from django.contrib import admin
    5
    +
    6
    +from account.models import UserInfo
    7
    +
    8
    +
    9
    +class UserInfoAdmin(ReadonlyModelAdmin, admin.ModelAdmin):
    10
    +    list_display = ('user_id', 'unionid', 'openid', 'name', 'sex', 'nickname', 'phone', 'country', 'province', 'city', 'location', 'status', 'created_at', 'updated_at')
    11
    +    list_filter = ('sex', 'status')
    12
    +    actions = None
    13
    +
    14
    +
    15
    +admin.site.register(UserInfo, UserInfoAdmin)

    + 43 - 0
    account/migrations/0001_initial.py

    @@ -0,0 +1,43 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +# Generated by Django 1.11.3 on 2017-09-24 14:50
    3
    +from __future__ import unicode_literals
    4
    +
    5
    +from django.db import migrations, models
    6
    +import shortuuidfield.fields
    7
    +
    8
    +
    9
    +class Migration(migrations.Migration):
    10
    +
    11
    +    initial = True
    12
    +
    13
    +    dependencies = [
    14
    +    ]
    15
    +
    16
    +    operations = [
    17
    +        migrations.CreateModel(
    18
    +            name='UserInfo',
    19
    +            fields=[
    20
    +                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
    21
    +                ('status', models.BooleanField(db_index=True, default=True, help_text='\u72b6\u6001', verbose_name='status')),
    22
    +                ('created_at', models.DateTimeField(auto_now_add=True, help_text='\u521b\u5efa\u65f6\u95f4', verbose_name='created_at')),
    23
    +                ('updated_at', models.DateTimeField(auto_now=True, help_text='\u66f4\u65b0\u65f6\u95f4', verbose_name='updated_at')),
    24
    +                ('user_id', shortuuidfield.fields.ShortUUIDField(blank=True, db_index=True, editable=False, help_text='\u7528\u6237\u552f\u4e00\u6807\u8bc6', max_length=22, unique=True)),
    25
    +                ('unionid', models.CharField(blank=True, db_index=True, help_text='\u5fae\u4fe1 Unionid', max_length=255, null=True, unique=True, verbose_name='unionid')),
    26
    +                ('openid', models.CharField(blank=True, db_index=True, help_text='\u5fae\u4fe1 Openid', max_length=255, null=True, unique=True, verbose_name='openid')),
    27
    +                ('name', models.CharField(blank=True, help_text='\u7528\u6237\u59d3\u540d', max_length=255, null=True, verbose_name='name')),
    28
    +                ('sex', models.IntegerField(choices=[(1, '\u7537'), (0, '\u5973')], default=1, help_text='\u7528\u6237\u6027\u522b', verbose_name='sex')),
    29
    +                ('nickname', models.CharField(blank=True, help_text='\u7528\u6237\u6635\u79f0', max_length=255, null=True, verbose_name='nickname')),
    30
    +                ('avatar', models.CharField(blank=True, help_text='\u7528\u6237\u5934\u50cf', max_length=255, null=True, verbose_name='avatar')),
    31
    +                ('phone', models.CharField(blank=True, db_index=True, help_text='\u7528\u6237\u7535\u8bdd', max_length=255, null=True, unique=True, verbose_name='phone')),
    32
    +                ('country', models.CharField(blank=True, help_text='\u7528\u6237\u56fd\u5bb6', max_length=255, null=True, verbose_name='country')),
    33
    +                ('province', models.CharField(blank=True, help_text='\u7528\u6237\u7701\u4efd', max_length=255, null=True, verbose_name='province')),
    34
    +                ('city', models.CharField(blank=True, help_text='\u7528\u6237\u57ce\u5e02', max_length=255, null=True, verbose_name='city')),
    35
    +                ('location', models.CharField(blank=True, help_text='\u7528\u6237\u5730\u5740', max_length=255, null=True, verbose_name='location')),
    36
    +                ('user_status', models.IntegerField(choices=[(0, '\u672a\u9a8c\u8bc1'), (1, '\u5df2\u6fc0\u6d3b'), (2, '\u5df2\u7981\u7528'), (3, '\u5df2\u5220\u9664')], default=0, verbose_name='user_status')),
    37
    +            ],
    38
    +            options={
    39
    +                'verbose_name': 'userinfo',
    40
    +                'verbose_name_plural': 'userinfo',
    41
    +            },
    42
    +        ),
    43
    +    ]

    + 0 - 0
    account/migrations/__init__.py


    + 62 - 0
    account/models.py

    @@ -0,0 +1,62 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +
    3
    +from django.db import models
    4
    +from django.utils.translation import ugettext_lazy as _
    5
    +from shortuuidfield import ShortUUIDField
    6
    +
    7
    +from course.basemodels import CreateUpdateMixin
    8
    +
    9
    +
    10
    +class UserInfo(CreateUpdateMixin):
    11
    +    UNVERIFIED = 0
    12
    +    ACTIVATED = 1
    13
    +    DISABLED = 2
    14
    +    DELETED = 3
    15
    +
    16
    +    USER_STATUS = (
    17
    +        (UNVERIFIED, u'未验证'),
    18
    +        (ACTIVATED, u'已激活'),
    19
    +        (DISABLED, u'已禁用'),
    20
    +        (DELETED, u'已删除'),
    21
    +    )
    22
    +
    23
    +    MALE = 1
    24
    +    FEMALE = 0
    25
    +
    26
    +    SEX_TYPE = (
    27
    +        (MALE, u'男'),
    28
    +        (FEMALE, u'女'),
    29
    +    )
    30
    +
    31
    +    user_id = ShortUUIDField(_(u'user_id'), max_length=255, help_text=u'用户唯一标识', db_index=True, unique=True)
    32
    +
    33
    +    # 微信授权用户
    34
    +    unionid = models.CharField(_(u'unionid'), max_length=255, blank=True, null=True, help_text=u'微信 Unionid', db_index=True, unique=True)
    35
    +    openid = models.CharField(_(u'openid'), max_length=255, blank=True, null=True, help_text=u'微信 Openid', db_index=True, unique=True)
    36
    +    # 用户基本信息
    37
    +    name = models.CharField(_(u'name'), max_length=255, blank=True, null=True, help_text=u'用户姓名')
    38
    +    sex = models.IntegerField(_(u'sex'), choices=SEX_TYPE, default=MALE, help_text=u'用户性别')
    39
    +    nickname = models.CharField(_(u'nickname'), max_length=255, blank=True, null=True, help_text=u'用户昵称')
    40
    +    avatar = models.CharField(_(u'avatar'), max_length=255, blank=True, null=True, help_text=u'用户头像')
    41
    +    phone = models.CharField(_(u'phone'), max_length=255, blank=True, null=True, help_text=u'用户电话', db_index=True, unique=True)
    42
    +    country = models.CharField(_(u'country'), max_length=255, blank=True, null=True, help_text=u'用户国家')
    43
    +    province = models.CharField(_(u'province'), max_length=255, blank=True, null=True, help_text=u'用户省份')
    44
    +    city = models.CharField(_(u'city'), max_length=255, blank=True, null=True, help_text=u'用户城市')
    45
    +    location = models.CharField(_(u'location'), max_length=255, blank=True, null=True, help_text=u'用户地址')
    46
    +
    47
    +    user_status = models.IntegerField(_(u'user_status'), choices=USER_STATUS, default=UNVERIFIED)
    48
    +
    49
    +    class Meta:
    50
    +        verbose_name = _(u'userinfo')
    51
    +        verbose_name_plural = _(u'userinfo')
    52
    +
    53
    +    def __unicode__(self):
    54
    +        return unicode(self.pk)
    55
    +
    56
    +    @property
    57
    +    def data(self):
    58
    +        return {
    59
    +            'user_id': self.user_id,
    60
    +            'nickname': self.nickname,
    61
    +            'avatar': self.avatar,
    62
    +        }

    + 4 - 0
    account/tests.py

    @@ -0,0 +1,4 @@
    1
    +from django.test import TestCase
    2
    +
    3
    +
    4
    +# Create your tests here.

    + 4 - 0
    account/views.py

    @@ -0,0 +1,4 @@
    1
    +from django.shortcuts import render
    2
    +
    3
    +
    4
    +# Create your views here.

    + 0 - 0
    api/__init__.py


    + 4 - 0
    api/admin.py

    @@ -0,0 +1,4 @@
    1
    +from django.contrib import admin
    2
    +
    3
    +
    4
    +# Register your models here.

    + 0 - 0
    api/migrations/__init__.py


    + 4 - 0
    api/models.py

    @@ -0,0 +1,4 @@
    1
    +from django.db import models
    2
    +
    3
    +
    4
    +# Create your models here.

    + 35 - 0
    api/oauth_views.py

    @@ -0,0 +1,35 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +
    3
    +from __future__ import division
    4
    +
    5
    +from django.conf import settings
    6
    +from django.core.urlresolvers import reverse
    7
    +from django.db import transaction
    8
    +from django.shortcuts import redirect
    9
    +from furl import furl
    10
    +from logit import logit
    11
    +
    12
    +from account.models import UserInfo
    13
    +from utils.redis.connect import r
    14
    +
    15
    +
    16
    +@logit
    17
    +@transaction.atomic
    18
    +def oauth_redirect(request):
    19
    +    unique_identifier = request.GET.get(settings.WECHAT_UNIQUE_IDENTIFICATION, '')
    20
    +
    21
    +    user, created = UserInfo.objects.select_for_update().get_or_create(**{settings.WECHAT_UNIQUE_IDENTIFICATION: unique_identifier})
    22
    +    user.unionid = request.GET.get('unionid', '')
    23
    +    user.openid = request.GET.get('openid', '')
    24
    +    user.nickname = request.GET.get('nickname', '')
    25
    +    user.avatar = request.GET.get('headimgurl', '')
    26
    +    user.save()
    27
    +
    28
    +    token_check_key = user.user_id
    29
    +
    30
    +    query_params = {
    31
    +        settings.TOKEN_CHECK_KEY: token_check_key,
    32
    +        'vtoken': r.token(token_check_key, ex=False, buf=False),
    33
    +    }
    34
    +
    35
    +    return redirect(furl(reverse('page:user_oauth')).add(request.GET).add(query_params).url)

    + 4 - 0
    api/tests.py

    @@ -0,0 +1,4 @@
    1
    +from django.test import TestCase
    2
    +
    3
    +
    4
    +# Create your tests here.

    + 16 - 0
    api/urls.py

    @@ -0,0 +1,16 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +
    3
    +from django.conf.urls import url
    4
    +
    5
    +from api import oauth_views
    6
    +from page import code_views, info_views, list_views
    7
    +
    8
    +
    9
    +urlpatterns = [
    10
    +    url(r'^code/exchange$', code_views.code_exchange, name='code_exchange'),
    11
    +]
    12
    +
    13
    +urlpatterns += [
    14
    +    url(r'^3rd/or$', oauth_views.oauth_redirect, name='3rd_or'),
    15
    +    url(r'^3rd/oauth_redirect$', oauth_views.oauth_redirect, name='3rd_oauth_redirect'),
    16
    +]

    + 4 - 0
    api/views.py

    @@ -0,0 +1,4 @@
    1
    +from django.shortcuts import render
    2
    +
    3
    +
    4
    +# Create your views here.

    + 9 - 0
    check.sh

    @@ -0,0 +1,9 @@
    1
    +#!/bin/bash
    2
    +
    3
    +echo '>> iSort'
    4
    +./isort.sh
    5
    +echo
    6
    +
    7
    +echo '>> PEP8'
    8
    +./pep8.sh
    9
    +echo

    + 0 - 0
    codes/__init__.py


    + 14 - 0
    codes/admin.py

    @@ -0,0 +1,14 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +
    3
    +from djadmin import ExportExcelModelAdmin, ReadonlyModelAdmin
    4
    +from django.contrib import admin
    5
    +
    6
    +from codes.models import CourseCodeInfo
    7
    +
    8
    +
    9
    +class CourseCodeInfoAdmin(ExportExcelModelAdmin, admin.ModelAdmin):
    10
    +    list_display = ('code', 'exchanged', 'user_id', 'status', 'created_at', 'updated_at')
    11
    +    list_filter = ('exchanged', 'status')
    12
    +
    13
    +
    14
    +admin.site.register(CourseCodeInfo, CourseCodeInfoAdmin)

    + 8 - 0
    codes/apps.py

    @@ -0,0 +1,8 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +from __future__ import unicode_literals
    3
    +
    4
    +from django.apps import AppConfig
    5
    +
    6
    +
    7
    +class CodesConfig(AppConfig):
    8
    +    name = 'codes'

    + 32 - 0
    codes/migrations/0001_initial.py

    @@ -0,0 +1,32 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +# Generated by Django 1.11.3 on 2017-09-24 14:50
    3
    +from __future__ import unicode_literals
    4
    +
    5
    +from django.db import migrations, models
    6
    +
    7
    +
    8
    +class Migration(migrations.Migration):
    9
    +
    10
    +    initial = True
    11
    +
    12
    +    dependencies = [
    13
    +    ]
    14
    +
    15
    +    operations = [
    16
    +        migrations.CreateModel(
    17
    +            name='CourseCodeInfo',
    18
    +            fields=[
    19
    +                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
    20
    +                ('status', models.BooleanField(db_index=True, default=True, help_text='\u72b6\u6001', verbose_name='status')),
    21
    +                ('created_at', models.DateTimeField(auto_now_add=True, help_text='\u521b\u5efa\u65f6\u95f4', verbose_name='created_at')),
    22
    +                ('updated_at', models.DateTimeField(auto_now=True, help_text='\u66f4\u65b0\u65f6\u95f4', verbose_name='updated_at')),
    23
    +                ('code', models.CharField(blank=True, db_index=True, help_text='\u5151\u6362\u7801', max_length=255, null=True, verbose_name='code')),
    24
    +                ('exchanged', models.BooleanField(db_index=True, default=False, help_text='\u5151\u6362\u72b6\u6001', verbose_name='exchanged')),
    25
    +                ('user_id', models.CharField(blank=True, db_index=True, help_text='\u7528\u6237\u552f\u4e00\u6807\u8bc6', max_length=255, null=True, unique=True, verbose_name='user_id')),
    26
    +            ],
    27
    +            options={
    28
    +                'verbose_name': 'coursecodeinfo',
    29
    +                'verbose_name_plural': 'coursecodeinfo',
    30
    +            },
    31
    +        ),
    32
    +    ]

    + 0 - 0
    codes/migrations/__init__.py


    + 20 - 0
    codes/models.py

    @@ -0,0 +1,20 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +
    3
    +from django.db import models
    4
    +from django.utils.translation import ugettext_lazy as _
    5
    +
    6
    +from course.basemodels import CreateUpdateMixin
    7
    +
    8
    +
    9
    +class CourseCodeInfo(CreateUpdateMixin):
    10
    +    code = models.CharField(_(u'code'), max_length=255, blank=True, null=True, help_text=u'兑换码', db_index=True)
    11
    +    exchanged = models.BooleanField(_(u'exchanged'), default=False, help_text=_(u'兑换状态'), db_index=True)
    12
    +
    13
    +    user_id = models.CharField(_(u'user_id'), max_length=255, blank=True, null=True, help_text=u'用户唯一标识', db_index=True, unique=True)
    14
    +
    15
    +    class Meta:
    16
    +        verbose_name = _(u'coursecodeinfo')
    17
    +        verbose_name_plural = _(u'coursecodeinfo')
    18
    +
    19
    +    def __unicode__(self):
    20
    +        return unicode(self.pk)

    + 7 - 0
    codes/tests.py

    @@ -0,0 +1,7 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +from __future__ import unicode_literals
    3
    +
    4
    +from django.test import TestCase
    5
    +
    6
    +
    7
    +# Create your tests here.

    + 7 - 0
    codes/views.py

    @@ -0,0 +1,7 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +from __future__ import unicode_literals
    3
    +
    4
    +from django.shortcuts import render
    5
    +
    6
    +
    7
    +# Create your views here.

    + 0 - 0
    course/__init__.py


    + 26 - 0
    course/basemodels.py

    @@ -0,0 +1,26 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +
    3
    +from django.db import models
    4
    +from django.utils.translation import ugettext_lazy as _
    5
    +
    6
    +
    7
    +class CreateUpdateMixin(models.Model):
    8
    +    status = models.BooleanField(_(u'status'), default=True, help_text=_(u'状态'), db_index=True)
    9
    +    created_at = models.DateTimeField(_(u'created_at'), auto_now_add=True, editable=True, help_text=_(u'创建时间'))
    10
    +    updated_at = models.DateTimeField(_(u'updated_at'), auto_now=True, editable=True, help_text=_(u'更新时间'))
    11
    +
    12
    +    class Meta:
    13
    +        abstract = True
    14
    +
    15
    +
    16
    +class SexChoicesMixin(models.Model):
    17
    +    MALE = 1
    18
    +    FEMALE = 0
    19
    +
    20
    +    SEX_TYPE = (
    21
    +        (MALE, u'男'),
    22
    +        (FEMALE, u'女'),
    23
    +    )
    24
    +
    25
    +    class Meta:
    26
    +        abstract = True

    + 36 - 0
    course/decorators.py

    @@ -0,0 +1,36 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +
    3
    +from functools import wraps
    4
    +
    5
    +from django.conf import settings
    6
    +from django.shortcuts import redirect, render
    7
    +from furl import furl
    8
    +from pywe_oauth import get_oauth_redirect_url
    9
    +
    10
    +from utils.redis.connect import r
    11
    +
    12
    +
    13
    +def check_token(func=None):
    14
    +    def decorator(func):
    15
    +        @wraps(func)
    16
    +        def returned_wrapper(request, *args, **kwargs):
    17
    +            vtoken = request.GET.get('vtoken', '') or request.POST.get('vtoken', '')
    18
    +            if not settings.DEBUG:
    19
    +                if not request.wechat:
    20
    +                    return render(request, 'django_we/errmsg.html', {'title': '错误', 'errmsg': '请在微信中打开'})
    21
    +                token_check_key = request.GET.get(settings.TOKEN_CHECK_KEY, '') or request.POST.get(settings.TOKEN_CHECK_KEY, '')
    22
    +                if not r.token_exists(token_check_key, vtoken):
    23
    +                    # 3rd OAuth
    24
    +                    return redirect(settings.WECHAT_OAUTH2_REDIRECT_URL)
    25
    +                    # Current OAuth
    26
    +                    redirect_url = furl(settings.WECHAT_OAUTH2_REDIRECT_ENTRY).add({}).url
    27
    +                    return redirect(get_oauth_redirect_url(settings.WECHAT_OAUTH2_REDIRECT_URI, 'snsapi_userinfo', redirect_url))
    28
    +            return func(request, *args, **kwargs)
    29
    +        return returned_wrapper
    30
    +
    31
    +    if not func:
    32
    +        def foo(func):
    33
    +            return decorator(func)
    34
    +        return foo
    35
    +
    36
    +    return decorator(func)

    + 27 - 0
    course/deploy.bak/course.ini

    @@ -0,0 +1,27 @@
    1
    +# course_uwsgi.ini file
    2
    +[uwsgi]
    3
    +
    4
    +# Django-related settings
    5
    +# the base directory (full path)
    6
    +chdir           = /home/paiai/work/course
    7
    +# Django's wsgi file
    8
    +module          = course.wsgi
    9
    +# the virtualenv (full path)
    10
    +# home            = /path/to/virtualenv
    11
    +
    12
    +# process-related settings
    13
    +# master
    14
    +master          = true
    15
    +# maximum number of worker processes
    16
    +processes       = 10
    17
    +# the socket (use the full path to be safe
    18
    +socket          = /home/paiai/work/course/course/deploy/course.sock
    19
    +# ... with appropriate permissions - may be needed
    20
    +chmod-socket    = 777
    21
    +# clear environment on exit
    22
    +vacuum          = true
    23
    +
    24
    +# 11: Resource temporarily unavailable
    25
    +reload-mercy    = 64
    26
    +max-requests    = 8192
    27
    +listen          = 4096

    + 40 - 0
    course/deploy.bak/course_nginx.conf

    @@ -0,0 +1,40 @@
    1
    +# course_nginx.conf
    2
    +
    3
    +# the upstream component nginx needs to connect to
    4
    +upstream course {
    5
    +    # server unix:///home/paiai/work/course/course/deploy/course.sock; # for a file socket
    6
    +    server 127.0.0.1:8888; # for a web port socket (we'll use this first)
    7
    +}
    8
    +
    9
    +# configuration of the server
    10
    +server {
    11
    +    # the port your site will be served on
    12
    +    listen      80;
    13
    +    # the domain name it will serve for
    14
    +    server_name .a.com; # substitute your machine's IP address or FQDN
    15
    +    charset     utf-8;
    16
    +
    17
    +    # max upload size
    18
    +    client_max_body_size 75M;   # adjust to taste
    19
    +
    20
    +    # JS接口安全域名 & 业务域名 验证
    21
    +    location /xxx.txt {
    22
    +        alias /home/paiai/work/course/docs/we/xxx.txt;
    23
    +    }
    24
    +
    25
    +    # Django media
    26
    +    location /media  {
    27
    +        alias /home/paiai/work/course/media;  # your Django project's media files - amend as required
    28
    +    }
    29
    +
    30
    +    location /static {
    31
    +        alias /home/paiai/work/course/collect_static; # your Django project's static files - amend as required
    32
    +    }
    33
    +
    34
    +    # Finally, send all non-media requests to the Django server.
    35
    +    location / {
    36
    +        # uwsgi_pass  course;
    37
    +        proxy_pass  http://course;
    38
    +        include     /home/paiai/work/course/course/deploy/uwsgi_params; # the uwsgi_params file you installed
    39
    +    }
    40
    +}

    + 10 - 0
    course/deploy.bak/course_supervisor.ini

    @@ -0,0 +1,10 @@
    1
    +[program:course]
    2
    +command=/home/paiai/env/bin/uwsgi --ini /home/paiai/work/course/course/deploy/course.ini
    3
    +autostart=true
    4
    +autorestart=true
    5
    +startretries=3
    6
    +exitcodes=0,1,2
    7
    +stopsignal=QUIT
    8
    +stdout_logfile=/var/log/supervisor_course_access.log
    9
    +stderr_logfile=/var/log/supervisor_course_error.log
    10
    +user=diors

    + 15 - 0
    course/deploy.bak/uwsgi_params

    @@ -0,0 +1,15 @@
    1
    +uwsgi_param	QUERY_STRING		$query_string;
    2
    +uwsgi_param	REQUEST_METHOD		$request_method;
    3
    +uwsgi_param	CONTENT_TYPE		$content_type;
    4
    +uwsgi_param	CONTENT_LENGTH		$content_length;
    5
    +
    6
    +uwsgi_param	REQUEST_URI		$request_uri;
    7
    +uwsgi_param	PATH_INFO		$document_uri;
    8
    +uwsgi_param	DOCUMENT_ROOT		$document_root;
    9
    +uwsgi_param	SERVER_PROTOCOL		$server_protocol;
    10
    +uwsgi_param	UWSGI_SCHEME		$scheme;
    11
    +
    12
    +uwsgi_param	REMOTE_ADDR		$remote_addr;
    13
    +uwsgi_param	REMOTE_PORT		$remote_port;
    14
    +uwsgi_param	SERVER_PORT		$server_port;
    15
    +uwsgi_param	SERVER_NAME		$server_name;

    + 16 - 0
    course/func_settings.py

    @@ -0,0 +1,16 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +
    3
    +import redis_extensions as redis
    4
    +
    5
    +
    6
    +def redis_conf(conf):
    7
    +    return {
    8
    +        'host': conf.get('HOST', 'localhost'),
    9
    +        'port': conf.get('PORT', 6379),
    10
    +        'password': '{0}:{1}'.format(conf.get('USER', ''), conf.get('PASSWORD', '')) if conf.get('USER') else '',
    11
    +        'db': conf.get('db', 0),
    12
    +    }
    13
    +
    14
    +
    15
    +def redis_connect(conf):
    16
    +    return redis.StrictRedisExtensions(connection_pool=redis.ConnectionPool(**redis_conf(conf)))

    + 43 - 0
    course/local_settings_bak.py

    @@ -0,0 +1,43 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +
    3
    +import os
    4
    +
    5
    +
    6
    +# DEBUG = False
    7
    +
    8
    +ALLOWED_HOSTS = ['127.0.0.1', 'localhost']
    9
    +
    10
    +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    11
    +PROJ_DIR = os.path.abspath(os.path.dirname(os.path.abspath(__file__)))
    12
    +
    13
    +TEMPLATES = [
    14
    +    {
    15
    +        'BACKEND': 'django.template.backends.django.DjangoTemplates',
    16
    +        'DIRS': [os.path.join(BASE_DIR, 'templates')],
    17
    +        # 'APP_DIRS': True,
    18
    +        'OPTIONS': {
    19
    +            'context_processors': [
    20
    +                'django.template.context_processors.debug',
    21
    +                'django.template.context_processors.request',
    22
    +                'django.contrib.auth.context_processors.auth',
    23
    +                'django.contrib.messages.context_processors.messages',
    24
    +            ],
    25
    +            'loaders': [
    26
    +                'django.template.loaders.filesystem.Loader',
    27
    +                'django.template.loaders.app_directories.Loader',
    28
    +            ],
    29
    +        },
    30
    +    },
    31
    +]
    32
    +
    33
    +# DOMAIN
    34
    +DOMAIN = 'http://a.com'
    35
    +
    36
    +# 邮件设置
    37
    +# 只有当 DEBUG = False 的时候,才会邮件发送报错信息
    38
    +SERVER_EMAIL = 'error.notify@exmail.com'
    39
    +EMAIL_HOST_USER = 'error.notify@exmail.com'
    40
    +EMAIL_HOST_PASSWORD = '<^_^>pwd<^_^>'
    41
    +DEFAULT_FROM_EMAIL = 'error.notify <error.notify@exmail.com>'
    42
    +ADMINS = [('Zhang San', 'san.zhang@exmail.com'), ('Li Si', 'si.li@exmail.com')]
    43
    +EMAIL_SUBJECT_PREFIX = u'[Templet] '

    + 33 - 0
    course/oauth_settings.py

    @@ -0,0 +1,33 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +
    3
    +
    4
    +def DJANGO_WE_CFG_FUNC(request, state=None):
    5
    +    """ WeChat CFG Callback Func """
    6
    +
    7
    +
    8
    +def DJANGO_WE_BASE_FUNC(code, state, access_info=None):
    9
    +    """ WeChat Base Redirect Callback Func """
    10
    +
    11
    +
    12
    +def DJANGO_WE_USERINFO_FUNC(code, state, access_info=None, userinfo=None):
    13
    +    """ WeChat Userinfo Redirect Callback Func """
    14
    +    from account.models import UserInfo
    15
    +    from django.conf import settings
    16
    +    from utils.redis.connect import r
    17
    +
    18
    +    # Save profile or something else
    19
    +    unique_identifier = userinfo.get(settings.WECHAT_UNIQUE_IDENTIFICATION, '')
    20
    +
    21
    +    user, created = UserInfo.objects.select_for_update().get_or_create(**{settings.WECHAT_UNIQUE_IDENTIFICATION: unique_identifier})
    22
    +    user.unionid = userinfo.get('unionid', '')
    23
    +    user.openid = userinfo.get('openid', '')
    24
    +    user.nickname = userinfo.get('nickname', '')
    25
    +    user.avatar = userinfo.get('headimgurl', '')
    26
    +    user.save()
    27
    +
    28
    +    token_check_key = user.user_id
    29
    +
    30
    +    return {
    31
    +        settings.TOKEN_CHECK_KEY: token_check_key,
    32
    +        'vtoken': r.token(token_check_key, ex=False, buf=False),
    33
    +    }

    + 266 - 0
    course/settings.py

    @@ -0,0 +1,266 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +
    3
    +"""
    4
    +Django settings for course project.
    5
    +
    6
    +Generated by 'django-admin startproject' using Django 1.11.3.
    7
    +
    8
    +For more information on this file, see
    9
    +https://docs.djangoproject.com/en/1.11/topics/settings/
    10
    +
    11
    +For the full list of settings and their values, see
    12
    +https://docs.djangoproject.com/en/1.11/ref/settings/
    13
    +"""
    14
    +
    15
    +import os
    16
    +
    17
    +
    18
    +# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
    19
    +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    20
    +PROJ_DIR = os.path.abspath(os.path.dirname(os.path.abspath(__file__)))
    21
    +
    22
    +
    23
    +# Quick-start development settings - unsuitable for production
    24
    +# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
    25
    +
    26
    +# SECURITY WARNING: keep the secret key used in production secret!
    27
    +SECRET_KEY = 'b*_bo@vg6jv1!7lhfci-)7^155c+%83y69zgf6)@s^9=_32edi'
    28
    +
    29
    +# SECURITY WARNING: don't run with debug turned on in production!
    30
    +DEBUG = True
    31
    +
    32
    +ALLOWED_HOSTS = []
    33
    +
    34
    +
    35
    +# Application definition
    36
    +
    37
    +INSTALLED_APPS = [
    38
    +    'django.contrib.admin',
    39
    +    'django.contrib.auth',
    40
    +    'django.contrib.contenttypes',
    41
    +    'django.contrib.sessions',
    42
    +    'django.contrib.messages',
    43
    +    'django.contrib.staticfiles',
    44
    +    'django_uniapi',
    45
    +    'django_we',
    46
    +    'account',
    47
    +    'api',
    48
    +    'codes',
    49
    +    'courses',
    50
    +    'page',
    51
    +]
    52
    +
    53
    +MIDDLEWARE = [
    54
    +    'django.middleware.security.SecurityMiddleware',
    55
    +    'django.contrib.sessions.middleware.SessionMiddleware',
    56
    +    'django.middleware.common.CommonMiddleware',
    57
    +    # 'django.middleware.csrf.CsrfViewMiddleware',
    58
    +    'django.contrib.auth.middleware.AuthenticationMiddleware',
    59
    +    'django.contrib.messages.middleware.MessageMiddleware',
    60
    +    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    61
    +    'detect.middleware.UserAgentDetectionMiddleware',
    62
    +]
    63
    +
    64
    +ROOT_URLCONF = 'course.urls'
    65
    +
    66
    +TEMPLATES = [
    67
    +    {
    68
    +        'BACKEND': 'django.template.backends.django.DjangoTemplates',
    69
    +        'DIRS': [os.path.join(BASE_DIR, 'templates')],
    70
    +        # 'APP_DIRS': True,
    71
    +        'OPTIONS': {
    72
    +            'context_processors': [
    73
    +                'django.template.context_processors.debug',
    74
    +                'django.template.context_processors.request',
    75
    +                'django.contrib.auth.context_processors.auth',
    76
    +                'django.contrib.messages.context_processors.messages',
    77
    +            ],
    78
    +            'loaders': [
    79
    +                ('django.template.loaders.cached.Loader', [
    80
    +                    'django.template.loaders.filesystem.Loader',
    81
    +                    'django.template.loaders.app_directories.Loader',
    82
    +                ]),
    83
    +            ],
    84
    +        },
    85
    +    },
    86
    +]
    87
    +
    88
    +WSGI_APPLICATION = 'course.wsgi.application'
    89
    +
    90
    +
    91
    +# Database
    92
    +# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
    93
    +
    94
    +DATABASES = {
    95
    +    'default': {
    96
    +        'ENGINE': 'django.db.backends.mysql',
    97
    +        'NAME': 'course',
    98
    +        'USER': 'root',
    99
    +        'PASSWORD': '',
    100
    +        'CONN_MAX_AGE': 600,
    101
    +        'OPTIONS': {
    102
    +            # Utf8mb4 for Emoji
    103
    +            #
    104
    +            # Nickname
    105
    +            #
    106
    +            # account.WechatInfo ==> nickname
    107
    +            #   ALTER TABLE account_wechatinfo MODIFY COLUMN nickname VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
    108
    +            'charset': 'utf8mb4',
    109
    +        },
    110
    +    }
    111
    +}
    112
    +
    113
    +
    114
    +# Password validation
    115
    +# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
    116
    +
    117
    +AUTH_PASSWORD_VALIDATORS = [
    118
    +    {
    119
    +        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    120
    +    },
    121
    +    {
    122
    +        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    123
    +    },
    124
    +    {
    125
    +        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    126
    +    },
    127
    +    {
    128
    +        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    129
    +    },
    130
    +]
    131
    +
    132
    +
    133
    +# Internationalization
    134
    +# https://docs.djangoproject.com/en/1.11/topics/i18n/
    135
    +
    136
    +LANGUAGE_CODE = 'zh-Hans'
    137
    +
    138
    +TIME_ZONE = 'Asia/Shanghai'
    139
    +
    140
    +USE_I18N = True
    141
    +
    142
    +USE_L10N = True
    143
    +
    144
    +USE_TZ = True
    145
    +
    146
    +
    147
    +# Static files (CSS, JavaScript, Images)
    148
    +# https://docs.djangoproject.com/en/1.11/howto/static-files/
    149
    +
    150
    +STATICFILES_DIRS = (
    151
    +    os.path.join(PROJ_DIR, 'static').replace('\\', '/'),
    152
    +)
    153
    +
    154
    +STATIC_ROOT = os.path.join(BASE_DIR, 'collect_static').replace('\\', '/')
    155
    +
    156
    +STATIC_URL = '/static/'
    157
    +
    158
    +STATICFILES_FINDERS = (
    159
    +    'django.contrib.staticfiles.finders.FileSystemFinder',
    160
    +    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
    161
    +    # 'django.contrib.staticfiles.finders.DefaultStorageFinder',
    162
    +)
    163
    +
    164
    +MEDIA_ROOT = os.path.join(BASE_DIR, 'media').replace('\\', '/')
    165
    +
    166
    +MEDIA_URL = '/media/'
    167
    +
    168
    +# DOMAIN
    169
    +DOMAIN = 'http://a.com'
    170
    +
    171
    +# Redis 设置
    172
    +REDIS = {
    173
    +    'default': {
    174
    +        'HOST': '127.0.0.1',
    175
    +        'PORT': 6379,
    176
    +        'USER': '',
    177
    +        'PASSWORD': '',
    178
    +        'db': 0,
    179
    +    }
    180
    +}
    181
    +
    182
    +# 微信设置
    183
    +WECHAT = {
    184
    +    'JSAPI': {
    185
    +        'token': '5201314',
    186
    +        'appID': '',
    187
    +        'appsecret': '',
    188
    +        'mchID': '',
    189
    +        'apiKey': '',
    190
    +        'mch_cert': '',
    191
    +        'mch_key': '',
    192
    +        'redpack': {
    193
    +
    194
    +        }
    195
    +    },
    196
    +}
    197
    +
    198
    +# 微信唯一标识
    199
    +# Choices: 'unionid' or 'openid'
    200
    +#
    201
    +# models.py
    202
    +#   'unique_identifier': self.unionid if settings.WECHAT_UNIQUE_IDENTIFICATION == 'unionid' else self.openid,
    203
    +# views.py
    204
    +#   unique_identifier = request.POST.get(settings.WECHAT_UNIQUE_IDENTIFICATION, '')
    205
    +#   profile = Profile.objects.get(**{settings.WECHAT_UNIQUE_IDENTIFICATION: unique_identifier})
    206
    +WECHAT_UNIQUE_IDENTIFICATION = 'unionid'
    207
    +
    208
    +# Token 错误重授权设置
    209
    +TOKEN_CHECK_KEY = 'user_id'
    210
    +WECHAT_OAUTH2_REDIRECT_ENTRY = ''
    211
    +WECHAT_OAUTH2_REDIRECT_URL = ''
    212
    +
    213
    +# 错误信息邮件设置
    214
    +# Email address that error messages come from.
    215
    +SERVER_EMAIL = 'kimi@pai.ai'
    216
    +# The email backend to use. For possible shortcuts see django.core.mail.
    217
    +# The default is to use the SMTP backend.
    218
    +# Third-party backends can be specified by providing a Python path
    219
    +# to a module that defines an EmailBackend class.
    220
    +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
    221
    +# Host for sending email.
    222
    +EMAIL_HOST = 'smtp.exmail.qq.com'
    223
    +# Port for sending email.
    224
    +EMAIL_PORT = 25
    225
    +# Optional SMTP authentication information for EMAIL_HOST.
    226
    +EMAIL_HOST_USER = 'kimi@pai.ai'
    227
    +EMAIL_HOST_PASSWORD = '<^_^>pwd<^_^>'
    228
    +EMAIL_USE_TLS = False
    229
    +EMAIL_USE_SSL = False
    230
    +EMAIL_SSL_CERTFILE = None
    231
    +EMAIL_SSL_KEYFILE = None
    232
    +EMAIL_TIMEOUT = None
    233
    +# Default email address to use for various automated correspondence from
    234
    +# the site managers.
    235
    +DEFAULT_FROM_EMAIL = 'Kimi <kimi@pai.ai>'
    236
    +# People who get code error notifications.
    237
    +# In the format [('Full Name', 'email@example.com'), ('Full Name', 'anotheremail@example.com')]
    238
    +ADMINS = [('Kimi', 'kimi@pai.ai')]
    239
    +# Not-necessarily-technical managers of the site. They get broken link
    240
    +# notifications and other various emails.
    241
    +MANAGERS = ADMINS
    242
    +# Subject-line prefix for email messages send with django.core.mail.mail_admins
    243
    +# or ...mail_managers.  Make sure to include the trailing space.
    244
    +EMAIL_SUBJECT_PREFIX = u'[Course] '
    245
    +
    246
    +# Admin Settings
    247
    +DISABLE_ACTION = False
    248
    +
    249
    +try:
    250
    +    from local_settings import *
    251
    +except ImportError:
    252
    +    pass
    253
    +
    254
    +# 依赖 local_settings 中的配置
    255
    +# 微信授权设置
    256
    +WECHAT_OAUTH2_REDIRECT_URI = '{0}/we/oauth2?scope={{0}}redirect_url={{1}}'.format(DOMAIN)
    257
    +WECHAT_BASE_REDIRECT_URI = '{0}/we/base_redirect'.format(DOMAIN)
    258
    +WECHAT_USERINFO_REDIRECT_URI = '{0}/we/userinfo_redirect'.format(DOMAIN)
    259
    +WECHAT_DIRECT_BASE_REDIRECT_URI = '{0}/we/direct_base_redirect'.format(DOMAIN)
    260
    +WECHAT_DIRECT_USERINFO_REDIRECT_URI = '{0}/we/direct_userinfo_redirect'.format(DOMAIN)
    261
    +
    262
    +try:
    263
    +    from func_settings import redis_connect
    264
    +    REDIS_CACHE = redis_connect(REDIS.get('default', {}))
    265
    +except ImportError:
    266
    +    REDIS_CACHE = None

    + 331 - 0
    course/static/course/js/jswe-0.0.1.js

    @@ -0,0 +1,331 @@
    1
    +!(function(e, t) {
    2
    +    var config = {
    3
    +        wxconfig: 'http://api.pai.ai/wx/jsapi_signature',
    4
    +        callback: 'callback'
    5
    +    }, wxData = {
    6
    +        debug: false,
    7
    +        imgUrl: '',
    8
    +        link: '',
    9
    +        desc: '',
    10
    +        title: '',
    11
    +        timeLine: ''
    12
    +    }, wxConfig = {
    13
    +        hide: false,
    14
    +        close: false
    15
    +    }, jsApiList = [
    16
    +        'checkJsApi',
    17
    +        'onMenuShareTimeline',
    18
    +        'onMenuShareAppMessage',
    19
    +        'onMenuShareQQ',
    20
    +        'onMenuShareWeibo',
    21
    +        'hideMenuItems',
    22
    +        'showMenuItems',
    23
    +        'hideAllNonBaseMenuItem',
    24
    +        'showAllNonBaseMenuItem',
    25
    +        'translateVoice',
    26
    +        'startRecord',
    27
    +        'stopRecord',
    28
    +        'onRecordEnd',
    29
    +        'playVoice',
    30
    +        'pauseVoice',
    31
    +        'stopVoice',
    32
    +        'uploadVoice',
    33
    +        'downloadVoice',
    34
    +        'chooseImage',
    35
    +        'previewImage',
    36
    +        'uploadImage',
    37
    +        'downloadImage',
    38
    +        'getNetworkType',
    39
    +        'openLocation',
    40
    +        'getLocation',
    41
    +        'hideOptionMenu',
    42
    +        'showOptionMenu',
    43
    +        'closeWindow',
    44
    +        'scanQRCode',
    45
    +        'chooseWXPay',
    46
    +        'openEnterpriseRedPacket',
    47
    +        'openProductSpecificView',
    48
    +        'addCard',
    49
    +        'chooseCard',
    50
    +        'openCard'
    51
    +    ], wxApiFun
    52
    +
    53
    +    function isOpenOnPC() {  // 判断当前网页是否在 PC 浏览器中打开
    54
    +        var ua = navigator.userAgent
    55
    +        return /windows nt/i.test(ua) || /macintosh/i.test(ua) || /linux x86_64/i.test(ua)
    56
    +    }
    57
    +
    58
    +    function isOpenInWeixin() {  // 判断当前网页是否在微信内置浏览器中打开
    59
    +        return /micromessenger/i.test(navigator.userAgent)
    60
    +    }
    61
    +
    62
    +    function getWeixinVersion() {
    63
    +        var ua = navigator.userAgent,
    64
    +            mt = ua.match(/micromessenger\/([\d.]+)/i)
    65
    +        return (mt ? mt[1] : '')
    66
    +    }
    67
    +
    68
    +    // This function checks whether Wechat is the appointed version or not
    69
    +    // Cmp: http://jsperf.com/regexp-test-vs-indexof-ignore-upper-and-lower
    70
    +    function isWeixinVersion(version) {
    71
    +        // return new RegExp('micromessenger/' + version , 'i').test(navigator.userAgent)
    72
    +        return navigator.userAgent.toLowerCase().indexOf('micromessenger/' + version) != -1
    73
    +    }
    74
    +
    75
    +    function hideOptionMenu() {
    76
    +        wxConfig.hide = true
    77
    +        fixedWxData()
    78
    +    }
    79
    +
    80
    +    function showOptionMenu() {
    81
    +        wxConfig.hide = false
    82
    +        fixedWxData()
    83
    +    }
    84
    +
    85
    +    function closeWindow() {
    86
    +        wxConfig.close = true
    87
    +        fixedWxData()
    88
    +    }
    89
    +
    90
    +    function wxReady(data) {
    91
    +        data = typeof data === 'object' ? data : JSON.parse(data)
    92
    +        wx.config({
    93
    +            debug: wxData.debug,
    94
    +            appId: data.appId,
    95
    +            timestamp: data.timestamp,
    96
    +            nonceStr: data.nonceStr,
    97
    +            signature: data.signature,
    98
    +            jsApiList: jsApiList
    99
    +        })
    100
    +
    101
    +        var callbacks = {
    102
    +            trigger: function (res) {
    103
    +                // alert('用户点击发送给朋友')
    104
    +                if (JSWE.wxTrigger) {JSWE.wxTrigger(res)}
    105
    +            },
    106
    +            success: function (res) {
    107
    +                // alert('已分享')
    108
    +                if (JSWE.wxSuccess) {JSWE.wxSuccess(res)}
    109
    +            },
    110
    +            cancel: function (res) {
    111
    +                // alert('已取消')
    112
    +                if (JSWE.wxCancel) {JSWE.wxCancel(res)}
    113
    +            },
    114
    +            fail: function (res) {
    115
    +                // alert(JSON.stringify(res))
    116
    +                if (JSWE.wxFail) {JSWE.wxFail(res)}
    117
    +            }
    118
    +        }, shareInfo = function(flag) {
    119
    +            var _share = {
    120
    +                title: flag ? wxData.title : (wxData.timeLine || wxData.desc),
    121
    +                link: wxData.link,
    122
    +                imgUrl: wxData.imgUrl,
    123
    +                trigger: callbacks.trigger,
    124
    +                success: callbacks.success,
    125
    +                cancel: callbacks.cancel,
    126
    +                fail: callbacks.fail
    127
    +            }
    128
    +            if (flag) _share.desc = wxData.desc
    129
    +            return _share
    130
    +        }, wxShareApi = function() {
    131
    +            // 2. 分享接口
    132
    +            // 2.1 监听“分享给朋友”,按钮点击、自定义分享内容及分享结果接口
    133
    +            wx.onMenuShareAppMessage(shareInfo(1))
    134
    +            // 2.2 监听“分享到朋友圈”按钮点击、自定义分享内容及分享结果接口
    135
    +            wx.onMenuShareTimeline(shareInfo(0))
    136
    +            // 2.3 监听“分享到QQ”按钮点击、自定义分享内容及分享结果接口
    137
    +            wx.onMenuShareQQ(shareInfo(1))
    138
    +            // 2.4 监听“分享到微博”按钮点击、自定义分享内容及分享结果接口
    139
    +            wx.onMenuShareWeibo(shareInfo(1))
    140
    +        }, wxMenuApi = function () {
    141
    +            // 8. 界面操作接口
    142
    +            // 8.1 隐藏右上角菜单
    143
    +            // 8.2 显示右上角菜单
    144
    +            if (wxConfig.hide) {wx.hideOptionMenu()} else {wx.showOptionMenu()}
    145
    +            // 8.7 关闭当前窗口
    146
    +            if (wxConfig.close) {wx.closeWindow()}
    147
    +        }, wxApi = function () {
    148
    +            wxShareApi()
    149
    +            wxMenuApi()
    150
    +        }
    151
    +
    152
    +        wx.ready(wxApi)
    153
    +
    154
    +        return wxApiFun = wxApi
    155
    +    }
    156
    +
    157
    +    if (isOpenInWeixin() || isOpenOnPC()) {
    158
    +        if ('undefined' !== typeof JSWE_CONF_UPDATE) JSWE_CONF_UPDATE(config)
    159
    +        $.ajax({
    160
    +            url: config.wxconfig,
    161
    +            type: 'get',
    162
    +            dataType: 'jsonp',
    163
    +            jsonpCallback: config.callback,
    164
    +            data: {
    165
    +                url: window.location.href.split('#')[0]
    166
    +            },
    167
    +            success: wxReady
    168
    +        })
    169
    +    }
    170
    +
    171
    +    function initWxData(data, flag) {
    172
    +        for(var d in data) {if (d in wxData) wxData[d] = data[d]}
    173
    +        if (flag) fixedWxData()
    174
    +    }
    175
    +
    176
    +    function changeWxData(key, value, flag) {
    177
    +        if (key in falDwxDataata) {wxData[key] = value}
    178
    +        if (flag) fixedWxData()
    179
    +    }
    180
    +
    181
    +    function fixedWxData() {
    182
    +        if ('undefined' !== typeof wxApiFun) wxApiFun()
    183
    +    }
    184
    +
    185
    +    // 5 图片接口
    186
    +    // 5.1 拍照、本地选图
    187
    +    var images = {
    188
    +        localIds: [],
    189
    +        serverIds: []
    190
    +    };
    191
    +    // function chooseImage(count, directUpload, isShowProgressTips) {
    192
    +    function chooseImage(choose_params) {
    193
    +        if ('undefined' === typeof choose_params) choose_params = {}
    194
    +        wx.chooseImage({
    195
    +            count: choose_params.count || 9, // 默认9
    196
    +            sizeType: choose_params.sizeType || ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
    197
    +            sourceType: choose_params.sourceType || ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
    198
    +            success: function (res) {
    199
    +                images.localIds = res.localIds; // 返回选定照片的本地ID列表,localId可以作为img标签的src属性显示图片
    200
    +                // 判断是否直接上传
    201
    +                if (choose_params.directUpload) {setTimeout(uploadImages({localIds: images.localIds, isShowProgressTips: choose_params.isShowProgressTips || 1}), 100)}
    202
    +                // 拍照、本地选图成功后的回调函数
    203
    +                if (JSWE.wxChooseImageSuccess) {JSWE.wxChooseImageSuccess(res)}
    204
    +            }
    205
    +        });
    206
    +    }
    207
    +
    208
    +    // 5.2 图片预览
    209
    +    function previewImage(preview_params) {
    210
    +        wx.previewImage({
    211
    +            current: preview_params.current, // 当前显示图片的链接,不填则默认为 urls 的第一张
    212
    +            urls: preview_params.urls // 需要预览的图片链接列表
    213
    +        });
    214
    +    }
    215
    +
    216
    +    // 5.3 上传图片
    217
    +    // function uploadImage(localId, isShowProgressTips) {
    218
    +    function uploadImage(upload_params) {
    219
    +        // 上传图片为异步处理,重复上传同一图片,返回的serverId也是不同的
    220
    +        wx.uploadImage({
    221
    +            localId: upload_params.localId, // 需要上传的图片的本地ID,由chooseImage接口获得
    222
    +            isShowProgressTips: upload_params.isShowProgressTips || 1, // 默认为1,显示进度提示
    223
    +            success: function (res) {
    224
    +                images.serverIds.push(res.serverId); // 返回图片的服务器端ID
    225
    +                // 上传图片成功后的回调函数
    226
    +                if (JSWE.wxUploadImageSuccess) {JSWE.wxUploadImageSuccess(res)}
    227
    +            }
    228
    +        });
    229
    +    }
    230
    +
    231
    +    // function uploadImages(localIds, isShowProgressTips) {
    232
    +    function uploadImages(upload_params) {
    233
    +        var localIds = upload_params.localIds, isShowProgressTips = upload_params.isShowProgressTips || 1
    234
    +        images.serverIds = [];
    235
    +        for (var idx in localIds) {uploadImage({localId: localIds[idx], isShowProgressTips: isShowProgressTips})}
    236
    +    }
    237
    +
    238
    +    // 9 微信原生接口
    239
    +    // 9.1.1 扫描二维码并返回结果
    240
    +    // 9.1.2 扫描二维码并返回结果
    241
    +    function scanQRCode(scan_params) {
    242
    +        if ('undefined' === typeof scan_params) scan_params = {}
    243
    +        wx.scanQRCode({
    244
    +            needResult: scan_params.needResult || 0,  // 默认为0,0扫描结果由微信处理,1直接返回扫描结果
    245
    +            scanType: scan_params.scanType || ['qrCode', 'barCode'],  // 可以指定扫二维码还是一维码,默认二者都有
    246
    +            success: function (res) {  // 当 needResult 为 1 时,扫码返回的结果
    247
    +                if (JSWE.wxScanQRCodeSuccess) {JSWE.wxScanQRCodeSuccess(res)}
    248
    +            }
    249
    +        });
    250
    +    }
    251
    +
    252
    +    // QRCode & BarCode is different
    253
    +    function parseScanQRCodeResultStr(resultStr) {
    254
    +        var strs = resultStr.split(',')
    255
    +        return strs[strs.length - 1]
    256
    +    }
    257
    +
    258
    +    // 10 微信支付接口
    259
    +    // 10.1 发起一个支付请求
    260
    +    function chooseWXPay(wxpay_params) {
    261
    +        wx.chooseWXPay({
    262
    +            timestamp: wxpay_params.timeStamp, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
    263
    +            nonceStr: wxpay_params.nonceStr, // 支付签名随机串,不长于 32 位
    264
    +            package: wxpay_params.package, // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=***)
    265
    +            signType: wxpay_params.signType, // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'
    266
    +            paySign: wxpay_params.paySign, // 支付签名
    267
    +            success: function (res) {
    268
    +                // 支付成功后的回调函数
    269
    +                if (JSWE.wxPaySuccess) {JSWE.wxPaySuccess(res)}
    270
    +            }
    271
    +        })
    272
    +    }
    273
    +
    274
    +    // xx 微信原生企业红包接口
    275
    +    // xx.1 发起一个发送原生企业红包请求
    276
    +    function openEnterpriseRedPacket(wxredpack_params) {
    277
    +        wx.openEnterpriseRedPacket({
    278
    +            timeStamp: wxredpack_params.timeStamp, // 红包签名时间戳,注意原生企业红包接口timeStamp字段名需大写其中的S字符,而支付接口timeStamp字段名无需大写其中的S字符。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
    279
    +            nonceStr: wxredpack_params.nonceStr, // 红包签名随机串,不长于 32 位
    280
    +            package: encodeURIComponent(wxredpack_params.package), // 发放红包接口返回的prepay_id参数值,提交格式如:prepay_id=***)
    281
    +            signType: wxredpack_params.signType, // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'
    282
    +            paySign: wxredpack_params.paySign, // 红包签名
    283
    +            success: function (res) {
    284
    +                // 发送原生企业红包成功后的回调函数
    285
    +                if (JSWE.wxEnterpriseRedPacketSuccess) {JSWE.wxEnterpriseRedPacketSuccess(res)}
    286
    +            }
    287
    +        })
    288
    +    }
    289
    +
    290
    +    var v = {
    291
    +        version: '1.0.5',
    292
    +
    293
    +        // Basic Vars
    294
    +        config: config,
    295
    +        wxData: wxData,
    296
    +        jsApiList: jsApiList,
    297
    +
    298
    +        // Weixin Function
    299
    +        isOpenInWeixin: isOpenInWeixin,
    300
    +        getWeixinVersion: getWeixinVersion,
    301
    +        isWeixinVersion: isWeixinVersion,
    302
    +
    303
    +        // Menu Function
    304
    +        hideOptionMenu: hideOptionMenu,
    305
    +        showOptionMenu: showOptionMenu,
    306
    +        closeWindow: closeWindow,
    307
    +
    308
    +        // Share Function
    309
    +        initWxData: initWxData,
    310
    +        changeWxData: changeWxData,
    311
    +        fixedWxData: fixedWxData,
    312
    +
    313
    +        // Image Function
    314
    +        images: images,
    315
    +        chooseImage: chooseImage,
    316
    +        previewImage: previewImage,
    317
    +        uploadImage: uploadImage,
    318
    +        uploadImages: uploadImages,
    319
    +
    320
    +        // Scan Function
    321
    +        scanQRCode: scanQRCode,
    322
    +        parseScanQRCodeResultStr: parseScanQRCodeResultStr,
    323
    +
    324
    +        // Pay Function
    325
    +        chooseWXPay: chooseWXPay,
    326
    +
    327
    +        // EnterpriseRedPacket Function
    328
    +        openEnterpriseRedPacket: openEnterpriseRedPacket
    329
    +    }
    330
    +    e.JSWE = e.V = v
    331
    +})(window)

    + 36 - 0
    course/urls.py

    @@ -0,0 +1,36 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +
    3
    +"""course URL Configuration
    4
    +
    5
    +The `urlpatterns` list routes URLs to views. For more information please see:
    6
    +    https://docs.djangoproject.com/en/1.11/topics/http/urls/
    7
    +Examples:
    8
    +Function views
    9
    +    1. Add an import:  from my_app import views
    10
    +    2. Add a URL to urlpatterns:  url(r'^$', views.home, name='home')
    11
    +Class-based views
    12
    +    1. Add an import:  from other_app.views import Home
    13
    +    2. Add a URL to urlpatterns:  url(r'^$', Home.as_view(), name='home')
    14
    +Including another URLconf
    15
    +    1. Import the include() function: from django.conf.urls import url, include
    16
    +    2. Add a URL to urlpatterns:  url(r'^blog/', include('blog.urls'))
    17
    +"""
    18
    +from django.conf import settings
    19
    +from django.conf.urls import include, url
    20
    +from django.conf.urls.static import static
    21
    +from django.contrib import admin
    22
    +
    23
    +
    24
    +urlpatterns = [
    25
    +    url(r'^courseadmin/', admin.site.urls),
    26
    +    url(r'^api/', include('api.urls', namespace='api')),
    27
    +    url(r'^uniapi/', include('django_uniapi.urls', namespace='uniapi')),
    28
    +    url(r'^we/', include('django_we.urls', namespace='wechat')),
    29
    +
    30
    +    url(r'^page/', include('page.urls', namespace='page')),
    31
    +]
    32
    +
    33
    +urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
    34
    +urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
    35
    +
    36
    +admin.site.site_header = u'合作课程系统'

    + 17 - 0
    course/wsgi.py

    @@ -0,0 +1,17 @@
    1
    +"""
    2
    +WSGI config for course project.
    3
    +
    4
    +It exposes the WSGI callable as a module-level variable named ``application``.
    5
    +
    6
    +For more information on this file, see
    7
    +https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
    8
    +"""
    9
    +
    10
    +import os
    11
    +
    12
    +from django.core.wsgi import get_wsgi_application
    13
    +
    14
    +
    15
    +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "course.settings")
    16
    +
    17
    +application = get_wsgi_application()

    + 0 - 0
    courses/__init__.py


    + 20 - 0
    courses/admin.py

    @@ -0,0 +1,20 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +
    3
    +from djadmin import ExportExcelModelAdmin, ReadonlyModelAdmin
    4
    +from django.contrib import admin
    5
    +
    6
    +from courses.models import CourseInfo, CourseVideoInfo
    7
    +
    8
    +
    9
    +class CourseInfoAdmin(ExportExcelModelAdmin, admin.ModelAdmin):
    10
    +    list_display = ('course_id', 'course_name', 'course_time', 'course_cover', 'status', 'created_at', 'updated_at')
    11
    +    list_filter = ('status', )
    12
    +
    13
    +
    14
    +class CourseVideoInfoAdmin(ExportExcelModelAdmin, admin.ModelAdmin):
    15
    +    list_display = ('course', 'course_video_id', 'course_video_type', 'course_video_name', 'course_video_desc', 'course_video_time', 'course_video_cover', 'course_video_position', 'status', 'created_at', 'updated_at')
    16
    +    list_filter = ('course', 'status')
    17
    +
    18
    +
    19
    +admin.site.register(CourseInfo, CourseInfoAdmin)
    20
    +admin.site.register(CourseVideoInfo, CourseVideoInfoAdmin)

    + 8 - 0
    courses/apps.py

    @@ -0,0 +1,8 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +from __future__ import unicode_literals
    3
    +
    4
    +from django.apps import AppConfig
    5
    +
    6
    +
    7
    +class CoursesConfig(AppConfig):
    8
    +    name = 'courses'

    + 57 - 0
    courses/migrations/0001_initial.py

    @@ -0,0 +1,57 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +# Generated by Django 1.11.3 on 2017-09-25 07:47
    3
    +from __future__ import unicode_literals
    4
    +
    5
    +import courses.models
    6
    +from django.db import migrations, models
    7
    +import django.db.models.deletion
    8
    +import shortuuidfield.fields
    9
    +
    10
    +
    11
    +class Migration(migrations.Migration):
    12
    +
    13
    +    initial = True
    14
    +
    15
    +    dependencies = [
    16
    +    ]
    17
    +
    18
    +    operations = [
    19
    +        migrations.CreateModel(
    20
    +            name='CourseInfo',
    21
    +            fields=[
    22
    +                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
    23
    +                ('status', models.BooleanField(db_index=True, default=True, help_text='\u72b6\u6001', verbose_name='status')),
    24
    +                ('created_at', models.DateTimeField(auto_now_add=True, help_text='\u521b\u5efa\u65f6\u95f4', verbose_name='created_at')),
    25
    +                ('updated_at', models.DateTimeField(auto_now=True, help_text='\u66f4\u65b0\u65f6\u95f4', verbose_name='updated_at')),
    26
    +                ('course_id', shortuuidfield.fields.ShortUUIDField(blank=True, db_index=True, editable=False, help_text='\u8bfe\u7a0b\u552f\u4e00\u6807\u8bc6', max_length=22, unique=True)),
    27
    +                ('course_name', models.CharField(blank=True, help_text='\u8bfe\u7a0b\u6807\u9898', max_length=255, null=True, verbose_name='course_name')),
    28
    +                ('course_time', models.IntegerField(default=0, help_text='\u8bfe\u7a0b\u65f6\u95f4', verbose_name='course_time')),
    29
    +                ('course_cover', models.ImageField(blank=True, help_text='\u8bfe\u7a0b\u89c6\u9891\u7f29\u7565\u56fe', null=True, upload_to=courses.models.upload_path, verbose_name='course_cover')),
    30
    +            ],
    31
    +            options={
    32
    +                'verbose_name': 'courseinfo',
    33
    +                'verbose_name_plural': 'courseinfo',
    34
    +            },
    35
    +        ),
    36
    +        migrations.CreateModel(
    37
    +            name='CourseVideoInfo',
    38
    +            fields=[
    39
    +                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
    40
    +                ('status', models.BooleanField(db_index=True, default=True, help_text='\u72b6\u6001', verbose_name='status')),
    41
    +                ('created_at', models.DateTimeField(auto_now_add=True, help_text='\u521b\u5efa\u65f6\u95f4', verbose_name='created_at')),
    42
    +                ('updated_at', models.DateTimeField(auto_now=True, help_text='\u66f4\u65b0\u65f6\u95f4', verbose_name='updated_at')),
    43
    +                ('course_video_id', shortuuidfield.fields.ShortUUIDField(blank=True, db_index=True, editable=False, help_text='\u8bfe\u7a0b\u89c6\u9891\u552f\u4e00\u6807\u8bc6', max_length=22, unique=True)),
    44
    +                ('course_video_type', models.CharField(blank=True, help_text='\u8bfe\u7a0b\u89c6\u9891\u7c7b\u578b', max_length=255, null=True, verbose_name='course_video_type')),
    45
    +                ('course_video_name', models.CharField(blank=True, help_text='\u8bfe\u7a0b\u89c6\u9891\u6807\u9898', max_length=255, null=True, verbose_name='course_video_name')),
    46
    +                ('course_video_desc', models.TextField(blank=True, help_text='\u8bfe\u7a0b\u89c6\u9891\u63cf\u8ff0', null=True, verbose_name='course_video_desc')),
    47
    +                ('course_video_time', models.IntegerField(default=0, help_text='\u8bfe\u7a0b\u89c6\u9891\u65f6\u95f4', verbose_name='course_video_time')),
    48
    +                ('course_video_cover', models.ImageField(blank=True, help_text='\u8bfe\u7a0b\u89c6\u9891\u7f29\u7565\u56fe', null=True, upload_to=courses.models.upload_path, verbose_name='course_video_cover')),
    49
    +                ('course_video_position', models.IntegerField(default=0, help_text='\u8bfe\u7a0b\u89c6\u9891\u6392\u5e8f', verbose_name='course_video_position')),
    50
    +                ('course', models.ForeignKey(blank=True, help_text='\u8bfe\u7a0b', null=True, on_delete=django.db.models.deletion.CASCADE, to='courses.CourseInfo', verbose_name='course')),
    51
    +            ],
    52
    +            options={
    53
    +                'verbose_name': 'coursevideoinfo',
    54
    +                'verbose_name_plural': 'coursevideoinfo',
    55
    +            },
    56
    +        ),
    57
    +    ]

    + 21 - 0
    courses/migrations/0002_coursevideoinfo_course_video.py

    @@ -0,0 +1,21 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +# Generated by Django 1.11.3 on 2017-09-25 08:55
    3
    +from __future__ import unicode_literals
    4
    +
    5
    +import courses.models
    6
    +from django.db import migrations, models
    7
    +
    8
    +
    9
    +class Migration(migrations.Migration):
    10
    +
    11
    +    dependencies = [
    12
    +        ('courses', '0001_initial'),
    13
    +    ]
    14
    +
    15
    +    operations = [
    16
    +        migrations.AddField(
    17
    +            model_name='coursevideoinfo',
    18
    +            name='course_video',
    19
    +            field=models.FileField(blank=True, help_text='\u8bfe\u7a0b\u89c6\u9891', null=True, upload_to=courses.models.upload_path, verbose_name='course_video'),
    20
    +        ),
    21
    +    ]

    + 0 - 0
    courses/migrations/__init__.py


    + 87 - 0
    courses/models.py

    @@ -0,0 +1,87 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +
    3
    +import os
    4
    +
    5
    +from django.db import models
    6
    +from django.utils.translation import ugettext_lazy as _
    7
    +from shortuuidfield import ShortUUIDField
    8
    +from TimeConvert import TimeConvert as tc
    9
    +
    10
    +from course.basemodels import CreateUpdateMixin
    11
    +from utils.url_utils import upload_file_url
    12
    +
    13
    +
    14
    +def upload_path(instance, old_filename):
    15
    +    return 'file/{ym}/{stamp}{ext}'.format(
    16
    +        ym=tc.local_string(format='%Y%m'),
    17
    +        stamp=tc.local_timestamp(ms=True),
    18
    +        ext=os.path.splitext(old_filename)[1].lower(),
    19
    +    )
    20
    +
    21
    +
    22
    +class CourseInfo(CreateUpdateMixin):
    23
    +    course_id = ShortUUIDField(_(u'course_id'), max_length=255, help_text=u'课程唯一标识', db_index=True, unique=True)
    24
    +    course_name = models.CharField(_(u'course_name'), max_length=255, blank=True, null=True, help_text=u'课程标题')
    25
    +    course_time = models.IntegerField(_(u'course_time'), default=0, help_text=u'课程时间')
    26
    +    course_cover = models.ImageField(_(u'course_cover'), upload_to=upload_path, blank=True, null=True, help_text=u'课程视频缩略图')
    27
    +
    28
    +    class Meta:
    29
    +        verbose_name = _(u'courseinfo')
    30
    +        verbose_name_plural = _(u'courseinfo')
    31
    +
    32
    +    def __unicode__(self):
    33
    +        return unicode(self.course_name)
    34
    +
    35
    +    @property
    36
    +    def course_cover_url(self):
    37
    +        return upload_file_url(self.course_cover)
    38
    +
    39
    +    @property
    40
    +    def data(self):
    41
    +        return {
    42
    +            'course_id': self.course_id,
    43
    +            'course_name': self.course_name,
    44
    +            'course_time': self.course_time,
    45
    +            'course_cover_url': self.course_cover_url,
    46
    +        }
    47
    +
    48
    +
    49
    +class CourseVideoInfo(CreateUpdateMixin):
    50
    +    course = models.ForeignKey(CourseInfo, verbose_name=_(u'course'), blank=True, null=True, help_text=u'课程', db_index=True)
    51
    +    course_video_id = ShortUUIDField(_(u'course_video_id'), max_length=255, help_text=u'课程视频唯一标识', db_index=True, unique=True)
    52
    +    course_video_type = models.CharField(_(u'course_video_type'), max_length=255, blank=True, null=True, help_text=u'课程视频类型')
    53
    +    course_video_name = models.CharField(_(u'course_video_name'), max_length=255, blank=True, null=True, help_text=u'课程视频标题')
    54
    +    course_video_desc = models.TextField(_(u'course_video_desc'), blank=True, null=True, help_text=u'课程视频描述')
    55
    +    course_video_time = models.IntegerField(_(u'course_video_time'), default=0, help_text=u'课程视频时间')
    56
    +    course_video_cover = models.ImageField(_(u'course_video_cover'), upload_to=upload_path, blank=True, null=True, help_text=u'课程视频缩略图')
    57
    +    course_video = models.FileField(_(u'course_video'), upload_to=upload_path, blank=True, null=True, help_text=u'课程视频')
    58
    +    course_video_position = models.IntegerField(_(u'course_video_position'), default=0, help_text=u'课程视频排序')
    59
    +
    60
    +    class Meta:
    61
    +        verbose_name = _(u'coursevideoinfo')
    62
    +        verbose_name_plural = _(u'coursevideoinfo')
    63
    +
    64
    +    def __unicode__(self):
    65
    +        return unicode(self.pk)
    66
    +
    67
    +    @property
    68
    +    def course_video_cover_url(self):
    69
    +        return upload_file_url(self.course_video_cover)
    70
    +
    71
    +    @property
    72
    +    def course_video_url(self):
    73
    +        return upload_file_url(self.course_video)
    74
    +
    75
    +    @property
    76
    +    def data(self):
    77
    +        return {
    78
    +            'course_id': self.course.course_id,
    79
    +            'course_video_id': self.course_video_id,
    80
    +            'course_video_type': self.course_video_type,
    81
    +            'course_video_name': self.course_video_name,
    82
    +            'course_video_desc': self.course_video_desc,
    83
    +            'course_video_time': self.course_video_time,
    84
    +            'course_video_cover_url': self.course_video_cover_url,
    85
    +            'course_video_url': self.course_video_url,
    86
    +            'course_video_position': self.course_video_position,
    87
    +        }

    + 7 - 0
    courses/tests.py

    @@ -0,0 +1,7 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +from __future__ import unicode_literals
    3
    +
    4
    +from django.test import TestCase
    5
    +
    6
    +
    7
    +# Create your tests here.

    + 7 - 0
    courses/views.py

    @@ -0,0 +1,7 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +from __future__ import unicode_literals
    3
    +
    4
    +from django.shortcuts import render
    5
    +
    6
    +
    7
    +# Create your views here.

    + 3 - 0
    isort.sh

    @@ -0,0 +1,3 @@
    1
    +#!/bin/bash
    2
    +
    3
    +isort -rc -sp . .

    + 23 - 0
    manage.py

    @@ -0,0 +1,23 @@
    1
    +#!/usr/bin/env python
    2
    +import os
    3
    +import sys
    4
    +
    5
    +
    6
    +if __name__ == "__main__":
    7
    +    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "course.settings")
    8
    +    try:
    9
    +        from django.core.management import execute_from_command_line
    10
    +    except ImportError:
    11
    +        # The above import may fail for some other reason. Ensure that the
    12
    +        # issue is really that Django is missing to avoid masking other
    13
    +        # exceptions on Python 2.
    14
    +        try:
    15
    +            import django
    16
    +        except ImportError:
    17
    +            raise ImportError(
    18
    +                "Couldn't import Django. Are you sure it's installed and "
    19
    +                "available on your PYTHONPATH environment variable? Did you "
    20
    +                "forget to activate a virtual environment?"
    21
    +            )
    22
    +        raise
    23
    +    execute_from_command_line(sys.argv)

    + 0 - 0
    page/__init__.py


    + 4 - 0
    page/admin.py

    @@ -0,0 +1,4 @@
    1
    +from django.contrib import admin
    2
    +
    3
    +
    4
    +# Register your models here.

    + 67 - 0
    page/code_views.py

    @@ -0,0 +1,67 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +
    3
    +from __future__ import division
    4
    +
    5
    +from django.conf import settings
    6
    +from django.core.urlresolvers import reverse
    7
    +from django.db import transaction
    8
    +from django.shortcuts import redirect, render
    9
    +from furl import furl
    10
    +
    11
    +from account.models import UserInfo
    12
    +from codes.models import CourseCodeInfo
    13
    +from course.decorators import check_token
    14
    +from utils.error.errno_utils import CourseCodeStatusCode, ProfileStatusCode
    15
    +from utils.error.response_utils import response
    16
    +
    17
    +
    18
    +@check_token
    19
    +@transaction.atomic
    20
    +def course_code(request):
    21
    +    unique_identifier = request.GET.get(settings.WECHAT_UNIQUE_IDENTIFICATION, '')
    22
    +
    23
    +    user, created = UserInfo.objects.select_for_update().get_or_create(**{settings.WECHAT_UNIQUE_IDENTIFICATION: unique_identifier})
    24
    +    user.unionid = request.GET.get('unionid', '')
    25
    +    user.openid = request.GET.get('openid', '')
    26
    +    user.nickname = request.GET.get('nickname', '')
    27
    +    user.avatar = request.GET.get('headimgurl', '')
    28
    +    user.save()
    29
    +
    30
    +    try:
    31
    +        course_code = CourseCodeInfo.objects.get(user_id=user.user_id, exchanged=True, status=True)
    32
    +    except CourseCodeInfo.DoesNotExist:
    33
    +        course_code = None
    34
    +
    35
    +    if course_code:
    36
    +        return redirect(furl(reverse('page:course_list')).add(request.GET).add({'user_id': user.user_id}).url)
    37
    +
    38
    +    return render(request, 'page/course_code.html', {
    39
    +        'domain': settings.DOMAIN,
    40
    +        'user_info': user.data,
    41
    +        'params': 'user_id={}&vtoken={}'.format(user.user_id, request.GET.get('vtoken', '')),
    42
    +    })
    43
    +
    44
    +
    45
    +@transaction.atomic
    46
    +def code_exchange(request):
    47
    +    user_id = request.POST.get('user_id', '')
    48
    +    code = request.POST.get('code', '')
    49
    +
    50
    +    try:
    51
    +        user = UserInfo.objects.select_for_update().get(user_id=user_id)
    52
    +    except UserInfo.DoesNotExist:
    53
    +        return response(ProfileStatusCode.PROFILE_NOT_FOUND)
    54
    +
    55
    +    try:
    56
    +        course_code = CourseCodeInfo.objects.select_for_update().get(code=code, status=True)
    57
    +    except CourseCodeInfo.DoesNotExist:
    58
    +        return response(CourseCodeStatusCode.COURSE_CODE_NOT_FOUND)
    59
    +
    60
    +    if course_code.exchanged:
    61
    +        return response(CourseCodeStatusCode.COURSE_CODE_HAS_EXCHANGED)
    62
    +
    63
    +    course_code.user_id = user.user_id
    64
    +    course_code.exchanged = True
    65
    +    course_code.save()
    66
    +
    67
    +    return response(200, 'Course Code Exchanged Success', u'课程兑换码兑换成功')

    + 47 - 0
    page/info_views.py

    @@ -0,0 +1,47 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +
    3
    +from __future__ import division
    4
    +
    5
    +from django.conf import settings
    6
    +from django.shortcuts import render
    7
    +
    8
    +from account.models import UserInfo
    9
    +from codes.models import CourseCodeInfo
    10
    +from course.decorators import check_token
    11
    +from courses.models import CourseInfo, CourseVideoInfo
    12
    +from utils.error.errno_utils import CourseCodeStatusCode, CourseStatusCode, ProfileStatusCode
    13
    +from utils.error.response_utils import response
    14
    +
    15
    +
    16
    +@check_token
    17
    +def course_info(request):
    18
    +    user_id = request.GET.get('user_id', '')
    19
    +    course_id = request.GET.get('course_id', '')
    20
    +
    21
    +    try:
    22
    +        user = UserInfo.objects.get(user_id=user_id, status=True)
    23
    +    except UserInfo.DoesNotExist:
    24
    +        return response(ProfileStatusCode.PROFILE_NOT_FOUND)
    25
    +
    26
    +    try:
    27
    +        course_code = CourseCodeInfo.objects.get(user_id=user.user_id, exchanged=True, status=True)
    28
    +    except CourseCodeInfo.DoesNotExist:
    29
    +        response(CourseCodeStatusCode.COURSE_CODE_NOT_FOUND)
    30
    +
    31
    +    try:
    32
    +        course = CourseInfo.objects.get(course_id=course_id)
    33
    +    except CourseInfo.DoesNotExist:
    34
    +        response(CourseStatusCode.COURSE_NOT_FOUND)
    35
    +
    36
    +    videos = CourseVideoInfo.objects.filter(course=course, status=True).order_by('course_video_position')
    37
    +    videos = [video.data for video in videos]
    38
    +
    39
    +    video_count = len(videos)
    40
    +
    41
    +    return render(request, 'page/course_info.html', {
    42
    +        'domain': settings.DOMAIN,
    43
    +        'video_default': videos[0] if video_count else '',
    44
    +        'video_count': video_count,
    45
    +        'videos': videos,
    46
    +        'params': 'user_id={}&vtoken={}'.format(user_id, request.GET.get('vtoken', '')),
    47
    +    })

    + 37 - 0
    page/list_views.py

    @@ -0,0 +1,37 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +
    3
    +from __future__ import division
    4
    +
    5
    +from django.conf import settings
    6
    +from django.shortcuts import render
    7
    +
    8
    +from account.models import UserInfo
    9
    +from codes.models import CourseCodeInfo
    10
    +from course.decorators import check_token
    11
    +from courses.models import CourseInfo
    12
    +from utils.error.errno_utils import CourseCodeStatusCode, ProfileStatusCode
    13
    +from utils.error.response_utils import response
    14
    +
    15
    +
    16
    +@check_token
    17
    +def course_list(request):
    18
    +    user_id = request.GET.get('user_id', '')
    19
    +
    20
    +    try:
    21
    +        user = UserInfo.objects.get(user_id=user_id, status=True)
    22
    +    except UserInfo.DoesNotExist:
    23
    +        return response(ProfileStatusCode.PROFILE_NOT_FOUND)
    24
    +
    25
    +    try:
    26
    +        course_code = CourseCodeInfo.objects.get(user_id=user.user_id, exchanged=True, status=True)
    27
    +    except CourseCodeInfo.DoesNotExist:
    28
    +        response(CourseCodeStatusCode.COURSE_CODE_NOT_FOUND)
    29
    +
    30
    +    courses = CourseInfo.objects.filter(status=True)
    31
    +    courses = [course.data for course in courses]
    32
    +
    33
    +    return render(request, 'page/course_list.html', {
    34
    +        'domain': settings.DOMAIN,
    35
    +        'courses': courses,
    36
    +        'params': 'user_id={}&vtoken={}'.format(user_id, request.GET.get('vtoken', '')),
    37
    +    })

    + 31 - 0
    page/migrations/0001_initial.py

    @@ -0,0 +1,31 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +# Generated by Django 1.11.3 on 2017-09-24 14:50
    3
    +from __future__ import unicode_literals
    4
    +
    5
    +from django.db import migrations, models
    6
    +import page.models
    7
    +
    8
    +
    9
    +class Migration(migrations.Migration):
    10
    +
    11
    +    initial = True
    12
    +
    13
    +    dependencies = [
    14
    +    ]
    15
    +
    16
    +    operations = [
    17
    +        migrations.CreateModel(
    18
    +            name='CourseCodeSettingInfo',
    19
    +            fields=[
    20
    +                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
    21
    +                ('status', models.BooleanField(db_index=True, default=True, help_text='\u72b6\u6001', verbose_name='status')),
    22
    +                ('created_at', models.DateTimeField(auto_now_add=True, help_text='\u521b\u5efa\u65f6\u95f4', verbose_name='created_at')),
    23
    +                ('updated_at', models.DateTimeField(auto_now=True, help_text='\u66f4\u65b0\u65f6\u95f4', verbose_name='updated_at')),
    24
    +                ('cover_image', models.ImageField(blank=True, help_text='\u5151\u6362\u8bfe\u7a0b\u9875\u56fe\u7247', null=True, upload_to=page.models.upload_path, verbose_name='cover_image')),
    25
    +            ],
    26
    +            options={
    27
    +                'verbose_name': 'coursecodesettinginfo',
    28
    +                'verbose_name_plural': 'coursecodesettinginfo',
    29
    +            },
    30
    +        ),
    31
    +    ]

    + 0 - 0
    page/migrations/__init__.py


    + 33 - 0
    page/models.py

    @@ -0,0 +1,33 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +
    3
    +import os
    4
    +
    5
    +from django.db import models
    6
    +from django.utils.translation import ugettext_lazy as _
    7
    +from TimeConvert import TimeConvert as tc
    8
    +
    9
    +from course.basemodels import CreateUpdateMixin
    10
    +from utils.url_utils import upload_file_url
    11
    +
    12
    +
    13
    +def upload_path(instance, old_filename):
    14
    +    return 'file/{ym}/{stamp}{ext}'.format(
    15
    +        ym=tc.local_string(format='%Y%m'),
    16
    +        stamp=tc.local_timestamp(ms=True),
    17
    +        ext=os.path.splitext(old_filename)[1].lower(),
    18
    +    )
    19
    +
    20
    +
    21
    +class CourseCodeSettingInfo(CreateUpdateMixin):
    22
    +    cover_image = models.ImageField(_(u'cover_image'), upload_to=upload_path, blank=True, null=True, help_text=u'兑换课程页图片')
    23
    +
    24
    +    class Meta:
    25
    +        verbose_name = _(u'coursecodesettinginfo')
    26
    +        verbose_name_plural = _(u'coursecodesettinginfo')
    27
    +
    28
    +    def __unicode__(self):
    29
    +        return unicode(self.pk)
    30
    +
    31
    +    @property
    32
    +    def cover_image_url(self):
    33
    +        return upload_file_url(self.cover_image)

    + 67 - 0
    page/static/page/css/weui.ext.css

    @@ -0,0 +1,67 @@
    1
    +/* Input valid or invalid */
    2
    +input:required:invalid {
    3
    +    color: #E64340;
    4
    +}
    5
    +input:required:valid {
    6
    +    color: rgb(0, 0, 0);
    7
    +}
    8
    +/* Input Placeholder */
    9
    + input::-webkit-input-placeholder, textarea::-webkit-input-placeholder {
    10
    +    font-size: 13px;
    11
    +}
    12
    +input:-moz-placeholder, textarea:-moz-placeholder {
    13
    +    font-size: 13px;
    14
    +}
    15
    +input::-moz-placeholder, textarea::-moz-placeholder {
    16
    +    font-size: 13px;
    17
    +}
    18
    +input:-ms-input-placeholder, textarea:-ms-input-placeholder {
    19
    +    font-size: 13px;
    20
    +}
    21
    +/* Radio Cells */
    22
    +.radio_cells {
    23
    +    margin-top: 0;
    24
    +    margin-left: 15px;
    25
    +}
    26
    +.radio_cells label {
    27
    +    padding: 8px 10px;
    28
    +    font-size: 15px;
    29
    +}
    30
    +/*.radio_cells>div:first-child .quartern:after {*/
    31
    +    /*border-left: none;*/
    32
    +/*}*/
    33
    +.radio_cells>div:last-child .quartern:after {
    34
    +    border-right: none;
    35
    +}
    36
    +/* Quartern */
    37
    +.quartern {
    38
    +    width: 25%;
    39
    +    box-sizing: border-box;
    40
    +    text-align: center;
    41
    +    border-radius: 5px;
    42
    +    float: left;
    43
    +}
    44
    +.quartern:after {
    45
    +    content: " ";
    46
    +    width: 200%;
    47
    +    height: 200%;
    48
    +    position: absolute;
    49
    +    top: 0;
    50
    +    left: 0;
    51
    +    border-right: 1px solid rgba(0, 0, 0, 0.2);
    52
    +    /*border-width: 0 1px 0 1px;*/
    53
    +    /*border-color: rgba(0, 0, 0, 0.2);*/
    54
    +    /*border-style: solid;*/
    55
    +    -webkit-transform: scale(0.5);
    56
    +            transform: scale(0.5);
    57
    +    -webkit-transform-origin: 0 0;
    58
    +            transform-origin: 0 0;
    59
    +    box-sizing: border-box;
    60
    +    border-radius: 10px;
    61
    +}
    62
    +/* Radio Checked Relative */
    63
    +.weui_check:checked + .quartern {
    64
    +    color: white;
    65
    +    background: #04BE02;
    66
    +    border-width: 0;
    67
    +}

    BIN
    page/static/page/img/code_cover.png


    + 135 - 0
    page/templates/page/course_code.html

    @@ -0,0 +1,135 @@
    1
    +{% load staticfiles %}
    2
    +
    3
    +<!DOCTYPE html>
    4
    +<html lang="zh-CN">
    5
    +    <head>
    6
    +        <meta charset="utf-8">
    7
    +        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    8
    +        <meta name="format-detection" content="telephone=no,email=no,address=no">
    9
    +        <meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no">
    10
    +        <title>课程兑换</title>
    11
    +
    12
    +        <link href="//res.wx.qq.com/open/libs/weui/0.4.3/weui.min.css" rel="stylesheet" type="text/css" />
    13
    +{#        <link href="{% static 'page/css/weui.ext.css' %}?v=1" rel="stylesheet" type="text/css" />#}
    14
    +
    15
    +        <style>
    16
    +            .code-cover, .code-cover>img {
    17
    +                width: 100%;
    18
    +            }
    19
    +            .code-area {
    20
    +                padding: 5px 10px;
    21
    +            }
    22
    +            .code-label {
    23
    +                width: 70%;
    24
    +                color: #c6c6c6;
    25
    +                font-size: 13px;
    26
    +                margin: 15px auto;
    27
    +            }
    28
    +            .code-input, .code-input>input, .code-submit {
    29
    +                width: 80%;
    30
    +                height: 40px;
    31
    +                line-height: 40px;
    32
    +                margin: 5px auto;
    33
    +                border-radius: 25px;
    34
    +                box-sizing: border-box;
    35
    +            }
    36
    +            .code-input>input {
    37
    +                width: 100%;
    38
    +                border: 1px solid #c6c6c6;
    39
    +                padding: 0 15px;
    40
    +                outline: medium;
    41
    +            }
    42
    +            .code-submit {
    43
    +                text-align: center;
    44
    +                background: #20a1f5;
    45
    +                color: #c6eaf9;
    46
    +                margin-top: 15px;
    47
    +            }
    48
    +        </style>
    49
    +    </head>
    50
    +    <body>
    51
    +        <div class="container">
    52
    +            <div class="code-cover"><img src="{% static 'page/img/code_cover.png' %}"></div>
    53
    +            <div class="code-area">
    54
    +                <div class="code-label">输入兑换码兑换课程</div>
    55
    +                <div class="code-input"><input id="code" placeholder="请输入兑换码"></div>
    56
    +                <div id="submit" class="code-submit">确认兑换</div>
    57
    +            </div>
    58
    +
    59
    +            <div class="weui_dialog_alert" id="dialog" style="display: none">
    60
    +                <div class="weui_mask"></div>
    61
    +                <div class="weui_dialog">
    62
    +                    <div class="weui_dialog_hd"><strong id="title" class="weui_dialog_title">弹窗标题</strong></div>
    63
    +                    <div id="content" class="weui_dialog_bd">弹窗内容,告知当前页面信息等</div>
    64
    +                    <div class="weui_dialog_ft">
    65
    +                        <a href="javascript:;" class="weui_btn_dialog primary">确定</a>
    66
    +                    </div>
    67
    +                </div>
    68
    +            </div>
    69
    +        </div>
    70
    +
    71
    +        <script src="//cdn.bootcss.com/zepto/1.1.6/zepto.min.js"></script>
    72
    +        <script>
    73
    +            $(function() {
    74
    +                function show_error_dialog(title, content) {
    75
    +                    $('#dialog #title').text(title);
    76
    +                    $('#dialog #content').text(content);
    77
    +                    $('#dialog').show();
    78
    +                }
    79
    +
    80
    +                function data_check() {
    81
    +                    var user_id = '{{ user_info.user_id }}';
    82
    +                    if (!user_id) {
    83
    +                        show_error_dialog('微信授权', '微信授权失败,请重新打开页面');
    84
    +                        return false;
    85
    +                    }
    86
    +
    87
    +                    var code = $('#code').val();
    88
    +                    if (!code) {
    89
    +                        show_error_dialog('兑换码', '兑换码错误,请检查重新输入');
    90
    +                        return false;
    91
    +                    }
    92
    +
    93
    +                    return {
    94
    +                        user_id: user_id,
    95
    +                        code: code,
    96
    +                    }
    97
    +                }
    98
    +
    99
    +                $('#submit').click(function () {
    100
    +                    var check_result = data_check();
    101
    +                    if (check_result){
    102
    +                        $.ajax({
    103
    +                            type: 'POST',
    104
    +                            url: '{{ domain }}/api/code/exchange',
    105
    +                            data: check_result,
    106
    +                            success: function(data) {
    107
    +                                if (data.status == 200) {
    108
    +                                    window.location.href = '{{ domain }}/page/course/list?{{ params|safe }}';
    109
    +                                } else {
    110
    +                                    show_error_dialog('错误', data.description);
    111
    +                                }
    112
    +                            }
    113
    +                        })
    114
    +                    }
    115
    +                });
    116
    +
    117
    +                $('#dialog .weui_btn_dialog').click(function () {
    118
    +                    $('#dialog').hide();
    119
    +                })
    120
    +            });
    121
    +        </script>
    122
    +        <script type="text/javascript" src="//res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>
    123
    +        <script type="text/javascript" src="{% static 'course/js/jswe-0.0.1.js' %}"></script>
    124
    +        <script>
    125
    +            V.initWxData({
    126
    +                imgUrl: 'http://pai.ai/static/pai2/img/paiai_96_96.png',
    127
    +                link: 'http://api.pai.ai/wx_oauth2?redirect_url=http://tamron.xfoto.com.cn/page/clerk',
    128
    +                desc: '店员授权',
    129
    +                title: '店员授权',
    130
    +                timeLine: ''
    131
    +            }, true);
    132
    +{#            V.hideOptionMenu();#}
    133
    +        </script>
    134
    +    </body>
    135
    +</html>

    + 124 - 0
    page/templates/page/course_info.html

    @@ -0,0 +1,124 @@
    1
    +{% load staticfiles %}
    2
    +
    3
    +<!DOCTYPE html>
    4
    +<html lang="zh-CN">
    5
    +    <head>
    6
    +        <meta charset="utf-8">
    7
    +        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    8
    +        <meta name="format-detection" content="telephone=no,email=no,address=no">
    9
    +        <meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no">
    10
    +        <title>课程详情</title>
    11
    +
    12
    +        <link href="//res.wx.qq.com/open/libs/weui/0.4.3/weui.min.css" rel="stylesheet" type="text/css" />
    13
    +        <link href="{% static 'page/css/weui.ext.css' %}?v=1" rel="stylesheet" type="text/css" />
    14
    +
    15
    +        <style>
    16
    +            .container {
    17
    +                position: absolute;
    18
    +                top: 0;
    19
    +                right: 0;
    20
    +                bottom: 0;
    21
    +                left: 0;
    22
    +                background: #efefef;
    23
    +            }
    24
    +            .video_wrapper, .video_select {
    25
    +                width: 100%;
    26
    +                background: #fff;
    27
    +                margin-bottom: 10px;
    28
    +                box-sizing: border-box;
    29
    +            }
    30
    +            .video_text, .video_select {
    31
    +                padding: 15px;
    32
    +            }
    33
    +            .course_video_name, .video_select_text {
    34
    +                font-size: 18px;
    35
    +                font-weight: bold;
    36
    +                color: #020001;
    37
    +                padding-bottom: 10px;
    38
    +            }
    39
    +            .course_video_desc {
    40
    +                font-size: 12px;
    41
    +                color: #999;
    42
    +            }
    43
    +            .video_select_item {
    44
    +                width: 100%;
    45
    +                text-align: center;
    46
    +                height: 40px;
    47
    +                line-height: 40px;
    48
    +                color: #020001;
    49
    +                border: 1px solid #e1e1e1;
    50
    +                border-radius: 5px;
    51
    +                margin-bottom: 15px;
    52
    +            }
    53
    +            .video_selected {
    54
    +                color: #ce8f8a !important;
    55
    +                border: 1px solid #ce8f8a;
    56
    +            }
    57
    +        </style>
    58
    +    </head>
    59
    +    <body>
    60
    +        <div class="container" >
    61
    +            <div class="video_wrapper">
    62
    +                <video id="video" width="100%" height="100%" autoplay controls x-webkit-airplay="true" webkit-playsinline="" playsinline="true" preload="none" poster="" src="{{ video_default.course_video_url }}" data-cursrc="1"></video>
    63
    +                <div class="video_text">
    64
    +                    <div class="course_video_name">{{ video_default.course_video_name }}</div>
    65
    +                    <div class="course_video_desc">{{ video_default.course_video_desc }}</div>
    66
    +                </div>
    67
    +            </div>
    68
    +
    69
    +            <div class="video_select">
    70
    +                <div class="video_select_text">选择视频</div>
    71
    +                {% for video in videos %}
    72
    +                    <div id="video{{ forloop.counter }}" class="video_select_item {% ifequal forloop.counter 1 %}video_selected{% endifequal %}" data-src="{{ video.course_video_url }}">{{ video.course_video_type }}</div>
    73
    +                {% endfor %}
    74
    +            </div>
    75
    +        </div>
    76
    +
    77
    +        <script src="//cdn.bootcss.com/zepto/1.1.6/zepto.min.js"></script>
    78
    +        <script src="//cdn.bootcss.com/video.js/6.2.8/video.min.js"></script>
    79
    +        <script>
    80
    +            $(function() {
    81
    +                var video_count = {{ video_count }};
    82
    +
    83
    +                $('.video_select_item').click(function () {
    84
    +                    $this = $(this);
    85
    +                    $('.video_select_item').removeClass('video_selected');
    86
    +                    $this.addClass('video_selected');
    87
    +                    $('#video').attr('src', $this.attr('data-src'));
    88
    +                })
    89
    +
    90
    +                $('#video')[0].onended = function() {
    91
    +                    var curscr = $(this).attr('data-cursrc');
    92
    +                    if (curscr >= video_count) {
    93
    +                        return
    94
    +                    }
    95
    +                    var next_video = $('#video' + (parseInt(curscr) + 1));
    96
    +                    $('#video').attr('src', next_video.attr('data-src'));
    97
    +                    $('.video_select_item').removeClass('video_selected');
    98
    +                    next_video.addClass('video_selected');
    99
    +                };
    100
    +            });
    101
    +        </script>
    102
    +        <script type="text/javascript" src="//res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>
    103
    +        <script type="text/javascript" src="{% static 'course/js/jswe-0.0.1.js' %}"></script>
    104
    +        <script>
    105
    +            V.initWxData({
    106
    +                imgUrl: 'http://pai.ai/static/pai2/img/paiai_96_96.png',
    107
    +                link: 'http://api.pai.ai/wx_oauth2?redirect_url=http://tamron.xfoto.com.cn/page/clerk',
    108
    +                desc: '店员授权',
    109
    +                title: '店员授权',
    110
    +                timeLine: ''
    111
    +            }, true);
    112
    +            V.hideOptionMenu();
    113
    +
    114
    +            $('#scan').click(function () {
    115
    +                V.scanQRCode({
    116
    +                    needResult: 1
    117
    +                });
    118
    +            });
    119
    +            V.wxScanQRCodeSuccess = function (res) {
    120
    +                $('#code').val(V.parseScanQRCodeResultStr(res.resultStr));
    121
    +            }
    122
    +        </script>
    123
    +    </body>
    124
    +</html>

    + 77 - 0
    page/templates/page/course_list.html

    @@ -0,0 +1,77 @@
    1
    +{% load staticfiles %}
    2
    +
    3
    +<!DOCTYPE html>
    4
    +<html lang="zh-CN">
    5
    +    <head>
    6
    +        <meta charset="utf-8">
    7
    +        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    8
    +        <meta name="format-detection" content="telephone=no,email=no,address=no">
    9
    +        <meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no">
    10
    +        <title>课程列表</title>
    11
    +
    12
    +        <link href="//res.wx.qq.com/open/libs/weui/0.4.3/weui.min.css" rel="stylesheet" type="text/css" />
    13
    +        <link href="{% static 'page/css/weui.ext.css' %}?v=1" rel="stylesheet" type="text/css" />
    14
    +
    15
    +        <style>
    16
    +            .container {
    17
    +                position: absolute;
    18
    +                top: 0;
    19
    +                right: 0;
    20
    +                bottom: 0;
    21
    +                left: 0;
    22
    +                background: #efefef;
    23
    +            }
    24
    +            .course_wrapper {
    25
    +                text-align: center;
    26
    +                background: white;
    27
    +                margin-bottom: 20px;
    28
    +                padding: 15px 0;
    29
    +            }
    30
    +            .course_name {
    31
    +                font-size: 18px;
    32
    +                font-weight: bold;
    33
    +                color: #020001;
    34
    +            }
    35
    +            .course_time {
    36
    +                font-size: 12px;
    37
    +                color: #999;
    38
    +            }
    39
    +            .course_cover>img {
    40
    +                width: 80%;
    41
    +                border-radius: 5px;
    42
    +            }
    43
    +        </style>
    44
    +    </head>
    45
    +    <body>
    46
    +        <div class="container" >
    47
    +            {% for course in courses %}
    48
    +                <div class="course_wrapper" data-courseid="{{ course.course_id }}">
    49
    +                    <div class="course_name">{{ course.course_name }}</div>
    50
    +                    <div class="course_time">{{ course.course_time }}分钟</div>
    51
    +                    <div class="course_cover"><img src="{{ course.course_cover_url }}"></div>
    52
    +                </div>
    53
    +            {% endfor %}
    54
    +        </div>
    55
    +
    56
    +        <script src="//cdn.bootcss.com/zepto/1.1.6/zepto.min.js"></script>
    57
    +        <script>
    58
    +            $(function() {
    59
    +                $('.course_wrapper').click(function () {
    60
    +                    window.location.href = '{{ domain }}/page/course/info?course_id=' + $(this).attr('data-courseid') + '&{{ params|safe }}';
    61
    +                })
    62
    +            });
    63
    +        </script>
    64
    +        <script type="text/javascript" src="//res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>
    65
    +        <script type="text/javascript" src="{% static 'course/js/jswe-0.0.1.js' %}"></script>
    66
    +        <script>
    67
    +            V.initWxData({
    68
    +                imgUrl: 'http://pai.ai/static/pai2/img/paiai_96_96.png',
    69
    +                link: 'http://api.pai.ai/wx_oauth2?redirect_url=http://tamron.xfoto.com.cn/page/clerk',
    70
    +                desc: '店员授权',
    71
    +                title: '店员授权',
    72
    +                timeLine: ''
    73
    +            }, true);
    74
    +{#            V.hideOptionMenu();#}
    75
    +        </script>
    76
    +    </body>
    77
    +</html>

    + 4 - 0
    page/tests.py

    @@ -0,0 +1,4 @@
    1
    +from django.test import TestCase
    2
    +
    3
    +
    4
    +# Create your tests here.

    + 12 - 0
    page/urls.py

    @@ -0,0 +1,12 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +
    3
    +from django.conf.urls import url
    4
    +
    5
    +from page import code_views, info_views, list_views
    6
    +
    7
    +
    8
    +urlpatterns = [
    9
    +    url(r'^course/code$', code_views.course_code, name='course_code'),
    10
    +    url(r'^course/list$', list_views.course_list, name='course_list'),
    11
    +    url(r'^course/info$', info_views.course_info, name='course_info'),
    12
    +]

    + 4 - 0
    page/views.py

    @@ -0,0 +1,4 @@
    1
    +from django.shortcuts import render
    2
    +
    3
    +
    4
    +# Create your views here.

    + 9 - 0
    pep8.sh

    @@ -0,0 +1,9 @@
    1
    +#!/bin/bash
    2
    +
    3
    +# Ignoring autogenerated files
    4
    +#  -- Migration directories
    5
    +# Ignoring error codes
    6
    +#  -- E128 continuation line under-indented for visual indent
    7
    +#  -- E501 line too long
    8
    +
    9
    +pep8 --exclude=migrations --ignore=E128,E501 .

    + 40 - 0
    requirements.txt

    @@ -0,0 +1,40 @@
    1
    +-e git+https://github.com/Brightcells/django-q.git#egg=django-q
    2
    +CodeConvert==2.0.4
    3
    +Django==1.11.3
    4
    +MySQL-python==1.2.5
    5
    +Pillow==3.4.2
    6
    +StatusCode==1.0.0
    7
    +TimeConvert==1.4.1
    8
    +cryptography==2.0.3
    9
    +django-curtail-uuid==1.0.0
    10
    +django-detect==1.0.5
    11
    +django-file-md5==1.0.1
    12
    +django-ip==1.0.1
    13
    +django-json-response==1.1.5
    14
    +django-logit==1.0.6
    15
    +django-multidomain==1.1.4
    16
    +django-paginator2==1.0.3
    17
    +django-rlog==1.0.7
    18
    +django-shortuuidfield==0.1.3
    19
    +django-six==1.0.2
    20
    +django-uniapi==1.0.0
    21
    +django-we==1.0.14
    22
    +djangorestframework==3.6.3
    23
    +furl==1.0.1
    24
    +hiredis==0.2.0
    25
    +isoweek==1.3.3
    26
    +jsonfield==2.0.2
    27
    +mock==2.0.0
    28
    +pep8==1.7.0
    29
    +pysnippets==1.0.4
    30
    +pywe-miniapp==1.0.0
    31
    +pywe-oauth==1.0.5
    32
    +pywe-response==1.0.1
    33
    +qiniu==7.1.5
    34
    +redis==2.10.6
    35
    +redis-extensions==1.1.1
    36
    +requests==2.18.4
    37
    +rlog==0.2
    38
    +shortuuid==0.5.0
    39
    +uWSGI==2.0.15
    40
    +versions==0.10.0

    + 0 - 0
    utils/__init__.py


    + 0 - 0
    utils/error/__init__.py


    + 51 - 0
    utils/error/errno_utils.py

    @@ -0,0 +1,51 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +
    3
    +from StatusCode import BaseStatusCode, StatusCodeField
    4
    +
    5
    +
    6
    +class ProfileStatusCode(BaseStatusCode):
    7
    +    """ 用户相关错误码 4000xx """
    8
    +    PROFILE_NOT_FOUND = StatusCodeField(400001, 'Profile Not Found', description=u'用户不存在')
    9
    +
    10
    +
    11
    +class CourseCodeStatusCode(BaseStatusCode):
    12
    +    """ 课程兑换码相关错误码 4001xx """
    13
    +    COURSE_CODE_NOT_FOUND = StatusCodeField(400101, 'Course Code Not Found', description=u'课程兑换码不存在')
    14
    +    COURSE_CODE_HAS_EXCHANGED = StatusCodeField(400102, 'Course Code Has Exchanged', description=u'课程兑换码已兑换')
    15
    +
    16
    +
    17
    +class CourseStatusCode(BaseStatusCode):
    18
    +    """ 课程相关错误码 4002xx """
    19
    +    COURSE_NOT_FOUND = StatusCodeField(400201, 'Course Not Found', description=u'课程不存在')
    20
    +
    21
    +
    22
    +class OrderStatusCode(BaseStatusCode):
    23
    +    """ 订单/支付相关错误码 4040xx """
    24
    +    WX_UNIFIED_ORDER_FAIL = StatusCodeField(404000, 'WX Unified Order Fail', description=u'微信统一下单失败')
    25
    +    WX_ORDER_NOT_FOUND = StatusCodeField(404001, 'WX Order Not Found', description=u'订单不存在')
    26
    +    WX_ORDER_NOT_PAY = StatusCodeField(404002, 'WX Order Not Pay', description=u'订单未支付')
    27
    +    WX_ORDER_PAYING = StatusCodeField(404003, 'WX Order Paying', description=u'订单支付中')
    28
    +    WX_ORDER_PAY_FAIL = StatusCodeField(404009, 'WX Order Pay Fail', description=u'微信支付失败')
    29
    +    SIGN_CHECK_FAIL = StatusCodeField(404010, 'Sign Check Fail', description=u'签名校验失败')
    30
    +    FEE_CHECK_FAIL = StatusCodeField(404011, 'FEE Check Fail', description=u'金额校验失败')
    31
    +    NO_DETAIL_PERMISSION = StatusCodeField(404015, 'No Detail Permission', description=u'无详情权限')
    32
    +    WX_ORDER_PAID_ALREADY_EXISTS = StatusCodeField(404020, 'WX Order Paid Already Exists', description=u'照片已购买')
    33
    +
    34
    +
    35
    +class PayStatusCode(BaseStatusCode):
    36
    +    """ 支付相关错误码 4041xx """
    37
    +
    38
    +
    39
    +class WithdrawStatusCode(BaseStatusCode):
    40
    +    """ 提现相关错误码 4042xx """
    41
    +    BALANCE_NOT_ENOUGH = StatusCodeField(404200, 'Balance Not Enough', description=u'提现金额不足')
    42
    +
    43
    +
    44
    +class MessageStatusCode(BaseStatusCode):
    45
    +    """ 消息相关错误码 4090xx """
    46
    +    MESSAGE_NOT_FOUND = StatusCodeField(409001, 'Message Not Found', description=u'消息不存在')
    47
    +
    48
    +
    49
    +class TokenStatusCode(BaseStatusCode):
    50
    +    """ 票据相关错误码 4090xx """
    51
    +    TOKEN_NOT_FOUND = StatusCodeField(409901, 'Token Not Found', description=u'票据不存在')

    + 18 - 0
    utils/error/response_utils.py

    @@ -0,0 +1,18 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +
    3
    +from django.http import JsonResponse
    4
    +from StatusCode import StatusCodeField
    5
    +
    6
    +
    7
    +def response_data(status_code=200, message=None, description=None, data={}, **kwargs):
    8
    +    return dict({
    9
    +        'status': status_code,
    10
    +        'message': message,
    11
    +        'description': description,
    12
    +        'data': data,
    13
    +    }, **kwargs)
    14
    +
    15
    +
    16
    +def response(status_code=200, message=None, description=None, data={}, **kwargs):
    17
    +    message, description = (message or status_code.message, description or status_code.description) if isinstance(status_code, StatusCodeField) else (message, description)
    18
    +    return JsonResponse(response_data(status_code, message, description, data, **kwargs), safe=False)

    + 0 - 0
    utils/redis/__init__.py


    + 6 - 0
    utils/redis/connect.py

    @@ -0,0 +1,6 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +
    3
    +from django.conf import settings
    4
    +
    5
    +
    6
    +r = settings.REDIS_CACHE

    + 68 - 0
    utils/redis/rkeys.py

    @@ -0,0 +1,68 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +
    3
    +# 唯一标识相关
    4
    +UUID_LIST = 'uuid:list'  # List,唯一标识列表
    5
    +
    6
    +# 用户相关
    7
    +PROFILE_INFO = 'profile:info:%s'  # STRING,用户信息,user_id
    8
    +
    9
    +# 导游相关
    10
    +TOUR_GUIDE_GROUP_GEO_INFO = 'tour:guide:group:geo:info:%s'  # ZSET,旅游团地理位置信息,group_id
    11
    +TOUR_GUIDE_GROUP_GEO_SUBMIT_DT = 'tour:guide:group:geo:submit:dt:%s'  # ZSET,旅游团地理位置最后上传时间,group_id
    12
    +TOUR_GUIDE_GROUP_CUR_SESSION = 'tour:guide:group:cur:session:%s'  # STRING,旅游团当前Session,group_id,导游设置集合时间的时候更新
    13
    +TOUR_GUIDE_GROUP_CUR_GATHER_INFO = 'tour:guide:group:cur:gather:info:%s'  # STRING,旅游团当前Session,group_id,导游设置集合时间的时候更新
    14
    +TOUR_GUIDE_GROUP_USER_GEO_LIST = 'tour:guide:group:user:geo:list:%s:%s:%s'  # LIST,旅游团当前用户地理位置列表,group_id、session_id、user_id
    15
    +
    16
    +TOUR_GUIDE_GROUP_USER_OWN = 'tour:guide:group:user:own:%s'  # STRING,导游当前拥有的旅行团,user_id,导游创建旅行团的时候更新
    17
    +TOUR_GUIDE_GROUP_USER_BELONG = 'tour:guide:group:user:belong:%s'  # STRING,用户当前所属旅行团,user_id,用户加入旅行团的时候更新
    18
    +
    19
    +# 群组相关
    20
    +GROUP_INFO = 'group:info:%s'  # STRING,群组信息,group_id
    21
    +
    22
    +# 群组用户相关
    23
    +GROUP_USERS_INFO = 'group:users:info:%s'  # STRING,群组用户信息,group_id
    24
    +GROUP_USERS_KV_INFO = 'group:users:kv:info:%s'  # STRING,群组用户信息,group_id
    25
    +GROUP_USERS_APPLYING_SET = 'group:users:applying:set:%s'  # SET,群组用户申请集合,group_id
    26
    +GROUP_USERS_PASSED_SET = 'group:users:passed:set:%s'  # SET,群组用户通过集合,group_id
    27
    +GROUP_USERS_REFUSED_SET = 'group:users:refused:set:%s'  # SET,群组用户拒绝集合,group_id
    28
    +GROUP_USERS_DELETED_SET = 'group:users:deleted:set:%s'  # SET,群组用户移除集合,group_id
    29
    +GROUP_USERS_QUIT_SET = 'group:users:quit:set:%s'  # SET,群组用户退出集合,group_id
    30
    +
    31
    +# 群组照片相关
    32
    +GROUP_PHOTO_DATA = 'group:photo:data:%s'  # STRING,群组数据记录,group_id
    33
    +GROUP_PHOTO_THUMB_UP = 'group:photo:thumb:up:%s:%s'  # STRING,群组照片用户点赞记录,photo_id、user_id
    34
    +GROUP_PHOTO_COMMENT_LIST = 'group:photo:comment:list:%s'  # STRING,群组照片用户评论列表,photo_id
    35
    +GROUP_PHOTO_THUMB_UP_LIST = 'group:photo:thumb:up:list:%s'  # STRING,群组照片用户点赞列表,photo_id
    36
    +GROUP_PHOTO_WATCHER_SET = 'group:photo:watcher:set:%s'  # SET,群组照片用户关注集合,photo_id,关注即评论点赞
    37
    +GROUP_LAST_PHOTO_PK = 'group:last:photo:pk:%s'  # STRING,群组最后一张照片PK,group_id
    38
    +
    39
    +# 摄影师照片相关
    40
    +LENSMAN_PHOTO_ORDER_RECORD = 'lensman:photo:order:record:%s:%s'  # STRING,摄影师照片购买记录,photo_id、user_id
    41
    +
    42
    +# 摄影师简报相关
    43
    +# 收入
    44
    +TOTAL_INCOME = 'total:income:%s:%s'  # STRING,总收入,user_id、photo_type
    45
    +WEEK_INCOME = 'week:income:%s:%s:%s'  # STRING,周收入,user_id、photo_type、Week.thisweek().isoformat()
    46
    +TODAY_INCOME = 'today:income:%s:%s:%s'  # STRING,日收入,user_id、photo_type、tc.local_string(format='%Y%m%d')
    47
    +# 上传
    48
    +TODAY_UPLOAD_PHOTO_AMOUNT = 'today:upload:photo:amount:%s:%s'  # STRING,日上传照片数量,user_id、tc.local_string(format='%Y%m%d')
    49
    +# 售出
    50
    +WEEK_SOLD = 'week:sold:%s:%s:%s'  # STRING,周售出,user_id、photo_type、Week.thisweek().isoformat()
    51
    +
    52
    +# 摄影师定价相关
    53
    +LENSMAN_PHOTO_PRICE_FIXED = 'lensman:photo:price:fixed:%s'  # STRING,摄影师照片定价(单位:分),user_id
    54
    +
    55
    +# 系统消息相关
    56
    +SYSTEM_MESSAGE_READ_INFO = 'system:message:read:info:%s'  # STRING,系统消息读取信息,user_id
    57
    +SYSTEM_MESSAGE_DELETED_INFO = 'system:message:deleted:info:%s'  # STRING,系统消息删除信息,user_id
    58
    +
    59
    +# 游客入口相关
    60
    +GUEST_ENTRANCE_CONTROL_INFO = 'guest:entrance:control:info:%s'  # STRING,游客入口控制信息,src
    61
    +
    62
    +# APP 相关
    63
    +LATEST_APP_INFO = 'latest:app:info:%s'  # STRING,最新 APP 信息,src
    64
    +APP_SETTINGS_INFO = 'app:settings:info:%s:%s:%s'  # STRING,APP 设置信息,platform、channel、version
    65
    +APP_PATCH_INFO = 'app:patch:info:%s:%s:%s'  # STRING,APP 补丁信息,platform、version、src
    66
    +
    67
    +# BOX 相关
    68
    +BOX_PROGRAM_VERSION_INFO = 'box:program:version:info'  # STRING,BOX 程序版本信息

    + 7 - 0
    utils/url_utils.py

    @@ -0,0 +1,7 @@
    1
    +# -*- coding: utf-8 -*-
    2
    +
    3
    +from django.conf import settings
    4
    +
    5
    +
    6
    +def upload_file_url(file_path):
    7
    +    return file_path and ('{}{}'.format(settings.DOMAIN, file_path.url)) or ''

    Pierakstīties - Gogs: Go Git Service

    Pierakstīties

    Aizmirsi paroli?