Node開發文件上傳系統及向七牛云存儲和亞馬遜AWS S3的文件上傳

背景起,有奏樂:

有偉人曰:學習技能的最好途徑莫過于理論與實踐相結合。

初學Node這貨時,每每讀教程必會Fall asleep。

當真要開發系統時,頓覺精神百倍,即便踩坑無數也不失斗志。

因為同團隊的小伙伴們都在辛勤工作,正是因為他們的工作,

才讓我有足夠的時間拖著我疲軟的智商來研究Node和AWS這些貨。

系統完成,雖不盡完善,但不敢怠慢,迅速記錄,免遺忘。

為后續更新和開發做一參考。

這就是人生。只要努力,便美美噠。

?

標題略長,其實這系統要做的事只三件:

1. 從本地上傳文件到我們自己的服務器,并存儲。

2. 將文件上傳到七牛云存儲。

3. 將文件上傳到亞馬遜的AWS S3存儲。

?

幾處說明:

1. 用Node的好處是寫服務端代碼也不用糾結語法問題了:

系統的開發用Node完成。寫前后端都是JS,免去了語法的困擾。

不僅回憶起數日之前寫Scala時對語法的糾結和困惑,一身冷汗。

2. Plupload是個好東東:

Client端的File Select用Plupload完成。

有了Plupload這貨,再不糾結<input type='file'>的難看樣式的兼容問題不好把控了。

Plupload雖然對File做了封裝,但也提供了如?getNative 等的接口供我們訪問原生。

十分體貼。

3. AWS的Upload在前端完成:

真相只有一個:在Node服務端的AWS的Upload我還沒跑通……

請盡情的鄙視我吧T_T

好在路路通羅馬。我繞路從前端趕到了羅馬。

服務端請求的Block在這里:

從服務端向AWS上傳文件時,其文件的Body以流方式被分塊上傳。

測試后發現,上傳完成,也只傳了部分,導致文件無法正常訪問。

而在前端上傳時,直接用原生File對象即可實現上傳。

遂成功抵達羅馬。

關于在服務端的上傳問題,有待繼續研究。

學海無涯0_0

4. 七牛的上傳在服務端完成:

七牛的上傳也可以在前端完成,只不過七牛自己的JS-SDK包裹了Plupload。

由于我的上傳邏輯是由自己的Plupload來觸發七牛和亞馬遜(或其他第三方上傳),

因此不在前端再New一個Plupload來做七牛的上傳了。

New兩個同樣的東西實在是太二了好么。

設計的理念是,所有第三方上傳都必須在我們的服務器Trigger之后才發生。

就醬任性。

?

—————— 我是冬季里顫巍巍的分割線 ——————

?

主要邏輯和部分代碼:

?

1. 主程序和框架:

使用Express框架和Jade渲染引擎。

主程序app.js只做服務器的創建和監聽,

涉及業務邏輯的請求和處理,都寫在二級目錄(./routes)的模塊里。

app.js 的部分內容如下:

 3 var express = require('express'); 
4
var favicon = require('serve-favicon'); 5 var bodyParser = require('body-parser'); 6 var debug = require('debug')('express:server'); 7 var http = require('http'); 8 var port = normalizePort(process.env.PORT || '3038'); 9 var app = express(); 10 var server = http.createServer(app); 11 var index = require('./routes/index'); // 業務邏輯在這里 12 13 app.set('port', port); 14 server.on('error', serverOnError); 15 server.on('listening', serverOnListening); 16 server.on('connection', serverOnConnecting); 17 server.listen(port); 18 19 app.set('views', path.join(__dirname, 'views')); 20 app.set('view engine', 'jade'); 21 app.use(favicon(path.join(__dirname, 'public/lib', 'favicon.ico'))); 22 app.use(bodyParser.json()); 23 app.use(bodyParser.urlencoded({ extended: true })); 24 app.use(express.static(path.join(__dirname, 'public'))); 25 26 app.use('/', index);

1 /* ====================================================== */

2 module.exports = app;?

?

2. POST請求將文件上傳并存儲在本地服務器:

需要注意的是,這里的POST請求用到了中間件:

1 var multipart = require('connect-multiparty');
3 var multipartMiddleware = multipart();
5 var express = require('express');
7 var router = express.Router();
9 router.post( ‘/saveInLocalServer’, multipartMiddleware, function(req, res){ 。。。});

這個請求接收的是從前端的Plupload上傳的File,

神秘的中間件會在服務器生成臨時文件,但不會刪除它們。

因此在處理的最后要手動刪除臨時文件req.files。How to?

收到請求后,處理文件的部分代碼如下:

 1 var file = req.files.file;
 2 var tempPath = file.path,
 3     fileName = file.name,
 4     fileType = file.type,
 5     fileSize = file.size;
 6 var uploadDirName = dirName.DirName; // 生成目錄的模塊,每月一生
 7 var filenameWithMd5 = MD5( new Date().getTime() ) + '-' + fileName;
 8 var filenameForCloud = fileRename.FileRename(fileName);
 9 // 保存到本地服務器的文件,使用MD5重命名文件
10 // 上傳到云存儲的文件,使用自定義的模塊重命名
11 var targetPath = path.resolve('./' + uploadDirName + '/' + filenameWithMd5);
12 // Save file in our local server:
13 fs.rename(tempPath, targetPath, function(err, data){
14   if( err ){
15     var result = 'error';
16     res.status( result ).send();
17   } else {
18     var result = 'ok';
19     var uploadInfos = { ... }; // AWS的config信息定義在服務端,由模塊引入并發送到前端,供JS接口調用:
20     res.status( result ).send( uploadInfos );
21     // Next do Qi Niu Upload ... blah blah blah
22   }
23 });

針對上述代碼的幾處說明:

a:關于在本地服務器生成目錄:

我們的需求是,每月首次觸發上傳動作時,在服務器創建一只新目錄。

該月內的其余上傳文件,都存儲在這一目錄里。

所有的文件會按上傳時間,以自然月為目錄而分類。

按月創建目錄的邏輯,我寫了一枚小小模塊,如下:

var fs = require('fs');var _d = new Date();
var _year = _d.getFullYear();
var _month = (_d.getMonth() + 1 < 10)?('0' + (_d.getMonth() + 1)):(_d.getMonth() + 1); // 為整齊,月份都顯示為兩位數,因此1-9月前面加0
var dir = _year + '-' + _month + '-alex_upload';if (!fs.existsSync(dir)){fs.mkdirSync(dir);
}exports.DirName = dir; // 輸出模塊名為DirName// ============================
// 假設這個文件名為makeDirName.js,則在業務邏輯中引入并應用要這樣:
var d_name = require('../routes/makeDirName');
var someName = d_name.DirName; // 輸出的模塊名在這里被這樣引用

b:關于文件重命名:

我們的需求是,存在本地服務器的文件,使用MD5重命名。

上傳到云存儲的文件,使用時間戳和隨機字符串共同重命名。

重命名文件的模塊是醬紫寫的:

 1 function rename( filename ) { 
 2   var name = ''; 
 3   var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
 4   var length = 6; // 隨機字符串的長度,暫用6
 5   for(var i = 0; i < length; i++){
 6     name += possible.charAt( Math.floor(Math.random() * possible.length) );
 7   }
 8   var timestamp = new Date().getTime();
 9   name = timestamp + '-' + name + '-' + filename;
10   return name;
11 };
12 
13 exports.FileRename = rename;

c:關于res.status( 200 ).send( data ):

每個請求的response必須Call一下res.end(),

以此來告訴服務器這個請求的header和body都已發送,

并且這個請求已經完成。

如果不告訴服務器,呆萌的服務器是永遠不會知道的。

瀏覽器會一直在請求狀態中,標題欄的小圈圈一直在轉啊轉,

表示請求一直在持續啊持續。

在Call了res.end()之后,res.finished 的值為true,否則是false。

res.send() 會Call res.end(),因此不需重復Call。

?

3:在前端請求AWS S3

在發送剛才所提到的POST請求之前,

前端先new一個plupload的Uploader,部分代碼如下:

 1 var _myUploader = new plupload.Uploader({
 2   runtimes:           'html5,flash,silverlight,html4',
 3   file_data_name:     'file',
 4   container:          _SCOPE.containerId,
 5   browse_button:      _SCOPE.filePickerId,
 6   uptoken_url:        _ELE.fileUptoken.innerHTML,
 7   url:                _ELE.fileLocalSave.innerHTML,
 8   flash_swf_url:      _SCOPE.swfUrl,
 9   silverlight_xap_url:_SCOPE.xapUrl,
10   filters: {
11     max_file_size:    _SCOPE.maxFileSize,
12     mime_types: [
13       {title: 'Image files', extensions: 'jpg,png,gif'},
14       {title: 'Zip files', extensions: 'zip'}
15     ]   
16   }, 
17   init: {...}
18 });

這里的 _SCOPE 和 _ELE 定義在全局作用域,或指定頁面模塊作用域下。

目的是從服務端接收相關的配置參數,在頁面發送請求時調用。

這里遵循了一個高端大氣上檔次的寫碼原則,即:

常量參數的配置,

如Domain地址、取token之通信接口、

even 賬戶的accessKey&accessToken blah blah blah……

都在服務端某指定模塊內統一配置。

當前端需要某參數時,由頁面渲染res.render() 傳遞到頁面元素HTML屬性里,

但是不可以將Key等賬戶密鑰渲染在頁面結構里

也可以通過前后端通信將參數傳遞給前端頁面,

例如剛才所述的POST接口里的uploadInfos。

這樣做,在一處定義,其余皆調用

當值有更新時,只在定義處更新其值即可。

避免多處賦值,更新時丟三落四陷入混亂。

嗯咳,所有工程師都知道的好么!我說多了……

……繼續說上傳:

使用Plupload,在其FileUploaded 的回調里,

即可執行向AWS S3發送請求了。

FileUploaded是在Plupload的文件上傳成功后才會觸發。

前端請求AWS S3的簡要方法如下:

(這里的file是從FileUploaded的方法里用getNative獲取到的原生file對象)

 1 function doAWSUpload( rename, file, info ) { 
 2   var file_name = file.name,
 3       file_type = file.type,
 4       file_size = file.size;
 5   var bucket = new AWS.S3();
 6   var uniqueName = rename;
 7   bucket.config.update({ // 配置信息,在服務端傳來的info里
 8     accessKeyId: info.accessKeyId,
 9     secretAccessKey: info.secretAccessKey
10   }); 
11   bucket.config.region = info.region;
12   var params = { 
13     Bucket: info.bucket, // 賬戶指定的bucket名
14     Key: uniqueName,
15     ContentType: file_type,
16     Body: file,
ACL: 'public-read', // 設置文件訪問權限
17 ServerSideEncryption: info.ServerSideEncryption 18 }; 19 bucket.putObject(params, function(err, data){ // 此賬戶必須要有putObject的操作權限才能調用 20 if(err){ 21 var errText = ' ' + file_name + ' failed in uploading to AWS! ' + err; 22 _ELE.fileConsole.innerHTML += errText; 23 }else{ 24 var url = 'https://s3.amazonaws.com/' + info.bucket + '/' + uniqueName;26 _ELE.fileConsole.innerHTML += ' AWS upload succeeded! ' + url; 27 } 28 }).on('httpUploadProgress', function(progress){ 29 console.log( 'AWS uploading...', Math.round(progress.loaded / progress.total * 100) ); 30 }); 31 };

執行這個方法的前提是前端頁面調用了JS-SDK,

并且,……最重要的是并且:

對應賬戶在AWS的Console管理后臺的相關配置要正確。

最討厭各種相關配置了,

配來配去一百年才成功一次……

?

4:AWS的賬戶在Console管理后臺的相關配置

首先注冊一枚高大上的AWS賬戶。

如果你經常在Amazon上買買買,也可以用你的Retail賬戶。

開通AWS服務,需要驗證,其過程要填寫Payment賬戶信息。

我十分Naive的填了自己的Credit Card信息,結果直接被扣掉1刀勒。

嚇尿之后,立刻刪。

大約因為作為Retail賬戶時我曾做過快捷支付神馬的腦殘設置吧。

總之,1美元而已,這已不是重點……

有了一枚飄逸的AWS賬戶后,登錄?https://console.aws.amazon.com

選擇S3服務,進來后無視一切,先Create Bucket

點擊這個新的Bucket,選擇Properties

Permissions里,再選擇 “Edit CORS Configuration”,

一個較為典型的CORS Configuration可以長這個樣子:

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
 3     <CORSRule>
 4         <AllowedOrigin>http://localhost:3038</AllowedOrigin> //本地測試入口
 5         <AllowedOrigin>http://shaojing.wang</AllowedOrigin> //線上測試入口
 6         <AllowedMethod>PUT</AllowedMethod> //可執行的方法
 7         <AllowedMethod>DELETE</AllowedMethod> //可執行的方法
 8         <MaxAgeSeconds>3000</MaxAgeSeconds>
 9         <ExposeHeader>x-amz-server-side-encryption</ExposeHeader>
10         <ExposeHeader>x-amz-request-id</ExposeHeader>
11         <ExposeHeader>x-amz-id-2</ExposeHeader>
12         <AllowedHeader>*</AllowedHeader>
13     </CORSRule>
14 </CORSConfiguration>

這里的CORS Configuration即對跨域請求所做限制,

只有“AllowedOrigin”里指定的端口才能向AWS發出請求,

而只有“AllowedHeader”里指定的端口才能接收請求(訪問文件)。

上傳成功后,可通過這樣的URI訪問到文件:

https://s3.amazonaws.com/myBucketName/1452581386878-hPp8Mc-test.png

附:AWS的文檔在這里:http://docs.aws.amazon.com/

關于如何在服務端進行AWS S3的上傳,下次再寫文章分享。

下面該講什么了……

?

5:在服務端實現向七牛云存儲上傳文件

該七牛了。

請八牛、九牛和十牛再耐心等一等。

六牛你不要鬧,你已經謝世了好么。

從服務器向七牛云發送請求之前,需要獲取授權,

請求授權之前,需要設置賬戶信息。

設置賬戶信息之前,你得先有一枚賬戶。

有了賬戶就有了AccessKey & SecretKey。

還是剛才講的,在統一配置參數的模塊里,配置好這些Key們的信息,

然后在服務端將發送請求之前,做賦值:

1 var qiniu = require('qiniu');
2 var qnConf = require('../config/qiniu_config');
3 
4 /* Prepare Qiniu config, we make Qiniu upload in Node Server not in browser*/
5 qiniu.conf.ACCESS_KEY = qnConf.QiniuConfig.ACCESS_KEY;
6 qiniu.conf.SECRET_KEY = qnConf.QiniuConfig.SECRET_KEY;

賦值之后,就可以開心的去請求upToken了!

寫一只孤零零的單獨小模塊,用來生成upToken,代碼長這樣:

1 var qiniu = require('qiniu');
2 
3 function uptoken(bucketname) { // 指定一個bucket傳名字進來
4   var putPolicy = new qiniu.rs.PutPolicy(bucketname);
5   return putPolicy.token();
6 }
7 
8 exports.Uptoken = uptoken;

拿到upToken就可以華麗麗麗麗的開始上傳了。

可以在剛才本地存儲的POST請求成功后的回調里做。

代碼就像醬紫:

 1       // Do Qiniu upload in here:
 2       var targetPath = path.resolve('./' + uploadDirName + '/' + filenameWithMd5); //接剛才的POST里的處理
 3       var qiniu_uptoken = generateUptoken.Uptoken(qnConf.QiniuConfig.Bucket_Name);
 4       var extra = null; // 放額外信息,先寫null
 5       fs.readFile(targetPath, function(error, data){
 6         qiniu.io.put(qiniu_uptoken, uploadDirName + '/' + filenameForCloud, data, extra, function(err, ret){
 7           if(err){
 8             console.log('Something is wrong with Qiniu upload! ', err);
 9           }else{
10             console.log('qiniu: ', ret);
11             console.log('Qiniu URL = ', qnConf.QiniuConfig.Domain + uploadDirName + '/' + filenameForCloud); //手動拼結果URL
12           }
13         });
14       });

至此,七牛的上傳也OK鳥!

撒花~~樂隊起~~

?

5:后記

本文所述內容,僅限于最主要最基本的邏輯,

未涉及頁面的交互和部分異常響應的處理。

僅供參考。表扔雞蛋。

?

轉載于:https://www.cnblogs.com/alex1128/p/nodeToUpload.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/257824.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/257824.shtml
英文地址,請注明出處:http://en.pswp.cn/news/257824.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

計算機學業水平考試及格,信息技術學業水平考試表格部分試題(帶答案)

第三章表格信息的加工與表達復習學案【學習目標】1.熟練使用excel加工表格信息&#xff0c;理解用圖表來表現信息的特點與意義&#xff0c;2.能根據表格數據關系選擇合適的圖表類型表達意圖。【考點】1.表格中常用的函數及其求值方法&#xff1b;2.根據數據選擇合適的圖表類型&…

Ok6410掛載NFS

虛擬機&#xff1a; apt-get install portmap apt-get install nfs-kernel-server mkdir /nfs/root/mNFS chmod 777 /nfs chmod 777 /nfs/root vi /etc/exports 添加&#xff1a;/nfs/root *(rw,sync,no_root_squash) 開發板&#xff1a; mount -t nfs 192.168.0.12…

云計算:容器技術變革云計算,SaaS帶動CaaS市場

報告摘要&#xff1a; 1、容器技術增速驚人&#xff0c;市場認可度提高 虛擬化是云計算的重要基礎&#xff0c;Docker定義了一套容器從構建到執行的標準化體系&#xff0c;改變了傳統的虛擬化技術&#xff0c;深度影響了云計算領域。 隨著谷歌、亞馬遜、微軟等云計算廠商紛紛加…

Jan 12 - Delete Node in a Linked List; Data Structure; Linked List; Pointer;

代碼&#xff1a; /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode(int x) { val x; }* }*/ public class Solution {public void deleteNode(ListNode node) {if(node null) return;while(node.next ! …

三年級神奇電子計算機教案,人教版小學三年級下冊信息技術教案

人教版小學三年級下冊信息技術教案 人教版小學信息技術教案第一課 神奇的信息世界教學目的&#xff1a;通過學習使學生更充分地了解信息技術在生活中的應用。教學內容&#xff1a;觀看“神奇的信息世界”光碟教學準備&#xff1a;1、調試每臺計算機 2、打開計算機并由教師機控制…

spark 安裝配置

最佳參考鏈接 https://opensourceteam.gitbooks.io/bigdata/content/spark/install/spark-160-bin-hadoop26an_zhuang.html Apache Spark1.1.0部署與開發環境搭建   Spark是Apache公司推出的一種基于Hadoop Distributed File System(HDFS)的并行計算架構。與MapReduce不同&am…

《大數據原理:復雜信息的準備、共享和分析》一一2.5 在標識符中嵌入信息:不推薦...

2.5 在標識符中嵌入信息&#xff1a;不推薦大多數標識符不是純粹的隨機數&#xff0c;它們通常含有一些可由熟悉標識系統的人解釋的嵌入信息。例如&#xff0c;標識符中可以嵌入姓的前三個字母&#xff0c;同樣&#xff0c;標識符中也可以嵌入出生年份的最后兩位數字。標識符中…

python基礎知識-列表,元組,字典

列表&#xff08;list&#xff09; 賦值方法&#xff1a; l [11,45,67,34,89,23] l list() 列表的方法&#xff1a; 1 #!/usr/bin/env python2 3 class list(object):4 """5 list() -> new empty list6 list(iterable) -> new list initial…

車站計算機聯鎖系統的仿真設計,車站計算機聯鎖仿真設計.doc

車站計算機聯鎖仿真設計2012 屆 交通運輸 學院專 業學 號 2008學生姓名指導教師完成日期 2012年 月日計算機聯鎖是保證車站內列車和調車作業安全&#xff0c;提高車站通過能力的一種信號設備。設計以沙盤模型為根據&#xff0c;練習制作聯鎖信號圖表&#xff0c;使用Visual Bas…

如何解決機器學習中的數據不平衡問題?

在機器學習任務中&#xff0c;我們經常會遇到這種困擾&#xff1a;數據不平衡問題。 數據不平衡問題主要存在于有監督機器學習任務中。當遇到不平衡數據時&#xff0c;以總體分類準確率為學習目標的傳統分類算法會過多地關注多數類&#xff0c;從而使得少數類樣本的分類性能下降…

ubuntu每次登陸都用root賬號登陸

sudo -s 進入 root 用戶權限模式 vi /etc/lightdm/lightdm.conf [SeatDefaults] greeter-sessionunity-greeter user-sessionUbuntu greeter-show-manual-logintrue allow-guestfasle 重啟后再登陸就會 直接用root登陸了 版權聲明&#xff1a;本文為博主原創文章&#xff0c;未…

js-BOM

私有變量&#xff1a; 1、在一個實例上調用setName&#xff08;&#xff09;會影響所有的實例 BOM&#xff1a; 1、全局變量不能通過delete操作符刪除&#xff0c;而直接在window對象上定義的屬性可以 2、嘗試訪問為聲明的變量會拋出錯誤&#xff0c;但通過查詢window對象&…

計算機組成實驗v代表什么,2014計算機組成原理實驗指導V1.3.docx

文檔介紹&#xff1a;實驗一運算器組成實驗實驗目的熟悉Logisim軟件平臺。掌握運算器基本工作原理掌握運算溢出檢測的原理和實現方法;理解有符號數和無符號數運算的區別;理解基于補碼的加/減運算實現原理;熟悉運算器的數據傳輸通路。實驗環境Logisim是一款數字電路模擬的教育軟…

四大技巧輕松搞定云容器

云容器技術&#xff0c;作為傳統虛擬化管理程序的一種替代品&#xff0c;正稱霸著云市場。容器是輕量級的&#xff0c;并提供增強的便攜性&#xff0c;允許應用在平臺之間遷移&#xff0c;而不需要開發者重做或重新架構應用。但是&#xff0c;盡管其好處讓開發人員感到驚嘆&…

Android 圖文混排 通過webview實現并實現點擊圖片

在一個開源項目看到是用的webview 實現的 1. 這是在asset中的一個模板html <html> <head> <title>News Detail</title> <meta name"viewport" content"widthdevice-width, minimum-scale0.5, initial-scale1.2, maximum-scale2.0…

h5engine造輪子

基于學習的造輪子&#xff0c;這是一個最簡單&#xff0c;最基礎的一個canvas渲染引擎&#xff0c;通過這個引擎架構&#xff0c;可以很快的學習canvas渲染模式&#xff01; 地址&#xff1a;https://github.com/RichLiu1023/h5engine 這是一個比較有意思的h5渲染引擎&#xff…

計算機硬件選型報價,組裝電腦硬件該怎么選擇?這幾個硬件要舍得花錢,千萬別買錯了!...

原標題&#xff1a;組裝電腦硬件該怎么選擇&#xff1f;這幾個硬件要舍得花錢&#xff0c;千萬別買錯了&#xff01;組裝電腦是多硬件組合的產物&#xff0c;每一個硬件對于電腦的性能都是有影響的&#xff0c;影響的大小與電腦的硬件有直接關系&#xff0c;有些硬件就要舍得花…

2017 省賽選撥 想打架嗎?算我一個!所有人,都過來!(3) 遞推 斐波拉數列的應用...

想打架嗎&#xff1f;算我一個&#xff01;所有人&#xff0c;都過來&#xff01;(3) Submit Page Summary Time Limit: 2 Sec Memory Limit: 128 Mb Submitted: 28 Solved: 9 Description 現在《爐石傳說》這款卡牌游戲已經風靡全球。2015年加入環境的“…

UITableViewCell中cell重用機制導致內容重復的方法

UITableView繼承自UIScrollview,是蘋果為我們封裝好的一個基于scroll的控件。上面主要是一個個的UITableViewCell,可以讓UITableViewCell響應一些點擊事件&#xff0c;也可以在UITableViewCell中加入UITextField或者UITextView等子視圖&#xff0c;使得可以在cell上進行文字編輯…

高級會計師計算機考試中級,會計師需要計算機等級考試嗎

塵伴考證達人06-19TA獲得超過671個贊[color#000][font宋體][size3][alignleft]廣東省高級會計師評審職稱外語&#xff0c;執行《關于調整完善我省職稱外語政策的通知》(粵人發〔2018〕120號)[/align][alignleft]三、報考職稱外語考試的等級要求[b][size3](一)申報高教、科研、衛…