目录

1.前端发送请求生成订单

  1.前端点击支付按钮生成订单

  2.结算成功之后应该清除结算页面的数据

  3.后端计算结算页面总原价格和总的真实价格并存到数据库订单表中

2.优惠劵

  1.准备工作

  2.前端展示优惠券信息-初始化

  3.优惠券-前端获取优惠券数据+后端接口

  4.结算页面计算真实总价格

  5.优惠券是否真的能够使用+优惠劵前端计算

    1.优惠劵的选中效果

    2.不可点击的不应该具有点击效果

    3.优惠券箭头收缩之后 取消优惠券的选中状态

    4.当选中的优惠劵发生变化时 重新计算总价

  6.优惠劵后端对优惠劵进行校验

1.前端发送请求生成订单

1.前端点击支付按钮生成订单

昨日讲到了后端如何生成订单(order/add_money),但是前端的请求还没有发,现在来做一下前端发送请求来生成订单。

点击支付按钮,触发生成订单事件,生成一个订单

order.vue

<!-- html -->
<!-- 给支付按钮绑定一个事件 该事件向后端发起请求来生成订单 -->
<el-col :span="4" class="cart-pay"><span @click="payhander">支付</span></el-col>
// js
// 生成订单
payhander(){
let token = localStorage.token || sessionStorage.token;
this.$axios.post(`${this.$settings.Host}/order/add_money/`,{ // 生成订单需要提交的支付类型、优惠券、积分
"pay_type":this.pay_type,
"coupon":this.current_coupon,
"credit":0 },{
headers:{
'Authorization':'jwt ' + token
}
}).then((res)=>{
this.$message.success('订单已经生成,马上跳转支付页面')
}).catch((error)=>{
this.$message.error(error.response.data.msg);
}) }

2.结算成功之后应该清除结算页面的数据

结算成功之后应该清除结算界面的数据,如果用户还想购买课程的话,应该去购物车去再次选中自己想要购买的商品再购买,然后再重新生成订单。

所以应该redis中删除用户选中的课程id,也就是selected_cart的数据

order/serializers.py

# serializers.py
def create(self, validated_data): # 结算成功之后,再清除
conn = get_redis_connection('cart')
conn.delete('selected_cart_%s' % user_id) return order_obj

3.后端计算结算页面总原价格和总的真实价格并存到数据库订单表中

order/serializers.py

def create(self, validated_data):
try:
# 生成订单号 [日期,用户id,自增数据] total_price = 0 # 总原价
total_real_price = 0 # 总真实价格 with transaction.atomic(): # 添加事务
# 生成订单,保存到数据库中
......
# 生成订单详情
......
# 计算所有课程的总原价
total_price += course_obj.price # 计算所有课程的总真实价格
total_real_price += course_obj.real_price(expire_id) # 将订单总原价和总真实价格存到数据库表中
order_obj.total_price = total_price
order_obj.real_price = total_real_price
order_obj.save() except Exception:
raise models.Order.DoesNotExist return order_obj

2.优惠劵

1.准备工作

1.创建coupon应用,并配置INSTALLAPP

python3 ../../manage.py startapp coupon

2.coupon/models.py

from django.db import models
from lyapi.utils.models import BaseModel
from users.models import User
from datetime import timedelta # Create your models here.
class Coupon(BaseModel):
"""优惠券"""
coupon_choices = (
(0, '折扣优惠'),
(1, '减免优惠')
)
name = models.CharField(max_length=32, verbose_name="优惠券标题")
coupon_type = models.SmallIntegerField(choices=coupon_choices, default=0, verbose_name="优惠券类型")
timer = models.IntegerField(verbose_name="优惠券有效期", default=7, help_text="默认当前优惠券7天有效,如果设置值为-1则表示当前优惠券永久有效")
condition = models.IntegerField(blank=True, default=0, verbose_name="满足使用优惠券的价格条件,如果设置值为0,则表示没有任何条件")
sale = models.TextField(verbose_name="优惠公式", help_text="""
*号开头表示折扣价,例如*0.82表示八二折;<br>
-号开头表示减免价,例如-10表示在总价基础上减免10元<br>
""") class Meta:
db_table = "ly_coupon"
verbose_name="优惠券"
verbose_name_plural="优惠券" def __str__(self):
return "%s" % (self.name) class UserCoupon(BaseModel):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="coupons", verbose_name="用户")
coupon = models.ForeignKey(Coupon, on_delete=models.CASCADE, related_name="users", verbose_name="优惠券")
start_time = models.DateTimeField(verbose_name="优惠策略的开始时间")
is_use = models.BooleanField(default=False,verbose_name="优惠券是否使用过") class Meta:
db_table = "ly_user_coupon"
verbose_name = "用户的优惠券"
verbose_name_plural = "用户的优惠券" def __str__(self):
return "优惠券:%s,用户:%s" % (self.coupon.name, self.user.username) @property
def end_time(self):
s_time = self.start_time
timer = self.coupon.timer #天数 return s_time + timedelta(days=timer)

优惠劵表结构设计

3.数据库迁移指令

python3 ../../manage.py makemigrations
python3 ../../manage.py migrate

4.adminx注册

coupon/adminx.py

import xadmin
from .models import Coupon
class CouponModelAdmin(object):
"""优惠券模型管理类"""
list_display = ["name","coupon_type","timer"]
xadmin.site.register(Coupon, CouponModelAdmin) from .models import UserCoupon
class UserCouponModelAdmin(object):
"""我的优惠券模型管理类"""
list_display = ["user","coupon","start_time","is_use"] xadmin.site.register(UserCoupon, UserCouponModelAdmin)

5.插入一些数据

INSERT INTO `ly_coupon` VALUES (1,1,1,0,'2019-08-21 15:59:04.568037','2019-08-21 15:59:04.568061','十元优惠券',1,30,10,'-10'),(2,2,1,0,'2019-08-21 15:59:33.764807','2019-08-21 15:59:33.764830','五十元优惠券',1,30,50,'-50'),(3,3,1,0,'2019-08-21 16:00:10.090100','2019-08-21 16:00:10.090126','9折优惠券',2,7,0,'*0.9');

INSERT INTO `ly_user_coupon` VALUES
(1,1,1,0,'2019-08-21 16:00:40.823977','2019-08-23 19:23:58.117600','2019-08-21 01:00:00.000000',1,3,1),
(2,2,1,0,'2019-08-21 16:00:49.868597','2019-08-22 09:37:46.010037','2019-10-01 01:00:00.000000',0,2,1),
(3,3,1,0,'2019-08-21 16:01:09.051862','2019-08-23 19:31:02.605253','2019-08-21 01:01:00.000000',1,1,1),
(4,5,1,0,'2019-08-22 08:48:56.406671','2019-08-22 08:48:56.406694','2019-08-22 17:48:00.000000',0,2,1);

2.前端展示优惠券信息-初始化

<!-- html -->
<div class="discount">
<div id="accordion">
<div class="coupon-box">
<div class="icon-box">
<span class="select-coupon">使用优惠劵:</span>
<a class="select-icon unselect" :class="use_coupon?'is_selected':''" @click="use_coupon=!use_coupon"><img class="sign is_show_select" src="../../static/img/12.png" alt=""></a>
<span class="coupon-num">有0张可用</span>
</div>
<p class="sum-price-wrap">商品总金额:<span class="sum-price">0.00元</span></p>
</div>
<div id="collapseOne" v-if="use_coupon">
<ul class="coupon-list" v-if="coupon_list.length>0">
<li class="coupon-item" :class="select_coupon(index,coupon.id)" v-for="(coupon,index) in coupon_list" @click="change_coupon(index,coupon.id)">
<p class="coupon-name">8.5折优惠券</p>
<p class="coupon-condition">满0元可以使用</p>
<p class="coupon-time start_time">开始时间:</p>
<p class="coupon-time end_time">过期时间:</p>
</li> </ul>
<div class="no-coupon" v-if="coupon_list.length<1">
<span class="no-coupon-tips">暂无可用优惠券</span>
</div>
</div>
</div>
<div class="credit-box">
<label class="my_el_check_box"><el-checkbox class="my_el_checkbox" v-model="use_credit"></el-checkbox></label>
<p class="discount-num1" v-if="!use_credit">使用我的贝里</p>
<p class="discount-num2" v-else><span>总积分:100,已抵扣 ¥0.00,本次花费0积分</span></p>
</div>
<p class="sun-coupon-num">优惠券抵扣:<span>0.00元</span></p>
</div>

前端展示优惠劵信息-HTML

/* css */
.coupon-box{
text-align: left;
padding-bottom: 22px;
padding-left:30px;
border-bottom: 1px solid #e8e8e8;
}
.coupon-box::after{
content: "";
display: block;
clear: both;
}
.icon-box{
float: left;
}
.icon-box .select-coupon{
float: left;
color: #666;
font-size: 16px;
}
.icon-box::after{
content:"";
clear:both;
display: block;
}
.select-icon{
width: 20px;
height: 20px;
float: left;
}
.select-icon img{
max-height:100%;
max-width: 100%;
margin-top: 2px;
transform: rotate(-90deg);
transition: transform .5s;
}
.is_show_select{
transform: rotate(0deg)!important;
}
.coupon-num{
height: 22px;
line-height: 22px;
padding: 0 5px;
text-align: center;
font-size: 12px;
float: left;
color: #fff;
letter-spacing: .27px;
background: #fa6240;
border-radius: 2px;
margin-left: 20px;
}
.sum-price-wrap{
float: right;
font-size: 16px;
color: #4a4a4a;
margin-right: 45px;
}
.sum-price-wrap .sum-price{
font-size: 18px;
color: #fa6240;
} .no-coupon{
text-align: center;
width: 100%;
padding: 50px 0px;
align-items: center;
justify-content: center; /* 文本两端对其 */
border-bottom: 1px solid rgb(232, 232, 232);
}
.no-coupon-tips{
font-size: 16px;
color: #9b9b9b;
}
.credit-box{
height: 30px;
margin-top: 40px;
display: flex;
align-items: center;
justify-content: flex-end
}
.my_el_check_box{
position: relative;
}
.my_el_checkbox{
margin-right: 10px;
width: 16px;
height: 16px;
}
.discount{
overflow: hidden;
}
.discount-num1{
color: #9b9b9b;
font-size: 16px;
margin-right: 45px;
}
.discount-num2{
margin-right: 45px;
font-size: 16px;
color: #4a4a4a;
}
.sun-coupon-num{
margin-right: 45px;
margin-bottom:43px;
margin-top: 40px;
font-size: 16px;
color: #4a4a4a;
display: inline-block;
float: right;
}
.sun-coupon-num span{
font-size: 18px;
color: #fa6240;
}
.coupon-list{
margin: 20px 0;
}
.coupon-list::after{
display: block;
content:"";
clear: both;
}
.coupon-item{
float: left;
margin: 15px 8px;
width: 180px;
height: 100px;
padding: 5px;
background-color: #fa3030;
cursor: pointer;
}
.coupon-list .active{
background-color: #fa9000;
}
.coupon-list .disable{
cursor: not-allowed;
background-color: #fa6060;
}
.coupon-condition{
font-size: 12px;
text-align: center;
color: #fff;
}
.coupon-name{
color: #fff;
font-size: 24px;
text-align: center;
}
.coupon-time{
text-align: left;
color: #fff;
font-size: 12px;
}
.unselect{
margin-left: 0px;
transform: rotate(-90deg);
}
.is_selected{
transform: rotate(-1turn)!important;
}
.coupon-item p{
margin: 0;
padding: 0;
}

前端展示优惠劵信息样式-CSS

3.优惠券-前端获取优惠券数据+后端接口

1.优惠劵后端接口

coupon/urls.py

# coupon/urls.py
from django.urls import path,re_path
from . import views urlpatterns = [
re_path(r'list/', views.CouponView.as_view(),), ]

lyapi/urls.py

# lyapi/urls.py

from xadmin.plugins import xversion
xversion.register_models() urlpatterns = [
......
path(r'coupon/',include('coupon.urls')), ]

coupon/views.py

# coupon/views.py
from django.shortcuts import render
from rest_framework.generics import ListAPIView
from . import models
from rest_framework.permissions import IsAuthenticated from .serializers import UserCouponModelSerializer class CouponView(ListAPIView): serializer_class = UserCouponModelSerializer
permission_classes = [IsAuthenticated, ]
def get_queryset(self): return models.UserCoupon.objects.filter(is_show=True,is_deleted=False,is_use=False, user_id=self.request.user.id)

coupon/serializers.py

# coupon/serializers.py
from rest_framework import serializers
from .models import Coupon, UserCoupon
class CouponModelSerializer(serializers.ModelSerializer):
class Meta:
model = Coupon
fields = ("name","coupon_type","timer","condition","sale") class UserCouponModelSerializer(serializers.ModelSerializer):
coupon = CouponModelSerializer()
class Meta:
model = UserCoupon
fields = ("id","start_time","coupon","end_time")

2.间接计算优惠劵的结束时间

# coupon/models.py
class UserCoupon(BaseModel):
......A
@property # 调用类中该方法时不需要加大括号 将其视作为属性
def end_time(self):
s_time = self.start_time
timer = self.coupon.timer #天数
return s_time + timedelta(days=timer)

3.前端发送请求获取优惠券数据

order.vue

// order.vue -js
get_user_coupon(){
let token = localStorage.token || sessionStorage.token;
this.$axios.get(`${this.$settings.Host}/coupon/list/`,{
headers:{
'Authorization':'jwt ' + token
}
}).then((res)=>{
this.coupon_list = res.data; // 获取到的优惠劵数据
}).catch((error)=>{
this.$message.error('优惠券获取错误')
}) },
<!-- order.vue  html  -->
<ul class="coupon-list" v-if="coupon_list.length>0">
<li class="coupon-item" :class="select_coupon(index,coupon.id)" v-for="(coupon,index) in coupon_list" @click="change_coupon(index,coupon.id)">
<p class="coupon-name">{{coupon.coupon.name}}</p>
<p class="coupon-condition">满{{coupon.coupon.condition}}元可以使用</p>
<p class="coupon-time start_time">开始时间:{{coupon.start_time.replace('T',' ')}}</p>
<p class="coupon-time end_time">过期时间:{{coupon.end_time.replace('T',' ')}}</p>
</li> </ul>

4.结算页面计算真实总价格

之前的结算页面只差真实的总价格没有计算了。现在通过后端计算真实总价格然后发送给前端。

1.后端计算好结算页面的总价格和总真实价格

cart/views.py

# cart/views.py

# 结算页面数据
def show_pay_info(self,request):
user_id = request.user.id
conn = get_redis_connection('cart')
select_list = conn.smembers('selected_cart_%s' % user_id)
data = [] ret = conn.hgetall('cart_%s' % user_id) # dict {b'1': b'0', b'2': b'0'} total_price = 0
total_real_price = 0 for cid, eid in ret.items():
expire_id = int(eid.decode('utf-8'))
if cid in select_list: course_id = int(cid.decode('utf-8'))
course_obj = models.Course.objects.get(id=course_id) if expire_id > 0:
expire_obj = models.CourseExpire.objects.get(id=expire_id) # 查询到每个已勾选课程的真实价格
course_real_price = course_obj.real_price(expire_id) # 计算出所有课程的总真实价格
total_real_price += course_real_price data.append({
'course_id':course_obj.id,
'name':course_obj.name,
'course_img':contains.SERVER_ADDR + course_obj.course_img.url , # 结算页面的每条数据都显示为每个课程的真实价格
'real_price':course_real_price, 'expire_text':expire_obj.expire_text,
})
else:
course_real_price = course_obj.real_price(expire_id)
total_real_price += course_real_price
data.append({
'course_id': course_obj.id,
'name': course_obj.name,
'course_img': contains.SERVER_ADDR + course_obj.course_img.url,
'real_price': course_real_price,
'expire_text': '永久有效',
}) return Response({'data':data,'total_real_price':total_real_price})

2.前端发送请求 获取结算页面的数据、总价格、总真实价格

order.vue

// Order.vue
get_order_data(){
let token = localStorage.token || sessionStorage.token;
this.$axios.get(`${this.$settings.Host}/cart/expires/`,{
headers:{
'Authorization':'jwt ' + token
}
}).then((res)=>{
// 获取到课程名称、课程封面图、有效期信息等
this.course_list = res.data.data; // 获取到后端发送过来的总真实价格
this.total_real_price = res.data.total_real_price; // 获取到后端发送过来的总原价格
this.total_price = res.data.total_real_price;
})
},

5.优惠券是否真的能够使用+优惠劵前端计算

1.优惠劵的选中效果

不在活动范围内的优惠劵应该设置不可选中的效果

如果选中了当期优惠劵,则应该给优惠劵设置为选中的效果

order.vue

// order.vue  js
select_coupon(index,coupon_id){ // 拿到你当前点击的那条优惠劵数据
let current_c = this.coupon_list[index]
if (this.total_real_price < current_c.coupon.condition){
return 'disable'
} // '/1000'拿到时间戳
let current_time = new Date() / 1000;
let s_time = new Date(current_c.start_time) / 1000
let e_time = new Date(current_c.end_time) / 1000 // 如果优惠劵不在活动时间范围内 则设置效果为不可选中
if (current_time < s_time || current_time > e_time){
return 'disable'
} // 如果优惠劵是当前被选中的优惠劵 则设置效果为选中
if (this.current_coupon === coupon_id){
return 'active'
} return ''
},
<!-- order.vue  html -->
<li class="coupon-item" :class="select_coupon(index,coupon.id)" v-for="(coupon,index) in coupon_list" @click="change_coupon(index,coupon.id)">

2.不可点击的不应该具有点击效果

change_coupon(index,coupon_id){
let current_c = this.coupon_list[index] // 如果优惠劵不符合条件 则优惠劵是不可点击的
if (this.total_real_price < current_c.coupon.condition){
return false
}
let current_time = new Date() / 1000;
let s_time = new Date(current_c.start_time) / 1000
let e_time = new Date(current_c.end_time) / 1000 // 如果优惠劵不符合条件 则优惠劵是不可点击的
if (current_time < s_time || current_time > e_time){
return false
} this.current_coupon = coupon_id;
this.coupon_obj = current_c; },

3.优惠券箭头收缩之后 取消优惠券的选中状态

order.vue

watch:{
use_coupon(){
// 如果点击箭头收缩 那么就将优惠劵的选中状态取消
if (this.use_coupon === false){
this.current_coupon = 0; }
},

4.当选中的优惠劵发生变化时 重新计算总价

order.vue

// 当选中的优惠券发生变化时,重新计算总价
current_coupon(){
this.cal_total_price();
} methods: { cal_total_price(){
// 当用户选中了某个优惠劵
if (this.current_coupon !== 0){ // 1.获取用户使用优惠劵前的真实价格
let tt = this.total_real_price; // 2.获取当前优惠劵的优惠公式
let sales = this.coupon_obj.coupon.sale; // 3.取出优惠公式的数字部分
let d = parseFloat(this.coupon_obj.coupon.sale.substr(1));
if (sales[0] === '-'){
tt = this.total_real_price - d
}else if (sales[0] === '*'){
tt = this.total_real_price * d
}
this.total_price = tt; } },

6.优惠劵后端对优惠劵进行校验

前端虽然对优惠劵进行校验,但是前端所显示的都是虚假的,后端也要对优惠劵进行校验。

class OrderModelSerializers:
def validate(self, attrs): # 验证支付方式是否合法
pay_type = int(attrs.get('pay_type',0)) # if pay_type not in [i[0] for i in models.Order.pay_choices]:
raise serializers.ValidationError('支付方式不对!') # 优惠券校验,看看是否过期了等等
coupon_id = attrs.get('coupon', 0)
if coupon_id > 0:
try:
user_conpon_obj = UserCoupon.objects.get(id=coupon_id)
except:
raise serializers.ValidationError('订单创建失败,优惠券id不对')
# 校验优惠劵是否在活动时间范围内
now = datetime.datetime.now().timestamp()
start_time = user_conpon_obj.start_time.timestamp()
end_time = user_conpon_obj.end_time.timestamp()
if now < start_time or now > end_time:
raise serializers.ValidationError('订单创建失败,优惠券不在使用范围内,滚犊子') # todo 积分上限校验 return attrs

day86:luffy:前端发送请求生成订单&结算页面优惠劵的实现的更多相关文章

  1. java web前端发送请求的4种方式

    表单 action, 链接href,js 绑定按钮 ajax绑定标签 <h1>通过表单提交参数</h1> <form action="/day46_v1/Ser ...

  2. day87:luffy:结算页面积分&支付宝接口

    目录 1.积分 2.支付 1.积分 1.关于积分的表结构 1.在user表中添加credit字段 + 设计一个积分的表结构 user/models.py class User(AbstractUser ...

  3. java后台发送请求并获取返回值

    项目中需要前端发送请求给后端,而后端需要从另一个平台中取数据然后再透传给前端,通过下述代码将其实现.在此记录一下. package com.autotest.utils; import java.io ...

  4. 用vue.js重构订单计算页面

    在很久很久以前做过一个很糟糕的订单结算页面,虽然里面各区域(收货地址)使用模块化加载,但是偶尔会遇到某个模块加载失败的问题导致订单提交的数据有误. 大致问题如下: 1. 每个模块都采用usercont ...

  5. 向.net后端发送请求获取数据,在前端动态填充表格

    实现效果 实现步骤 通过Ajax请求的方式 1.在前端定义Table 2.通过Ajax向.net后端发送数据请求 3.在.net后端定义方法供前端调用,并返回所需的数据 4.通过构造字符串的方式,将后 ...

  6. angularjs和jquery前端发送以http请求formdata数据

    formdata是比较常见的前端发送给后端的请求,不仅可以上传数据,而且同时可以上传文件. jquery使用http请求上传formdata数据的方法: var formdata = new Form ...

  7. Java生成二进制文件与Postman以二进制流的形式发送请求

    业务描述: 模拟终端(智能家居)发送HTTP POST请求,请求参数为二进制流:而且,二进制流是加密后的数据,因此调试分两步: 1.Java代码生成加密后数据,并保存为二进制流文件 (电脑上的图片就是 ...

  8. Query通过Ajax向PHP服务端发送请求并返回JSON数据

    Query通过Ajax向PHP服务端发送请求并返回JSON数据 服务端PHP读取MYSQL数据,并转换成JSON数据,传递给前端Javascript,并操作JSON数据.本文将通过实例演示了jQuer ...

  9. 爬虫模块介绍--request(发送请求模块)

    爬虫:可见即可爬   # 每个网站都有爬虫协议 基础爬虫需要使用到的三个模块 requests 模块  # 模拟发请求的模块 PS:python原来有两个模块urllib和urllib的升级urlli ...

随机推荐

  1. P2832 行路难

    题面 Link 题目背景 小X来到了山区,领略山林之乐.在他乐以忘忧之时,他突然发现,开学迫在眉睫 题目描述 山区有 \(n\) 座山.山之间有 \(m\) 条羊肠小道,每条连接两座山,只能单向通过, ...

  2. Codeforces Global Round 11 个人题解(B题)

    Codeforces Global Round 11 1427A. Avoiding Zero 题目链接:click here 待补 1427B. Chess Cheater 题目链接:click h ...

  3. eclipse 配置opencv

    1 准备 eclipse 2017 JDK1.8 opencv 4.40 2 配置 新建java工程 添加jar包 选择opencv-xxx.jar包 加入原生库 选择原生库位置 确认即可,测试 新建 ...

  4. 多测师讲解python_斐波那契数列:_高级讲师肖sir

    def f(n): a,b=1,1 if n==1 or n ==2: return 1 else: i=3 while i<=n: a,b=b,a+b i+=1 return bprint(f ...

  5. spring-security-结合JWT的简单demo

    spring-security-demo 前言:本来是想尽量简单简单点的写一个demo的,但是spring-security实在是内容有点多,写着写着看起来就没那么简单了,想入门spring-secu ...

  6. 《我想进大厂》之Dubbo普普通通9问

    这是面试专题系列第四篇,Dubbo系列.Dubbo本身并不复杂,而且官方文档写的非常清楚详细,面试中dubbo的问题一般不会很多,从分层到工作原理.负载均衡策略.容错机制.SPI机制基本就差不多了,最 ...

  7. 通过jQuery来获取DropDownList的Text/Value属性值

    脚本代码: <script src="Scripts/jquery-1.4.1-vsdoc.js" type="text/javascript">& ...

  8. jmeter环境变量配置

    参考博客:超全 https://blog.csdn.net/qq_39720249/article/details/80721777

  9. MySQL备份和恢复[3]-mysqldump备份工具

    mysqldump 概述 逻辑备份工具: mysqldump, mydumper, phpMyAdmin Schema和数据存储在一起.巨大的SQL语句.单个巨大的备份文件 mysqldump:是My ...

  10. [开源] .Net ORM FreeSql 1.10.0 稳步向行

    写在开头 FreeSql 是 .NET 开源生态下的 ORM 轮子,转眼快两年了,说真的开源不容易(只有经历过才明白).今天带点干货和湿货给大家,先说下湿货. 认识我的人,知道 CSRedisCore ...