diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js
index 3e9601bf8..e7a4d28ff 100644
--- a/app/assets/javascripts/admin.js
+++ b/app/assets/javascripts/admin.js
@@ -83,4 +83,4 @@ $(document).on("turbolinks:before-cache", function () {
// });
$(function () {
-});
\ No newline at end of file
+});
diff --git a/app/controllers/wechats/js_sdk_signatures_controller.rb b/app/controllers/wechats/js_sdk_signatures_controller.rb
new file mode 100644
index 000000000..e4ee0da27
--- /dev/null
+++ b/app/controllers/wechats/js_sdk_signatures_controller.rb
@@ -0,0 +1,8 @@
+class Wechats::JsSdkSignaturesController < ApplicationController
+ def create
+ signature = Util::Wechat.js_sdk_signature(params[:url], params[:noncestr], params[:timestamp])
+ render_ok(signature: signature)
+ rescue Util::Wechat::Error => ex
+ render_error(ex.message)
+ end
+end
\ No newline at end of file
diff --git a/app/libs/util/wechat.rb b/app/libs/util/wechat.rb
new file mode 100644
index 000000000..069322f18
--- /dev/null
+++ b/app/libs/util/wechat.rb
@@ -0,0 +1,74 @@
+module Util::Wechat
+ BASE_SITE = 'https://api.weixin.qq.com'.freeze
+
+ Error = Class.new(StandardError)
+
+ class << self
+ attr_accessor :appid, :secret
+
+ def js_sdk_signature(url, noncestr, timestamp)
+ str = { jsapi_ticket: jsapi_ticket, noncestr: noncestr, timestamp: timestamp, url: url }.to_query
+ Digest::SHA1.hexdigest(str)
+ end
+
+ def access_token
+ # 7200s 有效时间
+ Rails.cache.fetch(access_token_cache_key, expires_in: 100.minutes) do
+ result = request(:get, '/cgi-bin/token', appid: appid, secret: secret, grant_type: 'client_credential')
+ result['access_token']
+ end
+ end
+
+ def refresh_access_token
+ Rails.cache.delete(access_token_cache_key)
+ access_token
+ end
+
+ def jsapi_ticket
+ # 7200s 有效时间
+ Rails.cache.fetch(jsapi_ticket_cache_key, expires_in: 100.minutes) do
+ result = request(:get, '/cgi-bin/ticket/getticket', access_token: access_token, type: 'jsapi')
+ result['ticket']
+ end
+ end
+
+ def refresh_jsapi_ticket
+ Rails.cache.delete(jsapi_ticket_cache_key)
+ jsapi_ticket
+ end
+
+ def access_token_cache_key
+ "#{base_cache_key}/access_token"
+ end
+
+ def jsapi_ticket_cache_key
+ "#{base_cache_key}/jsapi_ticket"
+ end
+
+ def base_cache_key
+ "wechat/#{appid}"
+ end
+
+ private
+
+ def request(method, url, **params)
+ Rails.logger.error("[wechat] request: #{method} #{url} #{params.inspect}")
+
+ client = Faraday.new(url: BASE_SITE)
+ response = client.public_send(method, url, params)
+ result = JSON.parse(response.body)
+
+ Rails.logger.error("[wechat] response:#{response.status} #{result.inspect}")
+
+ if response.status != 200
+ raise Error, result.inspect
+ end
+
+ if result['errcode'].present? && result['errcode'].to_i.nonzero?
+ raise Error, result.inspect
+ end
+
+ result
+ end
+ end
+end
\ No newline at end of file
diff --git a/config/configuration.yml.example b/config/configuration.yml.example
index 2a4f4f017..6feee28d9 100644
--- a/config/configuration.yml.example
+++ b/config/configuration.yml.example
@@ -6,6 +6,9 @@ defaults: &defaults
cate_id: '-1'
callback_url: 'http://47.96.87.25:48080/api/callbacks/aliyun_vod.json'
signature_key: 'test12345678'
+ wechat:
+ appid: 'test'
+ secret: 'test'
development:
<<: *defaults
diff --git a/config/initializers/aliyun_vod_init.rb b/config/initializers/aliyun_vod_init.rb
index 47b1dc6a3..9185eac20 100644
--- a/config/initializers/aliyun_vod_init.rb
+++ b/config/initializers/aliyun_vod_init.rb
@@ -4,7 +4,7 @@ aliyun_vod_config = {}
begin
config = Rails.application.config_for(:configuration)
aliyun_vod_config = config['aliyun_vod']
- raise 'oauth wechat config missing' if aliyun_vod_config.blank?
+ raise 'aliyun vod config missing' if aliyun_vod_config.blank?
rescue => ex
raise ex if Rails.env.production?
diff --git a/config/initializers/util_wechat_init.rb b/config/initializers/util_wechat_init.rb
new file mode 100644
index 000000000..cb39afcaf
--- /dev/null
+++ b/config/initializers/util_wechat_init.rb
@@ -0,0 +1,16 @@
+wechat_config = {}
+
+begin
+ config = Rails.application.config_for(:configuration)
+ wechat_config = config['wechat']
+ raise 'wechat config missing' if wechat_config.blank?
+rescue => ex
+ raise ex if Rails.env.production?
+
+ puts %Q{\033[33m [warning] wechat config or configuration.yml missing,
+ please add it or execute 'cp config/configuration.yml.example config/configuration.yml' \033[0m}
+ wechat_config = {}
+end
+
+Util::Wechat.appid = wechat_config['appid']
+Util::Wechat.secret = wechat_config['secret']
diff --git a/config/routes.rb b/config/routes.rb
index e53f3a57a..27d5c4488 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -880,6 +880,10 @@ Rails.application.routes.draw do
end
end
+ namespace :wechats do
+ resource :js_sdk_signature, only: [:create]
+ end
+
#git 认证回调
match 'gitauth/*url', to: 'gits#auth', via: :all
diff --git a/public/react/src/modules/courses/boards/BoardsNew.js b/public/react/src/modules/courses/boards/BoardsNew.js
index 58cd0c02b..8411e8c49 100644
--- a/public/react/src/modules/courses/boards/BoardsNew.js
+++ b/public/react/src/modules/courses/boards/BoardsNew.js
@@ -186,7 +186,7 @@ class BoardsNew extends Component{
});
}
onAttachmentRemove = (file) => {
- if(file.response!=undefined){
+ if(!file.percent || file.percent == 100){
confirm({
// title: '确定要删除这个附件吗?',
title: '是否确认删除?',
diff --git a/public/react/src/modules/courses/busyWork/CommonWorkAppraise.js b/public/react/src/modules/courses/busyWork/CommonWorkAppraise.js
index d1e271981..698fbd398 100644
--- a/public/react/src/modules/courses/busyWork/CommonWorkAppraise.js
+++ b/public/react/src/modules/courses/busyWork/CommonWorkAppraise.js
@@ -218,7 +218,8 @@ class CommonWorkAppraise extends Component{
{item.filesize}
{/*{item.delete===true?this.onAttachmentRemove(item.id)}>:""}*/}
- {item.delete===true?this.onAttachmentRemove(item.id)}>:""}
+ {item.delete===true?this.onAttachmentRemove(item.id)}>:""}
+ {/* style={{display: 'none'}} */}
)
})}
diff --git a/public/react/src/modules/courses/busyWork/CommonWorkPost.js b/public/react/src/modules/courses/busyWork/CommonWorkPost.js
index fec3f02a1..98fe5401c 100644
--- a/public/react/src/modules/courses/busyWork/CommonWorkPost.js
+++ b/public/react/src/modules/courses/busyWork/CommonWorkPost.js
@@ -320,11 +320,19 @@ class CommonWorkPost extends Component{
// ModalSave: ()=>this.deleteAttachment(file),
// ModalCancel:this.cancelAttachment
// })
- if(file.response!=undefined){
- this.deleteAttachment(file)
+
+ if(!file.percent || file.percent == 100){
+ this.props.confirm({
+ content: '是否确认删除?',
+ onOk: () => {
+ this.deleteAttachment(file)
+ },
+ onCancel() {
+ console.log('Cancel');
+ },
+ });
return false;
}
-
}
cancelAttachment=()=>{
diff --git a/public/react/src/modules/courses/busyWork/NewWork.js b/public/react/src/modules/courses/busyWork/NewWork.js
index fb2d18de0..4b2224fd1 100644
--- a/public/react/src/modules/courses/busyWork/NewWork.js
+++ b/public/react/src/modules/courses/busyWork/NewWork.js
@@ -3,7 +3,8 @@ import { Input, InputNumber, Form, Button, Checkbox, Upload, Icon, message, Moda
import axios from 'axios'
import '../css/busyWork.css'
import '../css/Courses.css'
-import { WordsBtn, getUrl, ConditionToolTip, appendFileSizeToUploadFile, appendFileSizeToUploadFileAll } from 'educoder'
+import { WordsBtn, getUrl, ConditionToolTip, appendFileSizeToUploadFile, appendFileSizeToUploadFileAll
+ , getUploadActionUrl } from 'educoder'
import TPMMDEditor from '../../tpm/challengesnew/TPMMDEditor';
import CBreadcrumb from '../common/CBreadcrumb'
@@ -243,7 +244,7 @@ class NewWork extends Component{
}
onAttachmentRemove = (file, stateName) => {
- if(file.response!=undefined){
+ if(!file.percent || file.percent == 100){
this.props.confirm({
content: '是否确认删除?',
@@ -331,7 +332,7 @@ class NewWork extends Component{
// https://github.com/ant-design/ant-design/issues/15505
// showUploadList={false},然后外部拿到 fileList 数组自行渲染列表。
// showUploadList: false,
- action: `${getUrl()}/api/attachments.json`,
+ action: `${getUploadActionUrl()}`,
onChange: this.handleContentUploadChange,
onRemove: (file) => this.onAttachmentRemove(file, 'contentFileList'),
beforeUpload: (file) => {
diff --git a/public/react/src/modules/courses/coursesPublic/AccessoryModal.js b/public/react/src/modules/courses/coursesPublic/AccessoryModal.js
index 3b25ceb6d..750679111 100644
--- a/public/react/src/modules/courses/coursesPublic/AccessoryModal.js
+++ b/public/react/src/modules/courses/coursesPublic/AccessoryModal.js
@@ -73,7 +73,7 @@ class AccessoryModal extends Component{
// ModalCancel:this.cancelAttachment
// })
// return false;
- if(file.response!=undefined){
+ if(!file.percent || file.percent == 100){
this.deleteAttachment(file);
}
}
diff --git a/public/react/src/modules/courses/coursesPublic/AccessoryModal2.js b/public/react/src/modules/courses/coursesPublic/AccessoryModal2.js
index a9a627387..a15cb0617 100644
--- a/public/react/src/modules/courses/coursesPublic/AccessoryModal2.js
+++ b/public/react/src/modules/courses/coursesPublic/AccessoryModal2.js
@@ -64,7 +64,7 @@ class AccessoryModal2 extends Component{
// ModalCancel:this.cancelAttachment
// })
// return false;
- if(file.response!=undefined){
+ if(!file.percent || file.percent == 100){
this.deleteAttachment(file);
}
diff --git a/public/react/src/modules/courses/coursesPublic/SelectSetting.js b/public/react/src/modules/courses/coursesPublic/SelectSetting.js
index d52b328e2..1a18513e7 100644
--- a/public/react/src/modules/courses/coursesPublic/SelectSetting.js
+++ b/public/react/src/modules/courses/coursesPublic/SelectSetting.js
@@ -296,7 +296,7 @@ class Selectsetting extends Component{
onAttachmentRemove = (file) => {
- if(file.response!=undefined){
+ if(!file.percent || file.percent == 100){
const url = `/attachments/${file.response ? file.response.id : file.uid}.json`
axios.delete(url, {
})
diff --git a/public/react/src/modules/courses/coursesPublic/sendResource.js b/public/react/src/modules/courses/coursesPublic/sendResource.js
index 56c85439c..a9ceb6405 100644
--- a/public/react/src/modules/courses/coursesPublic/sendResource.js
+++ b/public/react/src/modules/courses/coursesPublic/sendResource.js
@@ -132,7 +132,7 @@ class Sendresource extends Component{
onAttachmentRemove = (file) => {
- if(file.response!=undefined){
+ if(!file.percent || file.percent == 100){
const url = `/attachments/${file.response ? file.response.id : file.uid}.json`
axios.delete(url, {
})
diff --git a/public/react/src/modules/courses/exercise/ExerciseDisplay.js b/public/react/src/modules/courses/exercise/ExerciseDisplay.js
index a0296476f..cbaf38c80 100644
--- a/public/react/src/modules/courses/exercise/ExerciseDisplay.js
+++ b/public/react/src/modules/courses/exercise/ExerciseDisplay.js
@@ -53,11 +53,15 @@ class ExerciseDisplay extends Component{
componentDidMount = () => {
const Id = this.props.match.params.Id
if (Id) {
- const url = `/exercises/${Id}.json`
+ const url = `/${this.props.urlPath || 'exercises'}/${Id}.json`
axios.get(url)
.then((response) => {
- if (response.data.status == 0) {
- this.setState({...response.data})
+ if (response.data.exercise) {
+ response.data.exercise.exercise_description = response.data.exercise.exercise_description || response.data.exercise.description
+ response.data.exercise.exercise_name = response.data.exercise.exercise_name || response.data.exercise.name
+ response.data.exercise.exercise_status = response.data.exercise.exercise_status == undefined ? 1 : response.data.exercise.exercise_status
+ this.setState({...response.data})
+ this.props.detailFetchCallback && this.props.detailFetchCallback(response);
}
})
.catch(function (error) {
diff --git a/public/react/src/modules/courses/graduation/tasks/GraduationTasksSubmitedit.js b/public/react/src/modules/courses/graduation/tasks/GraduationTasksSubmitedit.js
index b603e375d..2d9280ec1 100644
--- a/public/react/src/modules/courses/graduation/tasks/GraduationTasksSubmitedit.js
+++ b/public/react/src/modules/courses/graduation/tasks/GraduationTasksSubmitedit.js
@@ -157,7 +157,7 @@ class GraduationTasksSubmitedit extends Component{
}
onAttachmentRemove = (file) => {
- if(file.response!=undefined){
+ if(!file.percent || file.percent == 100){
let {attachments,fileList}=this.state;
const url = `/attachments/${file}.json`
axios.delete(url, {
diff --git a/public/react/src/modules/courses/graduation/tasks/GraduationTasksSubmitnew.js b/public/react/src/modules/courses/graduation/tasks/GraduationTasksSubmitnew.js
index ed2731c61..57eb6502f 100644
--- a/public/react/src/modules/courses/graduation/tasks/GraduationTasksSubmitnew.js
+++ b/public/react/src/modules/courses/graduation/tasks/GraduationTasksSubmitnew.js
@@ -146,7 +146,7 @@ class GraduationTasksSubmitnew extends Component{
// },
// });
// return false;
- if(file.response!=undefined){
+ if(!file.percent || file.percent == 100){
this.setState({
Modalstype:true,
Modalstopval:'确定要删除这个附件吗?',
diff --git a/public/react/src/modules/courses/graduation/tasks/GraduationTasksappraiseMainEditor.js b/public/react/src/modules/courses/graduation/tasks/GraduationTasksappraiseMainEditor.js
index 6b741f413..6a481c61c 100644
--- a/public/react/src/modules/courses/graduation/tasks/GraduationTasksappraiseMainEditor.js
+++ b/public/react/src/modules/courses/graduation/tasks/GraduationTasksappraiseMainEditor.js
@@ -88,7 +88,7 @@ class GraduationTasksappraiseMainEditor extends Component{
this.setState({ fileList });
}
onAttachmentRemove = (file, stateName) => {
- if(file.response!=undefined){
+ if(!file.percent || file.percent == 100){
this.props.confirm({
content: '确定要删除这个附件吗?',
okText: '确定',
diff --git a/public/react/src/modules/courses/graduation/tasks/GraduationTasksedit.js b/public/react/src/modules/courses/graduation/tasks/GraduationTasksedit.js
index e6139bad6..256dba7d4 100644
--- a/public/react/src/modules/courses/graduation/tasks/GraduationTasksedit.js
+++ b/public/react/src/modules/courses/graduation/tasks/GraduationTasksedit.js
@@ -150,7 +150,7 @@ class GraduationTasksedit extends Component{
}
onAttachmentRemove = (file) => {
- if(file.response!=undefined){
+ if(!file.percent || file.percent == 100){
// debugger
this.cancelAttachment();
const url = `/attachments/${file.response ? file.response.id : file.uid}.json`
diff --git a/public/react/src/modules/courses/graduation/tasks/GraduationTasksnew.js b/public/react/src/modules/courses/graduation/tasks/GraduationTasksnew.js
index 3b6b60ac8..da83e8d7e 100644
--- a/public/react/src/modules/courses/graduation/tasks/GraduationTasksnew.js
+++ b/public/react/src/modules/courses/graduation/tasks/GraduationTasksnew.js
@@ -173,7 +173,7 @@ class GraduationTasksnew extends Component {
}
onAttachmentRemove = (file) => {
- if(file.response!=undefined){
+ if(!file.percent || file.percent == 100){
const url = `/attachments/${file.response ? file.response.id : file.uid}.json`
// const url = `/attachments/${file}.json`
axios.delete(url, {})
diff --git a/public/react/src/modules/courses/graduation/topics/GraduateTopicNew.js b/public/react/src/modules/courses/graduation/topics/GraduateTopicNew.js
index 511deb511..28d6fcf77 100644
--- a/public/react/src/modules/courses/graduation/topics/GraduateTopicNew.js
+++ b/public/react/src/modules/courses/graduation/topics/GraduateTopicNew.js
@@ -216,7 +216,7 @@ class GraduateTopicNew extends Component{
}
onAttachmentRemove = (file) => {
- if(file.response!=undefined){
+ if(!file.percent || file.percent == 100){
confirm({
title: '确定要删除这个附件吗?',
okText: '确定',
diff --git a/public/react/src/modules/courses/graduation/topics/GraduateTopicPostWorksNew.js b/public/react/src/modules/courses/graduation/topics/GraduateTopicPostWorksNew.js
index 3998669d7..60fa071a9 100644
--- a/public/react/src/modules/courses/graduation/topics/GraduateTopicPostWorksNew.js
+++ b/public/react/src/modules/courses/graduation/topics/GraduateTopicPostWorksNew.js
@@ -163,7 +163,7 @@ class GraduateTopicPostWorksNew extends Component{
this.setState({ fileList });
}
onAttachmentRemove = (file) => {
- if(file.response!=undefined){
+ if(!file.percent || file.percent == 100){
confirm({
title: '确定要删除这个附件吗?',
okText: '确定',
diff --git a/public/react/src/modules/courses/members/modal/CreateGroupByImportModal.js b/public/react/src/modules/courses/members/modal/CreateGroupByImportModal.js
index 77a41e47d..afbe8de58 100644
--- a/public/react/src/modules/courses/members/modal/CreateGroupByImportModal.js
+++ b/public/react/src/modules/courses/members/modal/CreateGroupByImportModal.js
@@ -73,7 +73,7 @@ class CreateGroupByImportModal extends Component{
}
onAttachmentRemove = (file) => {
- if(file.response!=undefined){
+ if(!file.percent || file.percent == 100){
this.props.confirm({
content: '是否确认删除?',
diff --git a/public/react/src/modules/courses/shixunHomework/Listofworksstudentone.js b/public/react/src/modules/courses/shixunHomework/Listofworksstudentone.js
index 64e2e6a99..0755b9df5 100644
--- a/public/react/src/modules/courses/shixunHomework/Listofworksstudentone.js
+++ b/public/react/src/modules/courses/shixunHomework/Listofworksstudentone.js
@@ -2499,6 +2499,8 @@ class Listofworksstudentone extends Component {
// console.log(data);
// console.log(datas);
// console.log(this.props.isAdmin());
+ console.log("学生老师列表");
+
return (
this.props.isAdmin() === true ?
(
diff --git a/public/react/src/modules/forums/MemoNew.js b/public/react/src/modules/forums/MemoNew.js
index 476d41a8a..88425ab53 100644
--- a/public/react/src/modules/forums/MemoNew.js
+++ b/public/react/src/modules/forums/MemoNew.js
@@ -561,7 +561,7 @@ class MemoNew extends Component {
}
}
onAttachmentRemove = (file) => {
- if(file.response!=undefined){
+ if(!file.percent || file.percent == 100){
this.props.confirm({
// title: '确定要删除这个附件吗?',
content: '是否确认删除?',
diff --git a/public/react/src/modules/moop_cases/CaseNew.js b/public/react/src/modules/moop_cases/CaseNew.js
index 6d9e5b251..bb684de22 100644
--- a/public/react/src/modules/moop_cases/CaseNew.js
+++ b/public/react/src/modules/moop_cases/CaseNew.js
@@ -47,7 +47,7 @@ class CaseNew extends Component{
// 上传附件-删除确认框
onAttachmentRemove = (file, stateName) => {
- if(file.response!=undefined){
+ if(!file.percent || file.percent == 100){
this.props.confirm({
content: '是否确认删除?',
onOk: () => {
diff --git a/public/react/src/modules/tpm/TPMsettings/TPMsettings.js b/public/react/src/modules/tpm/TPMsettings/TPMsettings.js
index e7e8a1d75..ca6c6a342 100644
--- a/public/react/src/modules/tpm/TPMsettings/TPMsettings.js
+++ b/public/react/src/modules/tpm/TPMsettings/TPMsettings.js
@@ -1384,7 +1384,7 @@ export default class TPMsettings extends Component {
}
onAttachmentRemove = (file) => {
- if(file.response!=undefined){
+ if(!file.percent || file.percent == 100){
confirm({
title: '确定要删除这个附件吗?',
okText: '确定',
diff --git a/public/react/src/modules/tpm/newshixuns/Newshixuns.js b/public/react/src/modules/tpm/newshixuns/Newshixuns.js
index 9a22269f6..2313426a2 100644
--- a/public/react/src/modules/tpm/newshixuns/Newshixuns.js
+++ b/public/react/src/modules/tpm/newshixuns/Newshixuns.js
@@ -772,7 +772,7 @@ class Newshixuns extends Component {
}
onAttachmentRemove = (file) => {
- if(file.response!=undefined){
+ if(!file.percent || file.percent == 100){
confirm({
title: '确定要删除这个附件吗?',
okText: '确定',
diff --git a/public/react/src/modules/user/usersInfo/banks/BanksIndex.js b/public/react/src/modules/user/usersInfo/banks/BanksIndex.js
index 916601420..6a303f62b 100644
--- a/public/react/src/modules/user/usersInfo/banks/BanksIndex.js
+++ b/public/react/src/modules/user/usersInfo/banks/BanksIndex.js
@@ -149,6 +149,13 @@ class BanksIndex extends Component{
/>)
}
}>
+ {
+ return ()
+ }
+ }>
import('./PollBanksContent'),
loading: Loading,
})
+// 试卷详情
+const ExerciseBanksDetail = Loadable({
+ loader: () => import('./ExerciseBanksDetail'),
+ loading: Loading,
+});
class BanksTabIndex extends Component{
constructor(props){
@@ -53,7 +58,13 @@ class BanksTabIndex extends Component{
>
}
-
+ {
+ return ()
+ }
+ }>
{
+
+ }
+ detailFetchCallback = (result) => {
+ let Id=this.props.match.params.Id;
+
+ const crumbData={
+ title: result.data.exercise && result.data.exercise.name,
+ is_public: result.data.exercise && result.data.exercise.is_public,
+ crumbArray:[
+ {content:'详情'},
+ ]
+ }
+ const menuData={
+ tab:'0',//tab选中的index
+ menuArray:[//tab以及tab路由
+ {to:`/banks/exercise/${Id}`,content:'内容详情'}
+ ],
+ category:'exercise',//
+ tos: `/banks/exercise/${Id}/edit`,
+ id: Id,
+
+ }
+ this.props.initPublic(crumbData,menuData);
+ }
+
+ render(){
+ let { pollDetail } = this.state
+
+ return(
+
+
+
+
+ )
+ }
+}
+export default ExerciseBanksDetail
diff --git a/public/react/src/modules/user/usersInfo/banks/banksMenu.js b/public/react/src/modules/user/usersInfo/banks/banksMenu.js
index 0d7d1a24a..7680dc748 100644
--- a/public/react/src/modules/user/usersInfo/banks/banksMenu.js
+++ b/public/react/src/modules/user/usersInfo/banks/banksMenu.js
@@ -126,7 +126,7 @@ class BanksMenu extends Component{
this.deletecheckBoxValues(banksMenu&&banksMenu.id,banksMenu&&banksMenu.category)}style="blue" className="ml20 font-16">删除
- 编辑
+ 编辑
this.sendTopics()} style="blue" className="ml20 font-16">发送