【Python百日进阶-WEB开发】Day179 - Django案例:11短信验证码

news2024/11/19 5:14:29

文章目录

  • 九、短信验证码
    • 9.1 短信验证码逻辑分析
    • 9.2 容联云通讯短信平台
      • 9.2.1 容联云通讯短信平台介绍
      • 9.2.2 容联云通讯短信SDK测试
        • 9.2.2.1 美多商城meiduo_mall.apps.verifications.libs中新建yuntongxun包,结构如下:
        • 9.2.2.2 ccp_sms.py代码
        • 9.2.2.3 CCPRestSDK.py ,python3代码
        • 9.2.2.4 测试发送短信
      • 9.2.3 封装发送短信单例类
    • 9.3 短信验证码接口设计和定义
    • 9.4 短信验证码后端逻辑
    • 9.5 短信验证码前端逻辑

九、短信验证码

9.1 短信验证码逻辑分析

在这里插入图片描述

9.2 容联云通讯短信平台

了解容联云通讯平台和短信SDK的使用方式,

9.2.1 容联云通讯短信平台介绍

  1. 容联云官网:https://www.yuntongxun.com/
    在这里插入图片描述
    2.注册登录
    在这里插入图片描述
  2. 通过认证,企业认证或个人认证,提交申请后一般需要第二天通过。发送短信0.06元/条,注册赠送8元,我发过一条了。
    在这里插入图片描述
  3. 添加容联云子应用,通过认证后可以上线应用
    在这里插入图片描述
  4. 添加测试号码
    在这里插入图片描述
  5. 短信模板
    在这里插入图片描述
  6. Python Demo中模板短信的使用说明
    https://doc.yuntongxun.com/p/5a533e0c3b8496dd00dce08c
    在这里插入图片描述
  7. 开发文档-SDK接口文件
    https://www.yuntongxun.com/doc/ready/demo/1_4_1_2.html
    在这里插入图片描述

9.2.2 容联云通讯短信SDK测试

9.2.2.1 美多商城meiduo_mall.apps.verifications.libs中新建yuntongxun包,结构如下:

在这里插入图片描述

9.2.2.2 ccp_sms.py代码

#-*- coding: UTF-8 -*-  

from meiduo_mall.apps.verifications.libs.yuntongxun.CCPRestSDK import REST
# import ConfigParser
import ssl

# 全局取消证书验证
ssl._create_default_https_context = ssl._create_unverified_context  

#主帐号
accountSid= '8aaf07 这里填真实的主账号 5b0963df1';

#主帐号Token
accountToken= 'b809 这里填真实的Token 4018733';

#应用Id
appId='8a21 这里填真实的AppID 10d53ba6';

#请求地址,格式如下,不需要写http://
serverIP='app.cloopen.com';

#请求端口 
serverPort='8883';

#REST版本号
softVersion='2013-12-26';

  # 发送模板短信
  # @param to 手机号码
  # @param datas 内容数据 格式为数组 例如:{'12','34'},如不需替换请填 ''
  # @param $tempId 模板Id

def sendTemplateSMS(to,datas,tempId):

    
    #初始化REST SDK
    rest = REST(serverIP,serverPort,softVersion)
    rest.setAccount(accountSid,accountToken)
    rest.setAppId(appId)
    
    result = rest.sendTemplateSMS(to,datas,tempId)
    print(result)
    
   
#sendTemplateSMS(手机号码,内容数据,模板Id)
if __name__ == '__main__':
  # 注意测试的短信模板编号为1,短信验证码为123456,有效期为5分钟
  sendTemplateSMS('13953800865', ['123456', 5], 1)

9.2.2.3 CCPRestSDK.py ,python3代码

这个文件官网示例文件使用python2.7写的,有五六个地方需要修改,这是修改测试过的python3代码
修改内容主要包括:
1、头部导包
2、MD5加密
3、req.add_data
4、base64加密等

from hashlib import md5
import base64
import datetime
import urllib.request	# py3
import json
from meiduo_mall.apps.verifications.libs.yuntongxun.xmltojson import xmltojson	# py3
from xml.dom import minidom 

class REST:
    
    AccountSid=''
    AccountToken=''
    AppId=''
    SubAccountSid=''
    SubAccountToken=''
    ServerIP=''
    ServerPort=''
    SoftVersion=''
    Iflog=True #是否打印日志
    Batch=''  #时间戳
    BodyType = 'xml'#包体格式,可填值:json 、xml
    
     # 初始化
     # @param serverIP       必选参数    服务器地址
     # @param serverPort     必选参数    服务器端口
     # @param softVersion    必选参数    REST版本号
    def __init__(self,ServerIP,ServerPort,SoftVersion):

        self.ServerIP = ServerIP;
        self.ServerPort = ServerPort;
        self.SoftVersion = SoftVersion;
    
    
    # 设置主帐号
    # @param AccountSid  必选参数    主帐号
    # @param AccountToken  必选参数    主帐号Token
    
    def setAccount(self,AccountSid,AccountToken):
      self.AccountSid = AccountSid;
      self.AccountToken = AccountToken;   
    

    # 设置子帐号
    # 
    # @param SubAccountSid  必选参数    子帐号
    # @param SubAccountToken  必选参数    子帐号Token
 
    def setSubAccount(self,SubAccountSid,SubAccountToken):
      self.SubAccountSid = SubAccountSid;
      self.SubAccountToken = SubAccountToken;    

    # 设置应用ID
    # 
    # @param AppId  必选参数    应用ID

    def setAppId(self,AppId):
       self.AppId = AppId; 
    
    def log(self,url,body,data):
        print('这是请求的URL:')
        print (url);
        print('这是请求包体:')
        print (body);
        print('这是响应包体:')
        print (data);
        print('********************************')
    

    # 创建子账号
    # @param friendlyName   必选参数      子帐号名称
    def CreateSubAccount(self, friendlyName):
        
        self.accAuth()
        nowdate = datetime.datetime.now()
        self.Batch = nowdate.strftime("%Y%m%d%H%M%S")
        #生成sig
        signature = self.AccountSid + self.AccountToken + self.Batch;
        signature = signature.encode('utf-8') # py3
        # sig = md5.new(signature).hexdigest().upper()
        sig = md5(signature).hexdigest().upper() # py3
        #拼接URL
        url = "https://"+self.ServerIP + ":" + self.ServerPort + "/" + self.SoftVersion + "/Accounts/" + self.AccountSid + "/SubAccounts?sig=" + sig
        #生成auth
        src = self.AccountSid + ":" + self.Batch;
        # auth = base64.encodestring(src).strip()
        auth = base64.encodestring(src.encode()).strip() 	# py3
        req = urllib.request.Request(url)
        self.setHttpHeader(req)
        req.add_header("Authorization", auth)
        #xml格式
        body ='''<?xml version="1.0" encoding="utf-8"?><SubAccount><appId>%s</appId>\
            <friendlyName>%s</friendlyName>\
            </SubAccount>\
            '''%(self.AppId, friendlyName)
        
        if self.BodyType == 'json': 
            #json格式
            body = '''{"friendlyName": "%s", "appId": "%s"}'''%(friendlyName,self.AppId)
        data=''
        # req.add_data(body)
        req.data = body.encode() # py3
        try:
            res = urllib.request.urlopen(req);
            data = res.read()
            res.close()
        
            if self.BodyType=='json':
                #json格式
                locations = json.loads(data)
            else:
                #xml格式
                xtj=xmltojson()
                locations=xtj.main(data)
            if self.Iflog:
                self.log(url,body,data)
            return locations
        except Exception as error:
            if self.Iflog:
                self.log(url,body,data)
            return {'172001':'网络错误'}

9.2.2.4 测试发送短信

1.vscode终端输出
在这里插入图片描述

  1. 测试手机收到的短信
    在这里插入图片描述

9.2.3 封装发送短信单例类

问题:如果同时发送多个短信验证码,那么就会同时创建多个RET SDK的对象,会消耗很多额外的内存空间。
解决方法:使用单例类,它的特点是只有一个实例存在
使用场景:当我们希望在整个系统中,某个类只出现一个实例时,就可以使用单例类设计模式
改写后的代码:

#-*- coding: UTF-8 -*-  

from meiduo_mall.apps.verifications.libs.yuntongxun.CCPRestSDK import REST
# import ConfigParser
import ssl

# 全局取消证书验证
ssl._create_default_https_context = ssl._create_unverified_context  

#主帐号
accountSid= '8aaf07087a331dc7017afb85b0963df1';
#主帐号Token
accountToken= 'b809c84015db41c8a4a3d84224018733';
#应用Id
appId='8a216da87a332d53017afb8d10d53ba6';
#请求地址,格式如下,不需要写http://
serverIP='app.cloopen.com';
#请求端口 
serverPort='8883';
#REST版本号
softVersion='2013-12-26';
# 发送模板短信
# @param to 手机号码
# @param datas 内容数据 格式为数组 例如:{'12','34'},如不需替换请填 ''
# @param $tempId 模板Id

# def sendTemplateSMS(to,datas,tempId):
#     #初始化REST SDK
#     rest = REST(serverIP,serverPort,softVersion)
#     rest.setAccount(accountSid,accountToken)
#     rest.setAppId(appId)
    
#     result = rest.sendTemplateSMS(to,datas,tempId)
#     print(result)
    

class CCP(object):
  """ 发送短信验证码的单例类 """
  def __new__(cls, *args, **kwargs):
    """ 定义单例化的初始化方法,返回值为单例 """
    # 判断单例是否存在,利用动态赋值的_instance属性。如果单例不存在,就初始化单例
    if not hasattr(cls, '_instance'):
      cls._instance = super(CCP, cls).__new__(cls, *args, **kwargs)
      #初始化REST SDK,赋值给单例属性,实现与单例同生共死,唯一存在
      cls._instance.rest = REST(serverIP,serverPort,softVersion)
      cls._instance.rest.setAccount(accountSid,accountToken)
      cls._instance.rest.setAppId(appId)
    # 返回单例
    return cls._instance  

  def send_template_sms(self, to, datas, tempId):
    """ 
    定义对象方法,发送短信验证码 
    to:手机号码,字符串,多个手机号码用逗号分隔
    datas:发送内容,双元素列表,第一个元素为验证码字符串,第二个元素为整数有效时间(分钟)
    tempID:模板ID,测试模板为1
    返回值:成功:0,失败:-1
    """
    result = self.rest.sendTemplateSMS(to,datas,tempId)
    print(result)
    # 根据发送是否成功返回0或-1
    if result.get('statusCode') == '000000':
      return 0
    else:
      return -1

#sendTemplateSMS(手机号码,内容数据,模板Id)
if __name__ == '__main__':
  # 注意测试的短信模板编号为1,短信验证码为123456,有效期为5分钟
  # sendTemplateSMS('13953800865', ['123456', 5], 1)
  
  # 单例类发送短信验证码
  CCP().send_template_sms('13953800865', ['6543258', 5], 1)

9.3 短信验证码接口设计和定义

在这里插入图片描述

9.4 短信验证码后端逻辑

  1. verifications.urls.py中
from django.urls import path, re_path
from . import views

app_name = 'verifications'

urlpatterns = [
    # 图形验证码,re_path路由正则校验,响应json数据,不需要重定向,也就不需要命名空间
    re_path(r'^image_codes/(?P<uuid>[\w-]+)/$', views.ImageCodeView.as_view()),

    # 短信验证码,re_path路由正则校验,响应json数据,不需要重定向,也就不需要命名空间
    re_path(r'^sms_codes/(?P<mobile>1[3-9]\d{9})/$', views.SMSCodeView.as_view()),

]
  1. verifications.views.py中的短信验证码类视图
from django.views import View
from django_redis import get_redis_connection
from django import http
import random, logging

from meiduo_mall.apps.verifications.libs.captcha.captcha import captcha
from . import constants
from meiduo_mall.utils.response_code import RETCODE
from meiduo_mall.apps.verifications.libs.yuntongxun.ccp_sms import CCP


# 创建日志输出器
logger = logging.getLogger('django')

class SMSCodeView(View):
    """ 短信验证码 """
    def get (self, request, mobile):
        """
        param:request,请求对象;mobile,手机号
        return:JSON
        """
        """ 接收和校验参数 """
        # 接收参数
        image_code_client = request.GET.get('image_code')
        uuid = request.GET.get('uuid')

        # 校验参数,mobile不需要视图内校验,在路由处已经校验完毕了,错误进不了视图
        if not all([image_code_client, uuid]):
            return http.HttpResponseForbidden('缺少必传参数!')

        # 判断用户是否频繁发送短信验证码
        redis_conn = get_redis_connection('verify_code')     # 创建redis库的连接
        send_flag = redis_conn.get(f'send_flag_{mobile}')
        if send_flag:   #  已存在
            return http.JsonResponse({'code': RETCODE.THROTTLINGERR, 'errmsg': '发送短信验证码过于频繁!'})

        """" 主体业务逻辑 """
        # 1.从redis库中提取图形验证码
        image_code_server = redis_conn.get(f'img_{uuid}')
        if image_code_server is None:
            return http.JsonResponse({'code': RETCODE.IMAGECODEERR, 'errmsg': '图形验证码已失效!'})

        # 2.删除redis中存储的图形验证码
        redis_conn.delete(f'img_{uuid}')
        
        # 3.对比图形验证码
        image_code_server = image_code_server.decode()   # 提取的数据时bytes类型,需要转换为字符串
        if image_code_client.lower() != image_code_server.lower():  # 全部转为小写
            return http.JsonResponse({'code': RETCODE.IMAGECODEERR, 'errmsg': '输入图形验证码有误!'})

        # 4.生成短信验证码:随机6位数字,不足前面补0, 000007
        sms_code = '%06d' % random.randint(0, 999999)
        logger.info(sms_code)   # 手动输出短信验证码的日志

        # 5.保存短信验证码,为优化redis的性能,使用管道队列操作
        # 5.1 创建redis pipeline管道队列
        pl = redis_conn.pipeline()
        # 5.2 将命令添加到队列中
        pl.setex(f'sms_{mobile}', constants.SMS_CODE_REDIS_EXPIRES, sms_code)   # sms_code存储到redis数据库
        pl.setex(f'send_flag_{mobile}', constants.SEND_SMS_CODE_INTERVAL, 1)    # 保存短信验证码标记,有效期60秒,标记1表示60秒内给该手机发送了验证码
        # 5.3 执行队列命令
        pl.excute()

        # 6.单例类发送短信验证码
        CCP().send_template_sms(mobile, [sms_code, constants.SMS_CODE_REDIS_EXPIRES // 60], constants.SEND_SMS_TEMPLATE_ID)

        """ 响应结果 """
        return http.JsonResponse({'code': RETCODE.OK, 'errmsg': '短信验证码发送成功!'})

9.5 短信验证码前端逻辑

  1. register.html中的短信验证码部分
<li>
    <label for="">短信验证码</label>
    <input type="text" v-model="sms_code" @blur="check_sms_code" name="sms_code" id="sms_code" class="msg_input">
    <a @click="send_sms_code" class="get_msg_code">[[ sms_code_tip ]]</a>
    <span class="error_tip" v-show="error_sms_code">[[ error_sms_code_message]]</span>
</li>
  1. register.js中的方法
//发送手机验证码
send_sms_code(){
    //避免恶意用户频繁点击获取短信验证码的A标签
    if (this.send_flag == true) {   //已经点击了发送短信验证码
        return;
    }
    this.send_flag = true;      
    //校验用户输入的mobile和image_code
    this.check_mobile();
    this.check_image_code();
    if (this.error_image_code == true || this.error_mobile == true) {
        this.send_flag == false;
        return;
    }
    //?后面为查询字符串参数
    let url = '/sms_codes/'+ this.mobile +'/?image_code=' + this.image_code + '&uuid=' + this.uuid;  
    axios.get(url, {
        responseType: 'json'
    })
        .then(response => {
            if (response.data.code == '0') {    //发送短信验证码成功
                //展示倒计时60S效果 setInterval('回调函数', '时间间隔1000毫秒')
                let num = 60;
                let t = setInterval(() => {     // t 为定时器编号
                    if (num == 1){          //倒计时即将结束
                        clearInterval(t);   // 停止回调函数的执行
                        this.sms_code_tip = '获取短信验证码';   // 还原 sms_code_tip 的提示信息
                        this.generate_image_code();     //重新生成图形验证码
                        this.send_flag == false;
                    } else {                // 正在倒计时
                        num -= 1;
                        this.sms_code_tip = num + '秒';
                    }
                }, 1000)
            } else {    
                if (response.data.code == '4001') { // 图形验证码错误
                // 渲染错误信息
                    this.error_image_code_message = response.data.errmsg;
                    this.error_image_code = true;
                    this.send_flag == false;
                }
            }
        })
        .catch(error => {
            console.log(error.response);
            this.send_flag == false;
        })
},

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/10900.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一起来庆祝属于GISer的节日GIS DAY!

01 概述 作为一名GISer的你&#xff0c;有没有想过其实我们GISer也有自己的节日&#xff1f;这个节日便是GIS DAY&#xff0c;今年的GIS DAY恰在今天&#xff08;2022年11月16日&#xff09;。究竟什么是GIS DAY&#xff1f;这里为大家介绍一下这个节日。 02 什么是GIS DAY …

vue的移动端项目打包成手机的app软件apk格式

目录 前提准备&#xff1a; 1、vue项目npm run build打包成dist文件夹 2、注册hbuilderx账号&#xff0c;获取appid 步骤 一、创建h5app空模版 二、 将打包完成生成dist文件目录复制到新建的项目里 三、检测打包的index.html是否白屏 四、 配置manifest.js应用入口页面…

作为项目经理必须具备的能力

作为项目管理者&#xff0c;每天都要应对项目中的所有问题&#xff0c;安排任务&#xff0c;还要照顾下属的情绪。管理者应该怎么做。 1、计划制定 项目经理作为项目管理者&#xff0c;需要制定计划&#xff0c;合理化分配任务。 项目经理可以使用甘特图制定项目计划&#xf…

微服务feign接口声明的3种方式使用与分析

前言 feign调用方式是微服务调用最为广泛的使用方式&#xff0c;feign接口声明位置也是比较关键的一环。目前来说&#xff0c;feign的3种接口声明方式各自存在利弊&#xff0c;并不存在最优解决方案&#xff0c;只能根据需求去选择。本文中不作详细项目搭建过程&#xff0c;但…

做3D建模的女生多吗?揭露行业比列

有&#xff0c;但是不多&#xff0c;这是常态✅ 其实就像是IT领域的男女比例一样&#xff0c;但是也不是没有。更何况现在女孩子顶半边天&#xff0c;遇到领导是女生的也不少&#xff0c;未来的情况如何也不能完全的预估。 • ❤️事业是热爱做的事&#xff0c;工作是不得不做…

索引【MySQL】

1.1 概念 索引是一种特殊的文件&#xff0c;包含着对数据表里所有记录的引用指针。可以对表中的一列或多列创建索引&#xff0c; 并指定索引的类型&#xff0c;各类索引有各自的数据结构实现。 1.2 作用 数据库中的表、数据、索引之间的关系&#xff0c;类似于书架上的图书、…

简洁直观解释精确率、召回率、F1 值、ROC、AUC

混淆矩阵 当我们在做二分类预测时&#xff0c;把预测情况与实际情况的所有结果两两混合&#xff0c;结果就会出现以下4种情况&#xff0c;就组成了混淆矩阵。 P&#xff08;Positive&#xff09;&#xff1a;代表正样本N&#xff08;Negative&#xff09;&#xff1a;代表负样…

在vscode中开发sass教程:sass语法

sass官网&#xff1a;Sass世界上最成熟、稳定和强大的CSS扩展语言 | Sass中文网sass&#xff1a;世界上最成熟、最稳定、最强大的专业级css扩展语言&#xff01;sass是一个css的预编译工具&#xff0c;也就是能够更优雅的书写css&#xff1b;1、sass使用说明&#xff1a; 基于…

【毕业设计】后端实现——账单通过关键词简单分析收支

&#x1f308;据说&#xff0c;看我文章时 关注、点赞、收藏 的 帅哥美女们 心情都会不自觉的好起来。 前言&#xff1a; &#x1f9e1;作者简介&#xff1a;大家好我是 user_from_future &#xff0c;意思是 “ 来自未来的用户 ” &#xff0c;寓意着未来的自己一定很棒~ ✨个…

C++ STL中的set详解

前言 在学习csp题解的时候接触到这个数据结构&#xff0c;故在此记录一下其概念及应用。 基本概念 set的底层采用的是红黑树&#xff0c;所有元素都会根据元素的键值自动排序&#xff0c;方便管理元素&#xff0c;但不支持直接修改键值。 应用 头文件调用 #include <i…

python中is和==的区别,地址和重新复制后,地址变化

简单总结 现象描述&#xff1a;一、“编辑器“中和把赋值语句放在”同一行的cmd环境“中&#xff0c;相同值的不同变量会指向同一个地址 二、交互式/cmd环境中&#xff0c;若赋值相同值的变量在不同行输入&#xff0c;那么变量也会指向不同地址 三、程序编辑器的程序运行就相当…

B2B撮合管理系统优势有哪些?如何助力传统仪器仪表制造业企业数字化转型

仪器仪表制造业是我国制造业的重要组成部分&#xff0c;经过多年的快速发展&#xff0c;我国仪器仪表制造业生产和开发能力产业体系日趋庞大&#xff0c;产销增幅也是高歌猛进&#xff0c;对推动国民经济发展具有重要意义。 然而&#xff0c;随着我国仪器仪表制造业的发展&…

GAN模型1

本次是用GAN模型弄出时光机效果~ 穿越时空的人脸 得到你100年前的样子~~ 时光穿梭第一步&#xff1a;解决数据集难题 鉴于最近StyleGAN在高质量人脸合成和编辑方面的成功&#xff0c;许多工作都集中在使用预先训练好的StyleGAN模型进行人像编辑。 然而&#xff0c;现有的技术…

[附源码]计算机毕业设计JAVA儿童资源教育网站

[附源码]计算机毕业设计JAVA儿童资源教育网站 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybati…

聚观365|抖音上线“防打扰保护工具”;亚马逊拟计划裁员1万人

今日要闻&#xff1a;抖音上线“防打扰保护工具”&#xff1b;亚马逊拟计划裁员1万人&#xff1b;苹果为iPhone14提供同机维修&#xff1b;美国硅谷裁员潮蔓延&#xff1b;乔布斯拖鞋拍卖融入NTF元素 抖音上线“防打扰保护工具” 11月15日&#xff0c;抖音上线“防打扰保护工具…

【数据结构与算法】第一章 绪论 2-数据结构的基本概念

第一章 绪论 2-数据结构的基本概念 思考&#xff1f; 程序 算法数据结构&#xff0c;算法 逻辑控制数据结构有两大用途&#xff1a; 一是用于存放要处理的数据&#xff0c;如迷宫地图二是用于实现算法策略&#xff0c;如迷宫例子中探索方向增量数组&#xff0c;回溯的栈&am…

如何深度自定义mybatis

回顾mybatis的操作的核心步骤 编写核心类SqlSessionFacotryBuild进行解析配置文件 深度分析解析SqlSessionFacotryBuild干的核心工作 编写核心类SqlSessionFacotry 深度分析解析SqlSessionFacotry干的核心工作 编写核心类SqlSession 深度分析解析SqlSession干的核心工作 …

物联网电池产品硬件电路设计思维

最近在整改之前工程师设计的电路板&#xff0c;是采用18650电池供电的一个物联网小板。 像这种电池供电的产品&#xff0c;很重要的一点就是要保证其低功耗&#xff0c;才得以提高续航&#xff0c;因此&#xff0c;对于这类电路板的对外接口的设计&#xff0c;对供电的控制尤其…

Arch Linux 的安装

Arch Linux 的安装 作者&#xff1a;Grey 原文地址&#xff1a; 博客园&#xff1a;Arch Linux 的安装 CSDN&#xff1a;Arch Linux 的安装 版本 Arch Linux&#xff1a;2022.07.01 VMware workstation&#xff1a; 16.2 安装步骤 下载 Arch Linux 并记录其 kernel 版…

【考研英语语法】口语语法

区别一&#xff1a;句子结构 口语中结构更为简单&#xff0c;较少使用从句 只有少量高频连词&#xff08;and / but / or / so / because / if / when&#xff09;正式&#xff1a; While the region was remarkable for its natural beauty, the family experienced seriousl…