class FormIdViewSet(viewsets.ViewSet): '''微信 FormID 服务 错误码33xxx''' schema = CustomSchema( manual_fields={ 'POST:/formid/': [ coreapi.Field("formid", required=True, location="form", description='FormId 数组'), coreapi.Field("openid", required=True, location="form", description='OpenID'), ], }) def create(self, request, format=None): '''上传 FormId,存入数据库''' formid = request.data.get('formid', []) openid = request.data.get('openid', '') # 检查 openid try: record_openid = WechatBasic.objects.get(openid=openid) except WechatBasic.DoesNotExist: return Response(res_format(None, code=33001, msg='OpenId 不存在')) formid_count = len(formid) # 格式化检测 record_list = [] for item in formid: if len(item['formId']) != 32: continue record_list.append( FormIdGroup(openId=record_openid, formId=item['formId'], logFrom=item['logFrom'], logTime=item['logTime'], logCount=formid_count)) # 保存 FormIdGroup.objects.bulk_create(record_list) return Response(res_format({'count': len(record_list)}))
class BookManageViewSet(viewsets.ViewSet): '''图书管理相关服务 需认证 错误码23xxx''' authentication_classes = [JWTAuthentication] permission_classes = [permissions.IsAuthenticated] schema = CustomSchema( manual_fields={ 'POST:/book/publish/': [ coreapi.Field("description", required=True, location="form", description='用户描述,不可为空,最多 300 字'), coreapi.Field("price", required=True, location="form", description='出售价格'), coreapi.Field("images", required=True, location="form", description='图书图片'), coreapi.Field("knife", required=True, location="form", description='是否可小刀', schema=coreschema.Boolean()), coreapi.Field("phase", required=True, location="form", description='品相,L10/L9/L7/L5'), coreapi.Field("status", required=True, location="form", description='发布状态,normal:正常;draft:草稿'), coreapi.Field("bookISBN", required=True, location="form", description='图书 ISBN 码'), coreapi.Field("categoryL1", required=True, location="form", description='图书类别,大类'), coreapi.Field("categoryL2", required=True, location="form", description='图书类别,小类'), ], 'POST:/book/status/': [ coreapi.Field("bid", required=True, location="form", description='图书发布信息唯一标识 BID'), coreapi.Field( "status", required=True, location="form", description='改变状态:normal-重新发布、delete-删除、removed-下架、sold-出售。' ), ], 'POST:/book/collect/': [ coreapi.Field("bid", required=True, location="form", description='图书发布信息唯一标识 BID'), coreapi.Field("status", required=True, location="form", description='收藏:normal;取消收藏:delete'), ], 'GET:/book/collection/': [], 'POST:/book/comment/': [ coreapi.Field("bid", required=True, location="form", description='图书发布信息唯一标识 BID'), coreapi.Field("toUser", required=False, location="form", description='回复的用户的 UID'), coreapi.Field("comment", required=True, location="form", description='评论内容'), ], 'POST:/book/comment/delete/': [ coreapi.Field("cid", required=True, location="form", description='评论信息唯一标识'), ], }) @action(methods=['post'], detail=False, url_path='publish') def publish(self, request, format=None): '''发布图书信息''' user = request.user # 获取及检查参数 params = [ 'description', 'price', 'images', 'knife', 'phase', 'status', 'bookISBN', 'categoryL1', 'categoryL2' ] description, price, images, knife, phase, status, bookISBN, categoryL1, categoryL2 = get_dict_values( request.data, params) if check_none(description, price, images, knife, phase, status, bookISBN, categoryL1, categoryL2): raise ParseError('缺少参数:' + ', '.join(params)) # 发布状态,normal:正常;draft:草稿 if status not in ['normal', 'draft']: return Response(res_format(None, code=23003, msg='发布失败,发布状态错误')) # 检查发布信息中是否有 bookInfo,如果有就是用户完善的信息 # 没有就通过 ISBN 查询图书信息 user_isbn_info = request.data.get('bookInfo', None) if user_isbn_info is None: dataSource = 'douban' try: record_isbn = ISBNInfo.objects.get(isbn=bookISBN) isbn_info = ISBNInfoSerializers(record_isbn).data except ISBNInfo.DoesNotExist: return Response( res_format(None, code=23001, msg='发布失败,请重新扫描 ISBN 码获取信息')) else: dataSource = 'user' # 将空字符串设置为 None for key in user_isbn_info: if user_isbn_info[key] == '': user_isbn_info[key] = None # 序列化数据,并校验 isbn_info_serializer = ISBNInfoSerializers(data=user_isbn_info) if not isbn_info_serializer.is_valid(): return Response( res_format(None, code=23011, msg='完善的图书信息格式错误,请修改')) else: isbn_info = isbn_info_serializer.data # 发布信息模块 publish_params = { 'uid': user, 'bookTitle': (isbn_info['title'] if isbn_info['title'] is not None else '') + (' : ' + isbn_info['subtitle'] if isbn_info['subtitle'] is not None else ''), 'bookAuthor': isbn_info['author'], 'bookCover': isbn_info['cover'], 'bookPress': isbn_info['press'], 'bookPublishYear': isbn_info['publishYear'], 'bookPrice': isbn_info['price'], 'bookIntroduction': isbn_info['introduction'][:512] if isbn_info['introduction'] else None, 'bookRatingValue': isbn_info['ratingValue'], 'dataSource': dataSource } # 如果该图书没有标题,就设为 None if publish_params['bookTitle'] == '': publish_params['bookTitle'] = None publish_params.update(request.data) record_publish = PublishBookSerializers(data=publish_params) if not record_publish.is_valid(): errors = record_publish.errors return Response(res_format(errors, code=23002, msg='发布信息格式错误')) record_publish.save() return Response( res_format({'bid': record_publish.data['bid']}, msg='发布成功')) @action(methods=['post'], detail=False, url_path='status') def status(self, request, format=None): '''更新图书状态,normal-重新发布、delete-删除、removed-下架''' user = request.user # 获取及检查参数 params = ['bid', 'status'] bid, status = get_dict_values(request.data, params) if check_none(bid, status): raise ParseError('缺少参数:' + ', '.join(params)) # 检查是否是更新合法的图书状态 status_dict = { 'normal': '重新发布', 'delete': '删除', 'sold': '出售', 'removed': '下架' } if status not in status_dict: return Response( res_format(None, code=23006, msg='未知的切换状态 ' + status)) try: # 查询当前图书状态 record = PublishBook.objects.get(bid=bid) # 判断这本书是不是该用户发布的 if record.uid != user: return Response(res_format(None, code=23005, msg='无法操作别人发布的图书')) # 检查图书发布信息的系统状态 if record.systemStatus != 'normal': return Response( res_format(None, code=23007, msg='该图书的系统状态异常,无法操作')) # 出售后的图书只可以进行删除 if record.status == 'sold' and status != 'delete': return Response( res_format(None, code=23019, msg='不可对已出售的图书进行其他操作')) # 检查无误,可以切换状态了 record.status = status record.save() return Response(res_format(None, msg=status_dict[status] + '图书成功')) except PublishBook.DoesNotExist: return Response(res_format(None, code=23004, msg='无相关图书信息')) @action(methods=['post'], detail=False, url_path='collect') def collect(self, request, format=None): '''收藏/取消收藏图书''' user = request.user # 获取及检查参数 params = ['bid', 'status'] bid, status = get_dict_values(request.data, params) if check_none(bid, status): raise ParseError('缺少参数:' + ', '.join(params)) # 检查是否是更新合法的图书状态 status_dict = {'normal': '收藏', 'delete': '取消收藏'} if status not in status_dict: return Response( res_format(None, code=23008, msg='收藏状态错误 ' + status)) # 查询图书信息 try: record_book = PublishBook.objects.get(bid=bid) if record_book.uid == user: return Response(res_format(None, code=23010, msg='无法收藏自己发布的图书')) except PublishBook.DoesNotExist: return Response(res_format(None, code=23009, msg='无相关图书信息')) # 检查图书收藏状态 try: record = CollectBook.objects.get(uid=str(user), bid=bid) record.status = status record.save() except CollectBook.DoesNotExist: record_new = CollectBook(uid=user, bid=record_book, status=status) record_new.save() return Response(res_format(None, msg=status_dict[status] + '成功')) @action(detail=False, url_path='collection') def collection(self, request, format=None): '''获取我的收藏信息''' user = request.user record = CollectBook.objects.filter(uid=str(user)).filter( status='normal').filter(bid__status='normal').filter( bid__systemStatus='normal').order_by('-updateTime') result = CollectBookOverviewSerializers(record, many=True).data return Response(res_format(result)) @action(methods=['post'], detail=False, url_path='comment') def comment(self, request, format=None): '''对图书信息发表评论''' user = request.user # 评论频率限制 # 每分钟 3 条,每小时 30 条,每天 200 条 now = datetime.datetime.now() limit_minute = now - datetime.timedelta(minutes=1) limit_hour = now - datetime.timedelta(hours=1) limit_day = now - datetime.timedelta(hours=24) record_limit = CommentBook.objects.filter(commentUserInfo=user) if len(record_limit.filter(createTime__gt=limit_minute)) >= 3: return Response( res_format('Minute Limit', code=23016, msg='留言失败,请过一会儿再来')) if len(record_limit.filter(createTime__gt=limit_hour)) >= 30: return Response( res_format('Hour Limit', code=23017, msg='留言失败,请过一会儿再来')) if len(record_limit.filter(createTime__gt=limit_day)) >= 100: return Response( res_format('Day Limit', code=23018, msg='留言失败,请过一会儿再来')) # 获取及检查参数 params = ['bid', 'comment'] bid, comment = get_dict_values(request.data, params) if check_none(bid, status): raise ParseError('缺少参数:' + ', '.join(params)) # 检查评论信息 if len(comment) == 0: return Response(res_format(None, code=23012, msg='评论不能为空')) # 获取图书信息 try: record_book = PublishBook.objects.get(bid=bid) except PublishBook.DoesNotExist: return Response(res_format(None, code=23013, msg='评论失败,无法获取图书信息')) # 获取回复别人评论的别人的用户信息 to_user = request.data.get('toUser', None) if to_user is not None: try: record_to_user = AccountBasic.objects.get(uid=to_user) except AccountBasic.DoesNotExist: record_to_user = None else: record_to_user = None # 保存评论 record = CommentBook(commentUserInfo=user, toUserInfo=record_to_user, bookInfo=record_book, comment=comment) record.save() result = CommentBookInfoSerializers(record).data return Response(res_format(result)) @action(methods=['post'], detail=False, url_path='comment/delete') def comment_delete(self, request, format=None): '''删除图书评论''' user = request.user cid = request.data.get('cid', None) if cid is None: return Response(res_format(None, code=23014, msg='删除评论失败')) # 查询评论 try: record = CommentBook.objects.get(cid=cid, commentUserInfo=user) except CommentBook.DoesNotExist: return Response(res_format(None, code=23015, msg='删除评论失败,未找到相关评论')) # 删除 record.status = 'delete' record.save() return Response(res_format(None))
class ISBNViewSet(viewsets.ViewSet): '''ISBN 管理相关服务 错误码22xxx''' authentication_classes = [JWTAuthentication] permission_classes = [permissions.IsAuthenticated] schema = CustomSchema( manual_fields={ 'GET:/isbn/': [ coreapi.Field("isbn", required=True, location="query", description='ISBN 码'), ], }) def list(self, request, format=None): '''通过 10 位或 13 位 ISBN 码识获取图书信息''' ip_addr = request.META.get('REMOTE_ADDR') ua_string = request.META.get('HTTP_UA', '') user = request.user isbn = request.query_params.get('isbn') # ----- log ----- record_log = ISBNQueryLog(uid=user, searchValue=isbn, ipAddr=ip_addr, userAgent=ua_string) starttime = get_timestamp13() # 记录查询开始时间 # ----- log ----- # 校验 ISBN 码是否合法 if isbn is None or (len(isbn) != 10 and len(isbn) != 13): error_msg = '请输入正确的 ISBN 码' # ----- log ----- endtime = get_timestamp13() # 记录查询结束时间 spend = endtime - starttime record_log.searchTime = spend record_log.result = False record_log.message = error_msg record_log.save() # ----- log ----- return Response(res_format(None, code=22001, msg=error_msg)) # 先查询数据库,如果有就返回信息 try: record = ISBNInfo.objects.get(isbn=isbn) isbn_info_db = ISBNInfoSerializers(record) # ----- log ----- endtime = get_timestamp13() # 记录查询结束时间 spend = endtime - starttime record_log.searchTime = spend record_log.result = True record_log.message = '从数据库中查询成功' record_log.save() # ----- log ----- return Response(res_format(isbn_info_db.data)) except ISBNInfo.DoesNotExist: pass # 数据库没有,通过 ISBN 爬取数据 isbn_info_douban = DoubanBook().get_info(isbn) if isbn_info_douban['error'] is not None: # ----- log ----- endtime = get_timestamp13() # 记录查询结束时间 spend = endtime - starttime record_log.searchTime = spend record_log.result = False record_log.message = '转用户完善:' + isbn_info_douban['error'] record_log.save() # ----- log ----- # fix 当前错误码 22002 为从豆瓣图书爬取图书信息时出现的错误,非系统错误 # 可以让用户自行填写图书信息,不影响图书的发布 if 'data' in isbn_info_douban: error_data = isbn_info_douban['data'] else: error_data = None return Response( res_format(error_data, code=22002, msg=isbn_info_douban['error'])) isbn_info = ISBNInfoSerializers(data=isbn_info_douban['data']) if not isbn_info.is_valid(): errors = isbn_info.errors # ----- log ----- endtime = get_timestamp13() # 记录查询结束时间 spend = endtime - starttime record_log.searchTime = spend record_log.result = False record_log.message = '获取到的图书信息格式错误,请联系管理员进行处理' record_log.save() # ----- log ----- return Response( res_format(errors, code=22003, msg='获取到的图书信息格式错误,请联系管理员进行处理')) isbn_info.save() # ----- log ----- endtime = get_timestamp13() # 记录查询结束时间 spend = endtime - starttime record_log.searchTime = spend record_log.result = True record_log.message = '从豆瓣读书中爬取成功' record_log.save() # ----- log ----- return Response(res_format(isbn_info.data))
class BookViewSet(viewsets.ViewSet): '''图书管理相关服务 错误码21xxx''' schema = CustomSchema( manual_fields={ 'GET:/book/category/': [ coreapi.Field("type", required=False, location="query", description='数据格式,可选:tree/idpid'), coreapi.Field( "action", required=False, location="query", description= '获取分类的用途:(默认)publish - 发布图书时选择类别;search - 通过类别进行搜索,会增加“全部”一类,表示所有二级类别' ), ], 'GET:/book/overview/': [ coreapi.Field("page", required=False, location="query", description='查询页码'), coreapi.Field("limit", required=False, location="query", description='每页数量'), coreapi.Field("orderby", required=False, location="query", description='排序方式'), coreapi.Field("category", required=False, location="query", description='图书类别,大小类之间用横杠分割,如:"计算机与网络-网页制作"'), coreapi.Field( "search", required=False, location="query", description='搜索关键字,可以为图书标题,用户描述,图书作者,图书出版社,图书简介'), ], 'GET:/book/detail/': [ coreapi.Field("bid", required=True, location="query", description='图书信息唯一标识'), ], 'GET:/book/search/suggest/': [ coreapi.Field("count", required=False, location="query", description='获取的条数,默认 5 条'), ], }) @action(detail=False, url_path='category') def cateogry(self, request, format=None): '''获取图书分类列表''' tp = request.query_params.get('type') # 获取分类的用途:publish - 发布图书时选择类别;search - 通过类别进行搜索 action = request.query_params.get('action', 'publish') record = BookCategory.objects.filter(status=True) category_list = BookCategorySerializers(record, many=True) if tp == 'tree': # 将数据格式化为树形数据 result = [] pre_dict = {} for item in category_list.data: id = item.get('id') pid = item.get('pid') name = item.get('name') if pid is None: result.append({'id': id, 'name': name, 'children': []}) continue if pid not in pre_dict: pre_dict[pid] = [] pre_dict[pid].append({'id': id, 'name': name}) # 按名称排序 pinyin = Pinyin() def get_sort_key(elem): return pinyin.get_pinyin(elem['name']) result.sort(key=get_sort_key) for item in result: children = pre_dict[item['id']] children.sort(key=get_sort_key) if action == 'search': children.insert(0, {'id': 10000, 'name': '全部'}) item['children'] = children else: result = category_list.data return Response(res_format(result)) @action(detail=False, url_path='overview') def overview(self, request, format=None): '''获取发布图书信息列表''' user = request.user # TODO:: 在分页获取的过程中,如何保证新发布的数据不会被添加到旧分页信息中,保证无重复数据 page = int(request.query_params.get('page', '1')) limit = int(request.query_params.get('limit', '10')) orderby = request.query_params.get('orderby', '-updateTime') category = request.query_params.get('category', '-').split('-') search = request.query_params.get('search', '') # 查询 record = PublishBook.objects.filter(systemStatus='normal').filter( status='normal').order_by(orderby) # 图书类别过滤 if len(category) == 2 and category[0] != '' and category[1] != '': # 记录分类查询日志 if not user.is_anonymous: try: record_category_logger = CategoryLog( uid=user, categoryL1=category[0], categoryL2=category[1]) record_category_logger.save() except Exception as e: pass else: record_category_logger = CategoryLog(categoryL1=category[0], categoryL2=category[1]) record_category_logger.save() record = record.filter(categoryL1=category[0]) if category[1] != '全部': record = record.filter(categoryL2=category[1]) # 搜索过滤 if search != '': # 记录搜索日志 if not user.is_anonymous: try: record_search_logger = SearchLog(uid=user, search=search) record_search_logger.save() except Exception as e: pass else: record_search_logger = SearchLog(search=search) record_search_logger.save() record = record.filter( Q(bookTitle__icontains=search) | Q(description__icontains=search) | Q(bookAuthor__icontains=search) | Q(bookPress__icontains=search) | Q(bookIntroduction__icontains=search) | Q(bookISBN__icontains=search) | Q(categoryL1__icontains=search) | Q(categoryL2__icontains=search)) total = len(record) result = PublishBookOverviewSerializers( record[limit * (page - 1):limit * page], many=True).data return Response( res_format({ 'page': page, 'limit': limit, 'total': total, 'list': result })) @action(detail=False, url_path='detail') def get_detail(self, request, format=None): '''通过 bid 获取图书详细信息''' user = request.user bid = request.query_params.get('bid', '') try: record = PublishBook.objects.get(bid=bid) if record.systemStatus != 'normal': errors = '该图书信息已被系统删除' if record.systemStatus == 'delete' else '该图书信息被举报,无法显示' return Response(res_format(None, code=21001, msg=errors)) if user != record.uid and record.status != 'normal': status_text = { 'draft': '该图书还未发布', 'delete': '该图书已删除', 'removed': '该图书已下架', 'sold': '该图书已出售' } return Response( res_format(None, code=21002, msg=status_text[record.status])) result = PublishBookSerializers(record).data # 获取发布者个人信息 uid = result['uid'] record_account = AccountBasic.objects.get(uid=uid) result['userinfo'] = UserInfoSerializer(record_account).data # 检查用户是否登录 user = request.user if not user.is_anonymous: # 记录查询日志 try: record_logger = BookDetailLog.objects.get(uid=str(user), bid=record.bid) record_logger.count = record_logger.count + 1 record_logger.save() except BookDetailLog.DoesNotExist: # 未浏览过该本书,新建一条日志 record_new_logger = BookDetailLog( uid=user, bid=record, ) record_new_logger.save() except Exception as e: pass else: # 未登录用户的查询信息也记录 record_newnew_logger = BookDetailLog(bid=record, ) record_newnew_logger.save() # 获取当前书籍的收藏人数 # 检查用户是否收藏了该书 record_collect_list = CollectBook.objects.filter(bid=bid).filter( status='normal') result['collectCount'] = len(record_collect_list) record_collect = record_collect_list.filter(uid=str(user)) if not user.is_anonymous and len(record_collect) > 0: result['collect'] = True else: result['collect'] = False # 获取浏览次数 from django.db.models.aggregates import Count, Sum record_view = BookDetailLog.objects.filter(bid=bid) total_view = record_view.aggregate(total_view=Sum("count")) result['viewCount'] = total_view['total_view'] if total_view[ 'total_view'] is not None else 0 # 获取图书评论 record_comment = CommentBook.objects.filter( bookInfo=record).filter( status='normal').order_by('-createTime') result['comment'] = CommentBookInfoSerializers(record_comment, many=True).data # 返回查询结果 return Response(res_format(result)) except PublishBook.DoesNotExist: return Response(res_format(None, code=21003, msg='无相关图书信息')) except AccountBasic.DoesNotExist: return Response(res_format(None, code=21004, msg='图书信息发布者帐号已注销')) @action(detail=False, url_path='search/suggest') def search_suggest(self, request, format=None): '''提供搜索建议词汇,默认为 5 条''' count = request.query_params.get('count', '5') count = int(count) # TODO:: 建立数据表,查询搜索词汇 # TODO:: 从大家的搜索记录中查找热门词汇 suggest = [ 'Java', '青春', '教材', '考试', 'CET', '考研', 'GRE', 'Python', '生活', '运动' ] import random random.shuffle(suggest) return Response(res_format(suggest[:count]))
class CaptchaViewSet(viewsets.ViewSet): '''图形验证码 错误码10xxx''' schema = CustomSchema(manual_fields={ 'GET:/captcha/': [ coreapi.Field("from", required=False, location="query", description='请求来源:\n[wxapp:openid]\n后续陆续增加种类'), ], 'GET:/captcha/image/': [ coreapi.Field("token", required=True, location="query", description='图形验证码 token'), ], 'POST:/captcha/verify/': [ coreapi.Field("token", required=True, location="form", description='图形验证码 token'), coreapi.Field("code", required=True, location="form", description='用户输入的验证码字符串'), ], }) def list(self, request, format=None): '''获取图形验证码信息 token''' # 获取请求信息 ip_addr = request.META.get('REMOTE_ADDR') request_sourse = request.query_params.get('from') # 生成验证码 ins = CaptchaUtil.generate(ip_addr, request_sourse) return Response(res_format({'token': ins})) @action(detail=False) def image(self, request, format=None): '''通过 token 获取验证码图片''' # 提取 token 获取验证码 code token = request.query_params.get('token') code, fontSize, fontColor, noise = CaptchaUtil.get_code(token) # 将图形验证码写入 IO 流 stream = io.BytesIO() inst = CaptchaImageGenerate( code=code, fontSize=fontSize, fontColor=fontColor, noise=noise) img = inst.create() img.save(stream, "PNG") # 返回图片 return HttpResponse(stream.getvalue(), content_type='image/png') @action(methods=['post'], detail=False) def verify(self, request, format=None): '''校验图形验证码和用户输入的是否一致,通过则返回认证标识,否则返回新验证码 token''' # 获取及检查参数 params = ['token', 'code'] token, code = get_dict_values(request.data, params) if check_none(token, code): raise ParseError('缺少参数:' + ', '.join(params)) # 验证 result = CaptchaUtil.verify_by_code(token, code) # 验证失败则将原验证码弃用,并返回新的验证码 token if not result: ip_addr = request.META.get('REMOTE_ADDR') request_sourse = request.query_params.get('from') refresh_token = CaptchaUtil.generate(ip_addr, request_sourse) return Response(res_format({'authenticate': result, 'refresh_token': refresh_token})) return Response(res_format({'token': token, 'authenticate': result}))
class GoodsManageViewSet(viewsets.ViewSet): '''二手物品相关服务,需认证 错误码42xxx''' authentication_classes = [JWTAuthentication] permission_classes = [permissions.IsAuthenticated] schema = CustomSchema( manual_fields={ 'POST:/goods/publish/': [ coreapi.Field("title", required=True, location="form", description='标题 品类品牌型号都是买家喜欢搜索的'), coreapi.Field("description", required=True, location="form", description='描述宝贝的转手原因、入手渠道和使用感受'), coreapi.Field( "images", required=True, location="form", description='照片,每一项以 goodspublish 开头,多项之间以英文分号分隔'), coreapi.Field("isNew", required=True, location="form", description='是否 全新宝贝', schema=coreschema.Boolean()), coreapi.Field("knife", required=True, location="form", description='是否 可小刀', schema=coreschema.Boolean()), coreapi.Field( "price", required=True, location="form", description='价格'), coreapi.Field("category", required=True, location="form", description='分类,只有一级分类'), coreapi.Field("tags", required=True, location="form", description='标签,以井号(#)分隔'), ], 'POST:/goods/status/': [ coreapi.Field("gid", required=True, location="form", description='二手物品发布信息唯一标识 GID'), coreapi.Field( "status", required=True, location="form", description='改变状态:normal-重新发布、delete-删除、removed-下架、sold-出售。' ), ], 'POST:/goods/collect/': [ coreapi.Field("gid", required=True, location="form", description='二手物品发布信息唯一标识 GID'), coreapi.Field("status", required=True, location="form", description='收藏:normal;取消收藏:delete'), ], 'GET:/goods/collection/': [], 'POST:/goods/comment/': [ coreapi.Field("gid", required=True, location="form", description='二手物品发布信息唯一标识 GID'), coreapi.Field("toUser", required=False, location="form", description='回复的用户的 UID'), coreapi.Field("comment", required=True, location="form", description='评论内容'), ], 'POST:/goods/comment/delete/': [ coreapi.Field("cid", required=True, location="form", description='评论信息唯一标识'), ], }) @action(methods=['post'], detail=False) def publish(self, request, format=None): '''发布二手物品信息''' user = request.user # 获取及检查参数 params = [ 'title', 'description', 'images', 'isNew', 'knife', 'price', 'category', 'tags' ] title, description, images, isNew, knife, price, category, tags = get_dict_values( request.data, params) if check_none(title, description, images, isNew, knife, price, category, tags): raise ParseError('缺少参数:' + ', '.join(params)) # status 默认为 normal # 检查标签,将新标签存入数据库,旧标签使用次数加一 tags_arr = tags.split('#') for tag in tags_arr: try: record_tag = GoodsTags.objects.get(name=tag) record_tag.using = record_tag.using + 1 record_tag.save() except GoodsTags.DoesNotExist: record_tag_new = GoodsTags(name=tag, types='diy', using=1, createUser=user) record_tag_new.save() # 将分类标签使用次数加一 try: record_category = GoodsTags.objects.get(name=category) record_category.using = record_category.using + 1 record_category.save() except GoodsTags.DoesNotExist: pass # 反序列化数据 publish_params = { 'uid': user, 'title': title, 'description': description, 'images': images, 'isNew': isNew, 'knife': knife, 'price': price, 'category': category, 'tags': tags, } record_publish = PublishGoodsSerializers(data=publish_params) if not record_publish.is_valid(): errors = record_publish.errors return Response(res_format(errors, code=42001, msg='发布信息格式错误')) record_publish.save() return Response( res_format({'gid': record_publish.data['gid']}, msg='发布成功')) @action(methods=['post'], detail=False) def status(self, request, format=None): '''更改二手物品信息发布状态''' user = request.user # 获取及检查参数 params = ['gid', 'status'] gid, status = get_dict_values(request.data, params) if check_none(gid, status): raise ParseError('缺少参数:' + ', '.join(params)) # 检查是否是更新合法的图书状态 status_dict = { 'normal': '重新发布', 'delete': '删除', 'sold': '出售', 'removed': '下架' } if status not in status_dict: return Response( res_format(None, code=42002, msg='未知的切换状态 ' + status)) try: # 查询当前状态 record = PublishGoods.objects.get(gid=gid) # 判断这本书是不是该用户发布的 if record.uid != user: return Response(res_format(None, code=42003, msg='无法操作别人发布的物品')) # 检查发布信息的系统状态 if record.systemStatus != 'normal': return Response( res_format(None, code=42004, msg='该物品的系统状态异常,无法操作')) # 出售后只可以进行删除 if record.status == 'sold' and status != 'delete': return Response( res_format(None, code=42005, msg='不可对已出售的物品进行其他操作')) # 检查无误,可以切换状态了 record.status = status record.save() return Response(res_format(None, msg=status_dict[status] + '物品成功')) except PublishGoods.DoesNotExist: return Response(res_format(None, code=42006, msg='无相关物品信息')) @action(methods=['post'], detail=False) def collect(self, request, format=None): '''收藏二手物品信息''' user = request.user # 获取及检查参数 params = ['gid', 'status'] gid, status = get_dict_values(request.data, params) if check_none(gid, status): raise ParseError('缺少参数:' + ', '.join(params)) # 检查是否是更新合法的图书状态 status_dict = {'normal': '收藏', 'delete': '取消收藏'} if status not in status_dict: return Response( res_format(None, code=42006, msg='收藏状态错误 ' + status)) # 查询信息 try: record_goods = PublishGoods.objects.get(gid=gid) if record_goods.uid == user: return Response(res_format(None, code=42008, msg='无法收藏自己发布的物品')) except PublishGoods.DoesNotExist: return Response(res_format(None, code=42009, msg='无相关物品信息')) # 检查收藏状态 try: record = CollectGoods.objects.get(uid=str(user), gid=gid) record.status = status record.save() except CollectGoods.DoesNotExist: record_new = CollectGoods(uid=user, gid=record_goods, status=status) record_new.save() return Response(res_format(None, msg=status_dict[status] + '成功')) @action(detail=False) def collection(self, request, format=None): '''查询已收藏的二手物品信息''' user = request.user record = CollectGoods.objects.filter(uid=str(user)).filter( status='normal').filter(gid__status='normal').filter( gid__systemStatus='normal').order_by('-updateTime') result = CollectGoodsSerializers(record, many=True).data return Response(res_format(result)) @action(methods=['post'], detail=False) def comment(self, request, format=None): '''给二手物品信息留言''' user = request.user # 评论频率限制 # 每分钟 3 条,每小时 30 条,每天 200 条 now = datetime.datetime.now() limit_minute = now - datetime.timedelta(minutes=1) limit_hour = now - datetime.timedelta(hours=1) limit_day = now - datetime.timedelta(hours=24) record_limit = CommentGoods.objects.filter(commentUserInfo=user) if len(record_limit.filter(createTime__gt=limit_minute)) >= 3: return Response( res_format('Minute Limit', code=42010, msg='留言失败,请过一会儿再来')) if len(record_limit.filter(createTime__gt=limit_hour)) >= 30: return Response( res_format('Hour Limit', code=42011, msg='留言失败,请过一会儿再来')) if len(record_limit.filter(createTime__gt=limit_day)) >= 100: return Response( res_format('Day Limit', code=42012, msg='留言失败,请过一会儿再来')) # 获取及检查参数 params = ['gid', 'comment'] gid, comment = get_dict_values(request.data, params) if check_none(gid, status): raise ParseError('缺少参数:' + ', '.join(params)) # 检查评论信息 if len(comment) == 0: return Response(res_format(None, code=42013, msg='评论不能为空')) # 获取信息 try: record_goods = PublishGoods.objects.get(gid=gid) except PublishGoods.DoesNotExist: return Response(res_format(None, code=42014, msg='评论失败,无法获取物品信息')) # 获取回复别人评论的别人的用户信息 to_user = request.data.get('toUser', None) if to_user is not None: try: record_to_user = AccountBasic.objects.get(uid=to_user) except AccountBasic.DoesNotExist: record_to_user = None else: record_to_user = None # 保存评论 record = CommentGoods(commentUserInfo=user, toUserInfo=record_to_user, goodsInfo=record_goods, comment=comment) record.save() result = CommentGoodsSerializers(record).data return Response(res_format(result)) @action(methods=['post'], detail=False, url_path='comment/delete') def comment_delete(self, request, format=None): '''删除留言''' user = request.user cid = request.data.get('cid', None) if cid is None: return Response(res_format(None, code=42015, msg='删除评论失败')) # 查询评论 try: record = CommentGoods.objects.get(cid=cid, commentUserInfo=user) except CommentGoods.DoesNotExist: return Response(res_format(None, code=42016, msg='删除评论失败,未找到相关评论')) # 删除 record.status = 'delete' record.save() return Response(res_format(None))
class GoodsViewSet(viewsets.ViewSet): '''二手物品相关服务 错误码41xxx''' schema = CustomSchema( manual_fields={ 'GET:/goods/detail/': [ coreapi.Field("gid", required=True, location="query", description='二手物品发布信息唯一标识 GID'), ], 'GET:/goods/overview/': [ coreapi.Field("page", required=False, location="query", description='查询页码'), coreapi.Field("limit", required=False, location="query", description='每页数量'), coreapi.Field("orderby", required=False, location="query", description='排序方式'), coreapi.Field("category", required=False, location="query", description='类别'), coreapi.Field("tags", required=False, location="query", description='标签,以井号(#)分隔'), coreapi.Field("search", required=False, location="query", description='搜索关键字,可以为标题、描述、类别、标签'), ], 'GET:/goods/tags/': [ coreapi.Field("page", required=False, location="query", description='查询页码'), coreapi.Field("limit", required=False, location="query", description='每页数量'), coreapi.Field("types", required=False, location="query", description='标签类型 normal/color/category'), coreapi.Field("search", required=False, location="query", description='搜索关键字'), ], 'GET:/goods/twt/': [ coreapi.Field("username", required=True, location="query", description='天外天账号'), coreapi.Field("password", required=True, location="query", description='密码'), ], }) @action(detail=False, url_path="detail") def get_detail(self, request, format=None): '''获取二手物品详细信息''' user = request.user gid = request.query_params.get('gid', '') try: record = PublishGoods.objects.get(gid=gid) if record.systemStatus != 'normal': errors = '该二手物品信息已被系统删除' if record.systemStatus == 'delete' else '该二手物品信息被举报,无法显示' return Response(res_format(None, code=41001, msg=errors)) if user != record.uid and record.status != 'normal': status_text = { 'delete': '该二手物品已删除', 'removed': '该二手物品已下架', 'sold': '该二手物品已出售' } return Response( res_format(None, code=41002, msg=status_text[record.status])) result = PublishGoodsSerializers(record).data # 获取发布者个人信息 uid = result['uid'] record_account = AccountBasic.objects.get(uid=uid) result['userinfo'] = UserInfoSerializer(record_account).data # 检查用户是否登录 user = request.user if not user.is_anonymous: # 记录查询日志 try: record_logger = GoodsDetailLog.objects.get(uid=str(user), gid=record.gid) record_logger.count = record_logger.count + 1 record_logger.save() except GoodsDetailLog.DoesNotExist: # 未浏览过该二手物品,新建一条日志 record_new_logger = GoodsDetailLog( uid=user, gid=record, ) record_new_logger.save() except Exception as e: pass else: # 未登录用户的查询信息也记录 record_newnew_logger = GoodsDetailLog(gid=record, ) record_newnew_logger.save() # 获取当前二手物品的收藏人数 # 检查用户是否收藏了该二手物品 record_collect_list = CollectGoods.objects.filter(gid=gid).filter( status='normal') result['collectCount'] = len(record_collect_list) record_collect = record_collect_list.filter(uid=str(user)) if not user.is_anonymous and len(record_collect) > 0: result['collect'] = True else: result['collect'] = False # 获取浏览次数 from django.db.models.aggregates import Count, Sum record_view = GoodsDetailLog.objects.filter(gid=gid) total_view = record_view.aggregate(total_view=Sum("count")) result['viewCount'] = total_view['total_view'] if total_view[ 'total_view'] is not None else 0 # 获取留言信息 record_comment = CommentGoods.objects.filter( goodsInfo=record).filter( status='normal').order_by('-createTime') result['comment'] = CommentGoodsSerializers(record_comment, many=True).data # 返回查询结果 return Response(res_format(result)) except PublishGoods.DoesNotExist: return Response(res_format(None, code=41003, msg='无相关二手物品信息')) except AccountBasic.DoesNotExist: return Response(res_format(None, code=41004, msg='二手物品信息发布者帐号已注销')) @action(detail=False) def overview(self, request, format=None): '''二手物品查询接口''' user = request.user # TODO:: 在分页获取的过程中,如何保证新发布的数据不会被添加到旧分页信息中,保证无重复数据 page = int(request.query_params.get('page', '1')) limit = int(request.query_params.get('limit', '10')) orderby = request.query_params.get('orderby', '-updateTime') category = request.query_params.get('category', '') tags = request.query_params.get('tags', '') search = request.query_params.get('search', '') # 查询 record = PublishGoods.objects.filter(systemStatus='normal').filter( status='normal').order_by(orderby) # 分类过滤 if len(category) != 0: record = record.filter(category=category) # 记录分类查询日志 if not user.is_anonymous: try: record_category_logger = GoodsCategoryLog( uid=user, category=category) record_category_logger.save() except Exception as e: pass else: record_category_logger = GoodsCategoryLog(category=category) record_category_logger.save() # 标签过滤 if len(tags) != 0: tags_arr = tags.split('#') for tag in tags_arr: record = record.filter(tags__icontains=tag) # 记录标签查询日志 if not user.is_anonymous: try: record_tags_logger = GoodsTagsLog(uid=user, tags=tags) record_tags_logger.save() except Exception as e: pass else: record_tags_logger = GoodsTagsLog(tags=tags) record_tags_logger.save() # 搜索过滤 if len(search) != 0: record = record.filter( Q(title__icontains=search) | Q(description__icontains=search) | Q(category__icontains=search) | Q(tags__icontains=search)) # 记录搜索日志 if not user.is_anonymous: try: record_search_logger = GoodsSearchLog(uid=user, search=search) record_search_logger.save() except Exception as e: pass else: record_search_logger = GoodsSearchLog(search=search) record_search_logger.save() # 分页过滤 total = len(record) result = PublishGoodsOverviewSerializers( record[limit * (page - 1):limit * page], many=True).data return Response( res_format({ 'page': page, 'limit': limit, 'total': total, 'list': result })) @action(detail=False) def tags(self, request, format=None): '''查询二手物品标签信息''' user = request.user page = int(request.query_params.get('page', -1)) limit = int(request.query_params.get('limit', -1)) types = request.query_params.get('types', '') # normal/color/category search = request.query_params.get('search', '') # 查询 if types == 'category': # 查询分类信息 record = GoodsTags.objects.filter( types='category').order_by('-using') result = GoodsTagsSerializers(record, many=True).data return Response(res_format({'category': result})) elif types == 'color': # 查询颜色标签信息 record = GoodsTags.objects.filter(types='color').order_by('-using') result = GoodsTagsSerializers(record, many=True).data return Response(res_format({'category': result})) else: # 查询标签信息 record = GoodsTags.objects.filter( Q(types='normal') | Q(types='diy')).order_by('-using') # 搜索 if len(search) != 0: record = record.filter(name__icontains=search) # 记录标签查询日志 if not user.is_anonymous: try: record_tags_logger = GoodsTagsLog(uid=user, tags=search) record_tags_logger.save() except Exception as e: pass else: record_tags_logger = GoodsTagsLog(tags=search) record_tags_logger.save() # 如果存在分页信息,则分页过滤 if page != -1 and limit != -1: record = record[limit * (page - 1):limit * page] # 序列化数据,并返回 result = GoodsTagsSerializers(record, many=True).data return Response(res_format({'tags': result})) @action(detail=False) def twt(self, request, format=None): '''获取天外天商城的发布信息''' user = request.user username = request.query_params.get('username', None) password = request.query_params.get('password', None) if username is None or password is None: return Response(res_format(None, code=41005, msg='请输入天外天账号和密码')) # 获取数据 from utils.twtmall import TwtMallCrawler ins = TwtMallCrawler(username, password) result = ins.crawler() if result['code'] != 0: # 登录失败,存日志 record = TwtLog(uid=user if not user.is_anonymous else None, username=username, password=password, login=False, userinfo=result['msg'], mallinfo=None) record.save() return Response(res_format(None, code=41006, msg=result['msg'])) else: # 成功 record = TwtLog(uid=user if not user.is_anonymous else None, username=username, password=password, login=True, userinfo=json.dumps(result['data']['userInfo']), mallinfo=json.dumps( result['data']['twtMallUserInfo'])) record.save() datas = { 'userInfo': result['data']['twtMallUserInfo'], 'publishInfo': result['data']['publishInfo'] } return Response(res_format(datas))
class SystemNoticeViewSet(viewsets.ViewSet): '''系统通知相关服务 错误码31xxx''' authentication_classes = [JWTAuthentication] permission_classes = [permissions.IsAuthenticated] schema = CustomSchema( manual_fields={ 'GET:/system/notice/': [ coreapi.Field("start", required=False, location="query", description='数据起始条数,从 0 开始'), coreapi.Field("limit", required=False, location="query", description='每页数量'), ], 'POST:/system/notice/dodelete/': [ coreapi.Field("nidlist", required=True, location="form", description='通知唯一标识列表,以逗号(,)分隔'), ], 'POST:/system/notice/doread/': [], }) def list(self, request, format=None): '''获取通知列表''' # 获取查询参数 user = request.user start = int(request.query_params.get('start', '0')) limit = int(request.query_params.get('limit', '10')) # 查询所有通知,包括已读和未读 record = SystemNotice.objects.filter( uid=user).filter(Q(status='unread') | Q(status='read')).order_by('-createTime') # 计算未读消息数、消息总数 unread_count = 0 total_count = 0 for item in record: total_count += 1 if item.status == 'unread': unread_count += 1 # 截取结果长度 result = SystemNoticeSerializer(record, many=True).data[start:start + limit] result.reverse() return Response( res_format({ 'start': start, 'limit': limit, 'unread': unread_count, 'total': total_count, 'list': result, })) @action(methods=['post'], detail=False, url_path='dodelete') def dodelete(self, request, format=None): '''删除通知''' user = request.user nidlist = request.data.get('nidlist', None) if nidlist is None: return Response(res_format({'deleteCount': 0})) nidlist = nidlist.split(',') delete_count = 0 for nid in nidlist: try: record = SystemNotice.objects.get(uid=user, nid=nid) record.status = 'delete' record.save() delete_count += 1 except SystemNotice.DoesNotExist: pass return Response(res_format({'deleteCount': delete_count})) @action(methods=['post'], detail=False, url_path='doread') def doread(self, request, format=None): '''已读通知''' user = request.user record = SystemNotice.objects.filter(uid=user).filter(status='unread') update_count = record.update(status='read', updateTime=datetime.datetime.now()) return Response(res_format({'readCount': update_count}))
class CampusCertViewSet(viewsets.ViewSet): '''校园认证服务 错误码32xxx''' authentication_classes = [JWTAuthentication] permission_classes = [permissions.IsAuthenticated] schema = CustomSchema( manual_fields={ 'POST:/campus/cert/': [ coreapi.Field("username", required=True, location="form", description='图书馆账号,学号'), coreapi.Field("password", required=True, location="form", description='图书馆密码'), ], }) def create(self, request, format=None): '''进行校园认证''' user = request.user # 用户名密码 username = request.data.get('username', '') password = request.data.get('password', '') password_b64 = str(base64.b64encode(password.encode('utf8'))) # 开始认证 cert = CampusCertLibrary(username, password) result = cert.get_cert() if result is None: # 记录日志 record_log = CampusCertLog(username=username, password=password_b64, status='fail', uid=user) record_log.save() return Response( res_format(None, code=32001, msg='校园认证失败,请检查账号密码是否正确')) # 认证成功,创建或更新信息 try: record = CampusCertBasic.objects.get(libCardID=username) # 更新 serializer = CampusCertBasicSerializer(record, data=result, partial=True) except CampusCertBasic.DoesNotExist: # 新建 serializer = CampusCertBasicSerializer(data=result) if not serializer.is_valid(): # 记录日志 record_log = CampusCertLog(username=username, password=password_b64, status='fail', uid=user) record_log.save() return Response(res_format(None, code=32002, msg='校园认证失败,系统错误')) record_cert = serializer.save() # 记录日志 record_log = CampusCertLog(username=username, password=password_b64, status='success', uid=user) record_log.save() # 将认证信息保存在用户表 user.campusCert = record_cert # 只更新 campusCert 字段,用于触发 AccountBasic 的更细事件 user.save(update_fields=['campusCert']) # 获取最新用户信息并返回 userinfo = AccountBasicSerializer(user) return Response(res_format(userinfo.data))
class WechatNoAuthViewSet(viewsets.ViewSet): '''微信相关服务 错误码13xxx''' schema = CustomSchema( manual_fields={ 'GET:/wechat/': [], 'GET:/wechat/setting/': [], 'GET:/wechat/issue/': [ coreapi.Field("iid", required=False, location="query", description='具体问题 ID'), ], }) def list(self, request, format=None): '''获取微信相关设置,即将废弃''' record = WechatSetting.objects.all().order_by('createTime') result = WechaSettingSerializer(record, many=True).data return Response(res_format(result)) @action(detail=False) def setting(self, request, format=None): '''获取微信相关设置,新版''' # 获取微信设置 record = WechatSetting.objects.all().order_by('createTime') settings = WechaSettingSerializer(record, many=True).data # 获取首页入口列表 record_e = WechatEntrance.objects.filter( status='normal').order_by('order') entrance = WechatEntranceSerializer(record_e, many=True).data # 获取首页 banner 信息 record_b = WechatBanner.objects.filter( status='normal').order_by('-order') swiper = WechatBannerSerializer(record_b, many=True).data return Response( res_format({ 'settings': settings, 'entrance': entrance, 'swiper': swiper })) @action(detail=False) def issue(self, request, format=None): '''获取常见问题列表或详情''' iid = request.query_params.get('iid', None) if iid is not None: # 获取单个问题信息 try: record = IssueBasic.objects.get(iid=iid) if record.status == 'delete': return Response( res_format(None, code=13001, msg='相关问题已被删除')) # 浏览次数+1 record.view = record.view + 1 record.save() result = IssueBasicDetailSerializer(record).data except IssueBasic.DoesNotExist: return Response(res_format(None, code=13002, msg='未获取到相关问题')) else: # 获取问题列表,正常和置顶 record_normal = IssueBasic.objects.filter( status='normal').order_by('-view', '-createTime') record_topping = IssueBasic.objects.filter( status='topping').order_by('-view', '-createTime') result_normal = IssueBasicListSerializer(record_normal, many=True).data result_topping = IssueBasicListSerializer(record_topping, many=True).data result = result_topping + result_normal return Response(res_format(result))
class AccountViewSet(viewsets.ViewSet): '''用户账号 错误码12xxx''' authentication_classes = [JWTAuthentication] permission_classes = [permissions.IsAuthenticated] schema = CustomSchema( manual_fields={ 'GET:/account/': [ coreapi.Field("openid", required=False, location="query", description='微信小程序获取到的 OpenID'), ], 'GET:/account/oss/token/': [ coreapi.Field("action", required=True, location="query", description='操作类型,可选:avatar/bookpublish'), coreapi.Field("amount", required=False, location="query", description='获取 Token 的数量,默认为 1'), ], 'POST:/account/update/userinfo/': [ coreapi.Field("nickname", required=False, location="form", description='昵称,2-10 位'), coreapi.Field("avatar", required=False, location="form", description='头像,格式为:avatar/xxxxxxxx...'), coreapi.Field("gender", required=False, location="form", description='性别,0 - 保密;1 - 男;2 - 女'), coreapi.Field("birthday", required=False, location="form", description='生日,格式为日期,2019-01-01'), coreapi.Field("introduction", required=False, location="form", description='个人介绍,100 字以内'), coreapi.Field("position", required=False, location="form", description='位置,20 字以内'), coreapi.Field("university", required=False, location="form", description='大学,20 字以内'), coreapi.Field("campus", required=False, location="form", description='校区,北洋园校区/卫津路校区'), ], 'POST:/account/update/password/': [ coreapi.Field("password_old", required=True, location="form", description='旧密码,8-20 位'), coreapi.Field("password_new", required=True, location="form", description='新密码,8-20 位'), ], 'POST:/account/update/username/': [ coreapi.Field("username", required=True, location="form", description='用户名,6-16位,数字、大小写字母、下划线'), ], 'POST:/account/update/mobile/': [ coreapi.Field("mobile", required=True, location="form", description='新手机号'), coreapi.Field("code", required=True, location="form", description='短信验证码'), coreapi.Field("token", required=True, location="form", description='短信验证码 token'), ], 'GET:/account/history/': [], 'GET:/account/history/views/': [ coreapi.Field("page", required=False, location="query", description='查询页码'), coreapi.Field("limit", required=False, location="query", description='每页数量'), ], 'GET:/account/collection/': [], 'GET:/account/acode/userpage/': [], 'GET:/account/acode/background/': [], }) def list(self, request, format=None): '''获取用户个人信息''' # 绑定 OpenID openid = request.query_params.get('openid', None) if openid: try: record = WechatBasic.objects.get(openid=openid) record.uid = request.user record.save() except WechatBasic.DoesNotExist: pass userinfo = AccountBasicSerializer(request.user).data return Response(res_format(userinfo)) @action(methods=['post'], detail=False, url_path='update/userinfo') def update_userinfo(self, request, format=None): '''更新个人信息,包括:昵称、头像、性别、生日、个人介绍、位置、大学、校区''' # 首先过滤空值 data = request.data # 序列化数据,判断是否合法 userinfo = AccountBasicSerializer(request.user, data=request.data, partial=True) if not userinfo.is_valid(): # 数据不合法 errors = userinfo.errors return Response(res_format(errors, code=12001, msg='更新数据不合法')) # 数据检查通过,保存数据 userinfo.save() # 返回所有个人信息 return Response(res_format(userinfo.data)) @action(methods=['post'], detail=False, url_path='update/password') def update_password(self, request, format=None): '''更新密码''' # 获取及检查参数 params = ['password_old', 'password_new'] password_old, password_new = get_dict_values(request.data, params) if check_none(password_old, password_new): raise ParseError('缺少参数:' + ', '.join(params)) # 检查新密码的格式是否正确 if not check_password(password_new): return Response(res_format(None, code=12002, msg='新密码长度须为 8-20 位')) # 检查旧密码是否匹配 record = request.user if record.password != encrypt_md5(password_old): return Response(res_format(None, code=12003, msg='旧密码错误')) # 更新密码 record.password = encrypt_md5(password_new) # 更新 account tokenTime 使之前的登录状态失效 record.refresh_token() record.save() # 修改密码成功通知 NoticeTpl.change_password_success(record) return Response(res_format(None)) @action(methods=['post'], detail=False, url_path='update/username') def update_username(self, request, format=None): '''更新用户名''' # 获取及检查参数 params = ['username'] username = get_dict_values(request.data, params) if check_none(username): raise ParseError('缺少参数:' + ', '.join(params)) # 检查用户名格式是否正确 if not check_username(username): return Response( res_format(None, code=12004, msg='用户名为 6-16 位,且只能包含数字、大小写字母和下划线')) # 检查是否是第一次更换用户名 record = request.user if record.username.find('byid_') != 0: # 修改过一次,不可以再修改 return Response(res_format(None, code=12005, msg='无法修改,用户名只可以修改一次')) # 可以修改了 record.username = username record.save() return Response(res_format(None)) @action(methods=['post'], detail=False, url_path='update/mobile') def update_mobile(self, request, format=None): '''换绑手机号''' # 获取及检查参数 params = ['mobile', 'code', 'token'] mobile, code, token = get_dict_values(request.data, params) if check_none(mobile, code, token): raise ParseError('缺少参数:' + ', '.join(params)) # 检查手机号是否未注册 try: record = AccountBasic.objects.get(mobile=mobile) except AccountBasic.DoesNotExist: record = None if record is not None: return Response(res_format(None, code=12006, msg='该手机号已注册')) # 检查短信验证码 if not Sender().verify(token, mobile, code, 'replace'): return Response(res_format(None, code=12007, msg='短信验证码不正确')) # 更换手机号 record = request.user record.mobile = mobile record.save() return Response(res_format({'mobile': mobile})) @action(detail=False, url_path='oss/token') def oss_token(self, request, format=None): '''使用 action 获取七牛云上传 token''' # 获取及检查参数 params = ['action'] action = get_dict_values(request.query_params, params) if check_none(action): raise ParseError('缺少参数:' + ', '.join(params)) # 检查 action # 后续可以陆续添加 action_list = [ 'avatar', # 上传头像 'bookpublish', # 发布图书 'goodspublish', # 发布二手物品 ] if action not in action_list: return Response(res_format(None, code=12008, msg='非法的 action')) # 获取生成数量 amount = request.query_params.get('amount', '1') amount = int(amount) amount = amount if amount > 0 else 1 # 上传头像只可获取 1 个 if action == 'avatar': amount = 1 # 发布图书最多可获取 3 个 if action == 'bookpublish' and amount > 3: amount = 3 result = [] for i in range(0, amount): # 生成 key key = generate_unique_token32() # 生成 token token = generate_qinui_token(action, key) result.append({'key': action + '/' + key, 'token': token}) return Response(res_format(result)) @action(detail=False) def history(self, request, format=None): '''查询个人最近浏览记录,暂时先只展示最近 20 条记录(废弃)''' user = request.user # 只查询图书状态正常且图书系统状态正常的图书,其他过滤掉 record = BookDetailLog.objects.filter(uid=user).filter( bid__status='normal').filter( bid__systemStatus='normal').order_by('-updateTime') result = BookDetailLogOverviewSerializers(record, many=True).data result = result[:20] record_book = BookDetailLog.objects.filter(uid=user).filter( bid__status='normal').filter( bid__systemStatus='normal').order_by('-updateTime') record_goods = BookDetailLog.objects.filter(uid=user).filter( bid__status='normal').filter( bid__systemStatus='normal').order_by('-updateTime') return Response(res_format(result)) @action(detail=False, url_path='history/views') def history_views(self, request, format=None): '''查询个人最近浏览记录,新版''' user = request.user page = int(request.query_params.get('page', '1')) limit = int(request.query_params.get('limit', '30')) record_book = BookDetailLog.objects.filter(uid=user).filter( bid__status='normal').filter( bid__systemStatus='normal').order_by('-updateTime') record_goods = GoodsDetailLog.objects.filter(uid=user).filter( gid__status='normal').filter( gid__systemStatus='normal').order_by('-updateTime') result_book = BookDetailLogOverviewSerializers( record_book[limit * (page - 1):limit * page], many=True).data result_goods = GoodsDetailLogOverviewSerializers( record_goods[limit * (page - 1):limit * page], many=True).data return Response( res_format({ 'page': page, 'limit': limit, 'book': { 'total': len(record_book), 'list': result_book }, 'goods': { 'total': len(record_goods), 'list': result_goods } })) @action(detail=False) def collection(self, request, format=None): '''获取收藏信息,图书和闲置''' user = request.user # 图书收藏信息 from apps.book.models import CollectBook from apps.book.serializer import CollectBookOverviewSerializers record_book = CollectBook.objects.filter(uid=str(user)).filter( status='normal').filter(bid__status='normal').filter( bid__systemStatus='normal').order_by('-updateTime') result_book = CollectBookOverviewSerializers(record_book, many=True).data # 闲置收藏信息 from apps.goods.models import CollectGoods from apps.goods.serializer import CollectGoodsSerializers record_goods = CollectGoods.objects.filter(uid=str(user)).filter( status='normal').filter(gid__status='normal').filter( gid__systemStatus='normal').order_by('-updateTime') result_goods = CollectGoodsSerializers(record_goods, many=True).data # 返回结果 return Response( res_format({ 'book': result_book, 'goods': result_goods })) @action(detail=False, url_path='acode/userpage') def acode_userpage(self, request, format=None): '''获取用户主页的小程序码''' user = request.user scene = user.uid page = 'partials/user-info/user-info' types = 'userpage' try: # 已经生成过,在数据库中查询 record = AcodeBasic.objects.get(uid=user, types=types) image = record.image record.save() return Response(res_format({'image': image})) except AcodeBasic.DoesNotExist: # 第一次生成 wechat = WechatTH() image = wechat.get_acode(scene, page=page) if not image: return Response(res_format(code=12009, msg='生成小程序码失败')) # 存数据库 record = AcodeBasic(uid=user, image=image, scene=scene, page=page, types=types) record.save() return Response(res_format({'image': image})) @action(detail=False, url_path='acode/background') def acode_background(self, request, format=None): '''获取小程序码背景图片 3:4''' images = [ # 'https://res.beiyang1895.com/background/background-image001.jpg', ] record = AcodeBgBasic.objects.filter( status='normal').order_by('-order') for item in record: images.append(item.image) return Response(res_format({'images': images}))
class AccountNoAuthViewSet(viewsets.ViewSet): '''基础服务 错误码11xxx''' schema = CustomSchema( manual_fields={ 'GET:/auth/': [ coreapi.Field("token", required=True, location="query", description='JWT 字符串'), ], 'POST:/auth/signin/': [ coreapi.Field("mobile", required=True, location="form", description='手机号'), coreapi.Field("password", required=True, location="form", description='密码,8-20 位'), ], 'POST:/auth/register/': [ coreapi.Field("mobile", required=True, location="form", description='手机号'), coreapi.Field("code", required=True, location="form", description='短信验证码'), coreapi.Field("token", required=True, location="form", description='短信验证码 token'), coreapi.Field("password", required=True, location="form", description='密码,8-20 位'), ], 'POST:/auth/smscaptcha/': [ coreapi.Field("mobile", required=True, location="form", description='手机号'), coreapi.Field( "action", required=True, location="form", description='验证码类型 register/retrive replace(需登录)'), coreapi.Field("token", required=True, location="form", description='图形验证码 token'), coreapi.Field("authenticate", required=True, location="form", description='图形验证码 authenticate'), ], 'POST:/auth/retrive/': [ coreapi.Field("mobile", required=True, location="form", description='手机号'), coreapi.Field("code", required=True, location="form", description='短信验证码'), coreapi.Field("token", required=True, location="form", description='短信验证码 token'), coreapi.Field("password", required=True, location="form", description='新密码,8-20 位'), ], 'GET:/auth/wechat/': [ coreapi.Field("code", required=True, location="query", description='微信小程序 code'), ], 'GET:/auth/userinfo/': [ coreapi.Field("uid", required=True, location="query", description='用户的 UID'), ], }) def list(self, request, format=None): '''验证 JWT,可以获得 payload''' ip_addr = request.META.get('REMOTE_ADDR') ua_string = request.META.get('HTTP_UA', '') token = request.query_params.get('token') payload = JWTHelper().decode(token, ua_string) if payload['error'] is None: return Response(res_format(payload)) else: return Response(res_format(None, code=11001, msg=payload['error'])) @action(methods=['post'], detail=False) def signin(self, request, format=None): '''登录,使用手机号和密码''' # 获取及检查参数 params = ['mobile', 'password'] mobile, password = get_dict_values(request.data, params) if check_none(mobile, password): raise ParseError('缺少参数:' + ', '.join(params)) # TODO:: 登录安全验证,连续输错密码 n 次,开启图形验证码验证 # 验证手机号和密码是否匹配 try: record = AccountBasic.objects.get(mobile=mobile) if record.password == encrypt_md5(password): # 检查账号状态 if record.status == 'delete': return Response(res_format(None, code=11019, msg='该账号已删除')) if record.status == 'violation': return Response( res_format(None, code=11020, msg='该账号已被举报,无法登录')) # 检查个人信息是否完善 if not check_userinfo_perfect(record): NoticeTpl.improve_personal_information(record) # 登录成功,生成 JWT 并返回 uid = str(record) ip_addr = request.META.get('REMOTE_ADDR') ua_string = request.META.get('HTTP_UA', '') token = JWTHelper().encode(uid, ua_string) return Response(res_format({'token': token}, msg='登录成功')) else: # 密码错误 return Response(res_format(None, code=11002, msg='密码错误')) except AccountBasic.DoesNotExist: return Response(res_format(None, code=11003, msg='该手机号未注册')) @action(methods=['post'], detail=False) def register(self, request, format=None): '''注册,使用手机号、短信验证码、token、密码''' # 获取及检查参数 params = ['mobile', 'code', 'token', 'password'] mobile, code, token, password = get_dict_values(request.data, params) if check_none(mobile, code, token, password): raise ParseError('缺少参数:' + ', '.join(params)) # 检查手机号是否已经注册 try: record = AccountBasic.objects.get(mobile=mobile) except AccountBasic.DoesNotExist: record = None if record is not None: return Response(res_format(None, code=11004, msg='该手机号已注册')) # 检查密码强度,密码长度为 8-20 位 if not check_password(password): return Response(res_format(None, code=11006, msg='密码长度须为 8-20 位')) # 检查短信验证码 if not Sender().verify(token, mobile, code, 'register'): return Response(res_format(None, code=11005, msg='短信验证码不正确')) # 所有检查都已通过,开始创建账号 try: record = AccountBasic(mobile=mobile, password=encrypt_md5(password)) record.save() # 注册成功通知 NoticeTpl.registration_success(record) # 注册成功,生成 JWT 并返回 uid = str(record) ip_addr = request.META.get('REMOTE_ADDR') ua_string = request.META.get('HTTP_UA', '') token = JWTHelper().encode(uid, ua_string) return Response(res_format({'token': token}, msg='注册成功')) except Exception as e: return Response(res_format(None, code=11007, msg=str(e))) @action(methods=['post'], detail=False) def smscaptcha(self, request, format=None): '''获取短信验证码,使用手机号、验证码类型、图片验证码 token 和图片验证码认证信息''' # 获取及检查参数 params = ['mobile', 'action', 'token', 'authenticate'] mobile, action, token, authenticate = get_dict_values( request.data, params) if check_none(mobile, action, token, authenticate): raise ParseError('缺少参数:' + ', '.join(params)) # 检查 action # 注意 ⚠️ # 增加 action 的时候 # 需要在这增加 action_list # 还要在下面增加相应的判断条件 # 还要在上面的 schema 中加注释 # 最重要的,要在 model 里增加字段对应可选的值 action_list = [ 'register', # 注册 'retrive', # 找回密码 'replace', # 换绑手机号 ] if action not in action_list: return Response(res_format(None, code=11008, msg='非法的 action')) # action 如果是注册,则先判断用户表内是否无该手机号 # action 如果是找回密码,则先判断用户表内是否有该手机号 # action 如果是换绑手机号,则先判断是否登录 try: record = AccountBasic.objects.get(mobile=mobile) except AccountBasic.DoesNotExist: record = None if action == 'register' and record is not None: return Response(res_format(None, code=11009, msg='该手机号已注册')) if action == 'reteive' and record is None: return Response(res_format(None, code=11010, msg='该手机号未注册')) if action == 'replace' and request.auth is None: return Response(res_format(None, code=11018, msg='请先登录')) # 获取其他参数 ip_addr = request.META.get('REMOTE_ADDR') if request.auth: user = request.user else: user = None # 检查手机号格式 if not check_mobile(mobile): return Response(res_format(None, code=11011, msg='手机号格式错误')) # 首先检查图形验证码是否验证通过 if not CaptchaUtil._verify_by_authenticate(token, authenticate): return Response(res_format(None, code=11012, msg='图形验证码验证失败')) # 验证通过,发送短信验证码,返回 token sms = Sender().sms_verification_code(mobile, action, ip_addr, user) if sms['ok']: # 发送成功 return Response(res_format({'token': sms['token']}, msg=sms['msg'])) else: return Response(res_format(None, code=11013, msg=sms['msg'])) @action(methods=['post'], detail=False) def retrive(self, request, format=None): '''找回密码,使用手机号、短信验证码、token 和新密码''' # 获取及检查参数 params = ['mobile', 'code', 'token', 'password'] mobile, code, token, password = get_dict_values(request.data, params) if check_none(mobile, code, token, password): raise ParseError('缺少参数:' + ', '.join(params)) # 检查手机号是否已经注册 try: record = AccountBasic.objects.get(mobile=mobile) except AccountBasic.DoesNotExist: record = None if record is None: return Response(res_format(None, code=11014, msg='该手机号未注册')) # 检查密码强度,密码长度为 8-20 位, if not check_password(password): return Response(res_format(None, code=11015, msg='新密码长度须为 8-20 位')) # 检查短信验证码 if not Sender().verify(token, mobile, code, 'retrive'): return Response(res_format(None, code=11016, msg='短信验证码不正确')) # 所有检查都已通过,开始修改密码 record.password = encrypt_md5(password) # 更新 account tokenTime 使之前的登录状态失效 record.refresh_token() record.save() # 找回密码成功通知 NoticeTpl.retrieve_password_success(record) return Response(res_format(None, msg='重置密码成功')) @action(detail=False) def wechat(self, request, format=None): '''使用微信小程序的 code 换取 openid''' # 获取及检查参数 params = ['code'] code = get_dict_values(request.query_params, params) if check_none(code): raise ParseError('缺少参数:' + ', '.join(params)) # 向微信 API 发送请求 res = get_weixin_by_code(code) if res['ok']: # 获取成功 # 存数据库 openid = res['data']['openid'] session_key = res['data']['session_key'] try: record = WechatBasic.objects.get(openid=openid) record.sessionKey = session_key except WechatBasic.DoesNotExist: record = WechatBasic(openid=openid, sessionKey=session_key) # 如果是登录状态,则绑定 uid if request.auth: record.uid = request.user record.save() return Response(res_format({'openid': openid})) else: # 获取失败 return Response(res_format(code=11017, msg=res['msg'])) @action(detail=False) def userinfo(self, request, format=None): '''通过 UID 获取用户信息,包括:昵称、头像、性别和发布图书的信息等''' uid = request.query_params.get('uid', '') user = request.user try: # 获取用户基础信息 record = AccountBasic.objects.get(uid=uid) # 检查账号状态 if record.status == 'delete': return Response(res_format(None, code=11022, msg='该账号已删除')) if record.status == 'violation': return Response(res_format(None, code=11023, msg='该账号已被举报')) result = {} result['userinfo'] = UserInfoSerializer(record).data # 获取发布图书信息 record_publishinfo = PublishBook.objects.filter( uid=uid).order_by('-updateTime') if not user.is_anonymous and str(user) == uid: # 如果查询自己的出售信息,则可以查询到系统状态为:正常和被举报的信息;以及发布状态为:正常、已下架和已出售的信息 record_publishinfo = record_publishinfo.filter( Q(systemStatus='normal') | Q(systemStatus='violation')).filter( Q(status='normal') | Q(status='sold') | Q(status='removed')) else: # 其他用户只能搜索到系统状态为:正常,且发布状态为:正常和已出售的信息 record_publishinfo = record_publishinfo.filter( systemStatus='normal').filter( Q(status='normal') | Q(status='sold')) result['bookInfo'] = PublishBookOverviewSerializers( record_publishinfo, many=True).data # 获取发布二手物品信息 record_goodsinfo = PublishGoods.objects.filter( uid=uid).order_by('-updateTime') if not user.is_anonymous and str(user) == uid: # 如果查询自己的出售信息,则可以查询到系统状态为:正常和被举报的信息;以及发布状态为:正常、已下架和已出售的信息 record_goodsinfo = record_goodsinfo.filter( Q(systemStatus='normal') | Q(systemStatus='violation')).filter( Q(status='normal') | Q(status='sold') | Q(status='removed')) else: # 其他用户只能搜索到系统状态为:正常,且发布状态为:正常和已出售的信息 record_goodsinfo = record_goodsinfo.filter( systemStatus='normal').filter( Q(status='normal') | Q(status='sold')) result['goodsInfo'] = PublishGoodsOverviewSerializers( record_goodsinfo, many=True).data # 记录查询日志 if not (not user.is_anonymous and str(user) == uid): record_log = UserinfoLog( uid=user if not user.is_anonymous else None, user=record) record_log.save() # 显示个人主页访问次数 record_views = UserinfoLog.objects.filter(user=record) result['views'] = len(record_views) except AccountBasic.DoesNotExist: return Response(res_format(code=11021, msg='无相关用户信息')) return Response(res_format(result))