分類
發燒車訊

.Net Core微服務入門全紀錄(三)——Consul-服務註冊與發現(下)

前言

上一篇【.Net Core微服務入門全紀錄(二)——Consul-服務註冊與發現(上)】已經成功將我們的服務註冊到Consul中,接下來就該客戶端通過Consul去做服務發現了。

服務發現

  • 同樣Nuget安裝一下Consul:

  • 改造一下業務系統的代碼:

ServiceHelper.cs:

    public class ServiceHelper : IServiceHelper
    {
        private readonly IConfiguration _configuration;

        public ServiceHelper(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        public async Task<string> GetOrder()
        {
            //string[] serviceUrls = { "http://localhost:9060", "http://localhost:9061", "http://localhost:9062" };//訂單服務的地址,可以放在配置文件或者數據庫等等...

            var consulClient = new ConsulClient(c =>
            {
                //consul地址
                c.Address = new Uri(_configuration["ConsulSetting:ConsulAddress"]);
            });

            //consulClient.Catalog.Services().Result.Response;
            //consulClient.Agent.Services().Result.Response;
            var services = consulClient.Health.Service("OrderService", null, true, null).Result.Response;//健康的服務

            string[] serviceUrls = services.Select(p => $"http://{p.Service.Address + ":" + p.Service.Port}").ToArray();//訂單服務地址列表

            if (!serviceUrls.Any())
            {
                return await Task.FromResult("【訂單服務】服務列表為空");
            }

            //每次隨機訪問一個服務實例
            var Client = new RestClient(serviceUrls[new Random().Next(0, serviceUrls.Length)]);
            var request = new RestRequest("/orders", Method.GET);

            var response = await Client.ExecuteAsync(request);
            return response.Content;
        }

        public async Task<string> GetProduct()
        {
            //string[] serviceUrls = { "http://localhost:9050", "http://localhost:9051", "http://localhost:9052" };//產品服務的地址,可以放在配置文件或者數據庫等等...

            var consulClient = new ConsulClient(c =>
            {
                //consul地址
                c.Address = new Uri(_configuration["ConsulSetting:ConsulAddress"]);
            });

            //consulClient.Catalog.Services().Result.Response;
            //consulClient.Agent.Services().Result.Response;
            var services = consulClient.Health.Service("ProductService", null, true, null).Result.Response;//健康的服務

            string[] serviceUrls = services.Select(p => $"http://{p.Service.Address + ":" + p.Service.Port}").ToArray();//產品服務地址列表

            if (!serviceUrls.Any())
            {
                return await Task.FromResult("【產品服務】服務列表為空");
            }

            //每次隨機訪問一個服務實例
            var Client = new RestClient(serviceUrls[new Random().Next(0, serviceUrls.Length)]);
            var request = new RestRequest("/products", Method.GET);

            var response = await Client.ExecuteAsync(request);
            return response.Content;
        }
    }

appsettings.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConsulSetting": {
    "ConsulAddress": "http://localhost:8500"
  }
}

OK,以上代碼就完成了服務列表的獲取。

瀏覽器測試一下:

隨便停止2個服務:

繼續訪問:

這時候停止的服務地址就獲取不到了,客戶端依然正常運行。

這時候解決了服務的發現,新的問題又來了…

  • 客戶端每次要調用服務,都先去Consul獲取一下地址,這不僅浪費資源,還增加了請求的響應時間,這顯然讓人無法接受。

那麼怎麼保證不要每次請求都去Consul獲取地址,同時又要拿到可用的地址列表呢?
Consul提供的解決方案:——Blocking Queries (阻塞的請求)。詳情請見官網:https://www.consul.io/api-docs/features/blocking

Blocking Queries

這是什麼意思呢,簡單來說就是當客戶端請求Consul獲取地址列表時,需要攜帶一個版本號信息,Consul會比較這個客戶端版本號是否和Consul服務端的版本號一致,如果一致,則Consul會阻塞這個請求,直到Consul中的服務列表發生變化,或者到達阻塞時間上限;如果版本號不一致,則立即返回。這個阻塞時間默認是5分鐘,支持自定義。
那麼我們另外啟動一個線程去干這件事情,就不會影響每次的用戶請求了。這樣既保證了客戶端服務列表的準確性,又節約了客戶端請求服務列表的次數。

  • 繼續改造代碼:
    IServiceHelper增加一個獲取服務列表的接口方法:
    public interface IServiceHelper
    {
        /// <summary>
        /// 獲取產品數據
        /// </summary>
        /// <returns></returns>
        Task<string> GetProduct();

        /// <summary>
        /// 獲取訂單數據
        /// </summary>
        /// <returns></returns>
        Task<string> GetOrder();

        /// <summary>
        /// 獲取服務列表
        /// </summary>
        void GetServices();
    }

ServiceHelper實現接口:

    public class ServiceHelper : IServiceHelper
    {
        private readonly IConfiguration _configuration;
        private readonly ConsulClient _consulClient;
        private ConcurrentBag<string> _orderServiceUrls;
        private ConcurrentBag<string> _productServiceUrls;

        public ServiceHelper(IConfiguration configuration)
        {
            _configuration = configuration;
            _consulClient = new ConsulClient(c =>
            {
                //consul地址
                c.Address = new Uri(_configuration["ConsulSetting:ConsulAddress"]);
            });
        }

        public async Task<string> GetOrder()
        {
            if (_productServiceUrls == null)
                return await Task.FromResult("【訂單服務】正在初始化服務列表...");

            //每次隨機訪問一個服務實例
            var Client = new RestClient(_orderServiceUrls.ElementAt(new Random().Next(0, _orderServiceUrls.Count())));
            var request = new RestRequest("/orders", Method.GET);

            var response = await Client.ExecuteAsync(request);
            return response.Content;
        }

        public async Task<string> GetProduct()
        {
            if(_productServiceUrls == null)
                return await Task.FromResult("【產品服務】正在初始化服務列表...");

            //每次隨機訪問一個服務實例
            var Client = new RestClient(_productServiceUrls.ElementAt(new Random().Next(0, _productServiceUrls.Count())));
            var request = new RestRequest("/products", Method.GET);

            var response = await Client.ExecuteAsync(request);
            return response.Content;
        }

        public void GetServices()
        {
            var serviceNames = new string[] { "OrderService", "ProductService" };
            Array.ForEach(serviceNames, p =>
            {
                Task.Run(() =>
                {
                    //WaitTime默認為5分鐘
                    var queryOptions = new QueryOptions { WaitTime = TimeSpan.FromMinutes(10) };
                    while (true)
                    {
                        GetServices(queryOptions, p);
                    }
                });
            });
        }
        private void GetServices(QueryOptions queryOptions, string serviceName)
        {
            var res = _consulClient.Health.Service(serviceName, null, true, queryOptions).Result;
            
            //控制台打印一下獲取服務列表的響應時間等信息
            Console.WriteLine($"{DateTime.Now}獲取{serviceName}:queryOptions.WaitIndex:{queryOptions.WaitIndex}  LastIndex:{res.LastIndex}");

            //版本號不一致 說明服務列表發生了變化
            if (queryOptions.WaitIndex != res.LastIndex)
            {
                queryOptions.WaitIndex = res.LastIndex;

                //服務地址列表
                var serviceUrls = res.Response.Select(p => $"http://{p.Service.Address + ":" + p.Service.Port}").ToArray();

                if (serviceName == "OrderService")
                    _orderServiceUrls = new ConcurrentBag<string>(serviceUrls);
                else if (serviceName == "ProductService")
                    _productServiceUrls = new ConcurrentBag<string>(serviceUrls);
            }
        }
    }

Startup的Configure方法中調用一下獲取服務列表:

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceHelper serviceHelper)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });

            //程序啟動時 獲取服務列表
            serviceHelper.GetServices();
        }

代碼完成,運行測試:

現在不用每次先請求服務列表了,是不是流暢多了?

看一下控制台打印:

這時候如果服務列表沒有發生變化的話,獲取服務列表的請求會一直阻塞到我們設置的10分鐘。

隨便停止2個服務:

這時候可以看到,數據被立馬返回了。

繼續訪問客戶端網站,同樣流暢。
(gif圖傳的有點問題。。。)

至此,我們就通過Consul完成了服務的註冊與發現。
接下來又引發新的思考。。。

  1. 每個客戶端系統都去維護這一堆服務地址,合理嗎?
  2. 服務的ip端口直接暴露給所有客戶端,安全嗎?
  3. 這種模式下怎麼做到客戶端的統一管理呢?

代碼放在:https://github.com/xiajingren/NetCoreMicroserviceDemo

未完待續…

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

分類
發燒車訊

加州野火每5秒燒1英畝 吞噬酒莊數萬人撤離

摘錄自2020年9月28日中央社報導

美國加州野火在強風助長下,每5秒鐘延燒約1英畝的土地,蔓延到世界知名的葡萄酒之鄉,納帕(Napa)與索諾馬(Sonoma)山谷今天有數以萬計的民眾被迫逃離家園。

根據美國國家海洋暨大氣總署(NOAA)衛星影像,昨天清晨約4時從納帕山谷爆發的「玻璃之火」(Glass Fire),昨晚延燒了2500英畝的土地,到了今早擴大到1萬1000英畝,相當於每5秒鐘燒掉約1英畝(約0.4公頃)。

法新社報導,加州森林防火廳(Cal Fire)說,加州野火把天空染成橘紅色,在悶熱的熱浪侵襲之下,火勢以「危險的速度」蔓延,且沒有一處獲得控制,沿途燒毀數座葡萄園與建築物。

官員說,當局已下令近3萬4000名居民疏散,並要求約1萬4000人準備立即撤離,因為「迅速蔓延的火勢」延燒到乾燥的植被以及難以進入的山區。

氣候變遷
國際新聞
美國
加州
野火
森林野火

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

分類
發燒車訊

分析ThreadLocal的弱引用與內存泄漏問題-Java8,利用線性探測法解決hash衝突

目錄

一.介紹

二.問題提出

  2.1內存原理圖

  2.2幾個問題

三.回答問題

  3.1為什麼會出現內存泄漏

  3.2若Entry使用弱引用

  3.3弱引用配合自動回收

四.總結  

 

 

 

一.介紹

  之前使用ThreadLocal的時候,就聽過ThreadLocal怎麼怎麼的可能會出現內存泄漏,具體原因也沒去深究,就是一種不清不楚的狀態。最近在看JDK的源碼,其中就包含ThreadLocal,在對ThreadLocal的使用場景介紹以及源碼的分析后,對於ThreadLocal中可能存在的內存泄漏問題也搞清楚了,所以這裏專門寫一篇博客分析一下。

  在分析內存泄漏之前,先了解2個概念,就是內存泄漏和內存溢出:

  內存溢出(memory overflow):是指不能申請到足夠的內存進行使用,就會發生內存溢出,比如出現的OOM(Out Of Memory)

  內存泄漏(memory lack):內存泄露是指在程序中已經動態分配的堆內存由於某種原因未釋放或者無法釋放(已經沒有用處了,但是沒有釋放),造成系統內存的浪費,這種現象叫“內存泄露”。

  當內存泄露到達一定規模后,造成系統能申請的內存較少,甚至無法申請內存,最終導致內存溢出,所以內存泄露是導致內存溢出的一個原因。

 

二.問題提出

2.1內存原理圖

  下圖是程序運行中的內存分布圖,簡要介紹一下這種圖:當前線程有一個threadLocals屬性(ThreadLocalMap屬性),該map的底層是數組,每個數組元素時Entry類型,Entry類型的key是ThreadLocal類型(也就是創建的ThreadLocal對象),而value是則是ThreadLocal.set()方法設置的value。

  

  需要注意的是ThreadLocalMap的Entry,繼承自弱引用,定義如下,關於Java的引用介紹,可以參考:Java-強引用、軟引用、弱引用、虛引用

/**
 * ThreadLocalMap中存放的元素類型,繼承了弱引用類
 */
static class Entry extends WeakReference<ThreadLocal<?>> {
    // key對應的value,注意key是ThreadLocal類型
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

 

2.2問題提出

  在看了上面ThreadLocal和ThreadLocalMap相關的內存分佈以及關聯后,提出這樣幾個問題:

  1.ThreadLocal為什麼會出現內存溢出?

  2.Entry的key為什麼要用弱引用?

  3.使用弱引用是否就能解決內存溢出?

  為了回答上面這3個問題,我寫了一段代碼,後面根據這段代碼進行分析:

public void step1() {
    // some action
    
    step2();
    step3();
    
    // other action
}

// 在stepX中都會創建threadLocal對象
public void step2() {
    ThreadLocal<String> tl = new ThreadLocal<>();
    tl.set("this is value");
}
public void step3() {
    ThreadLocal<Integer> tl = new ThreadLocal<>();
    tl.set(99);
}

  在step1中會調用step2和step3,step2和step3都會創建ThreadLocal對象,當step2和step3執行完畢后,其中的棧內存中ThreadLocal引用就會被清除。

 

三.回答問題

 

  

  現在針對這個圖,一步一步的分析問題,中途會得出一些臨時的結論,但是最終的結論才是正確的

 

3.1為什麼會出現內存泄露

  現在有2點假設,本小節的分析都是基於這兩個假設之上的:

  1.Entry的key使用強引用,key對ThreadLocal對象使用強引用,也就是上面圖中連線5是強引用(key強引用ThreadLocal對象);

  2.ThreadLocalMap中不會對過期的Entry進行清理。

  上面代碼中,如果ThreadLocalMap的key使用強引用,那麼即使棧內存的ThreadLocal引用被清除,但是堆中的ThreadLocal對象卻並不會被清除,這是因為ThreadLocalMap中Entry的key對ThreadLocal對象是強引用。

  如果當前線程不結束,那麼堆中的ThreadLocal對象將會一直存在,對應的內存就不會被回收,與之關聯的Entry也不會被回收(Entry對應的value也不會被回收),當這種情況出現數量比較多的時候,未釋放的內存就會上升,就可能出現內存泄漏的問題。

  上面的結論是暫時的,有前提假設!!!最終結論還需要看後面分析。

 

3.2若Entry使用弱引用

  

  仍舊有1個假設,就是ThreadLocalMap中不會對過期的Entry進行清理,陳舊的Entry是指Entry的key為null。

  按照源碼,Entry繼承弱引用,其Key對ThreadLocal是弱引用,也就是上圖中連線5是弱引用,連線6仍為強引用。

  同樣以上面代碼為例,step2和step3創建了ThreadLocal對象,step2和step3執行完后,棧中的ThreadLocal引用被清除了;由於堆內存中ThreadLocalMap的Entry key弱引用ThreadLocal對象,根據垃圾收集器對弱引用對象的處理:

當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。

  此時堆中ThreadLocal對象會被gc回收(因為現在沒有對ThreadLocal的強引用,只有一個弱引用ThreadLocal對象),Entry的key為null,但是value不為null,且value也是強引用(連線6),所以Entry仍舊不能回收,只能釋放ThreadLocal的內存,仍舊可能導致內存泄漏

  在沒有自動清理陳舊Entry的前提下,即使Entry使用弱引用,仍可能出現內存泄漏。

 

3.3弱引用配合自動回收

  通過3.2的分析,其實只要陳舊的Entry能自動被回收,就能解決內存泄漏的問題,其實JDK就是這麼做的。

  如果看過源碼,就知道,ThreadLocalMap底層使用數組來保存元素,使用“線性探測法”來解決hash衝突,關於線性探測法的介紹可以查看:利用線性探測法解決hash衝突

  在每次調用ThreadLocal類的get、set、remove這些方法的時候,內部其實都是對ThreadLocalMap進行操作,對應ThreadLocalMap的get、set、remove操作。

  重點來了!重點來了!重點來了!

  ThreadLocalMap的每次get、set、remove,都會清理過期的Entry,下面以get操作解釋,其他操作也是一個意思,大致如下:

  1.ThreadLocalMap底層用數組保存元素,當get一個Entry時,根據key的hash值(非hashCode)計算出該Entry應該出在什麼位置;

  2.計算出的位置可能會有衝突,比如預期位置是position=5,但是position=5的位置已經有其他Entry了;

  3.出現衝突后,會使用線性探測法,找position=6位置上的Entry是否匹配(匹配是指hash相同),如果匹配,則返回position=6的Entry。

  4.在這個過程中,如果position=5位置上的Entry已經是陳舊的Entry(Entry的key為null),此時position=5的key就應該被清理;

  5.光清理position=5的Entry還不夠,為了保證線性探測法的規則,需要判斷數組中的其他元素是否需要調整位置(如果需要,則調整位置),在這個過程中,也會進行清理陳舊Entry的操作。

  上面這5個步驟就保證了每次get都會清理數組中(map)的陳舊Entry,清理一個陳舊的Entry,就是下面這三行代碼:

Entry.value = null; // 將Entry的value設為null
table[index] = null;// 將數組中該Entry的位置設置null
size--;	// map的size減一

  對於ThreadLocal的set、remove也類似這個原理。

  有了自動回收陳舊Entry的操作,需要注意的是,在這個時候,key使用弱引用就是至關重要的一點!!!

  因為key使用弱引用后,當弱引用的ThreadLocal對象被會回收后,該key的引用為null,則該Entry在下一次get、set、remove的時候就才會被清理,從未避免內存泄漏的問題。

  

四.總結

  在上面的分析中,看到ThreadLocal基本不會出現內存泄漏的問題了,因為ThreadLocalMap中會在get、set、remove的時候清理陳舊的Entry,與Entry的key使用弱引用密不可分。

  當然我們也可以在代碼中手動調用ThreadLocal的remove方法進行清除map中key為該threadLocal對象的Entry,同時清理過期的Entry。

  

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

分類
發燒車訊

.Net 對於PDF生成以及各種轉換的操作

前段時間公司的產品,要做一個新功能,簽章(就是把需要的數據整理成PDF很標準的文件,然後在蓋上我們在服務器上面的章)

然後我就在百度上找了找,發現搞PDF的類庫很少,要麼就要錢,要麼就有水印,破解版的沒找到。

先講一講我是怎麼生成PDF的

1、生成PDF

  這裏用到了 Spire.Pdf    這個類庫可以在NuGet裏面搜索到,上面帶個小紅標的就是免費版本。  

  當然也可以去他們的官網,上面還有文檔(https://www.e-iceblue.cn/Introduce/Spire-PDF-NET.html)。

  代碼(這是我自己寫的一個測試的表格)

  

        public static void abc()
        {
            //創建PDF文檔
            Spire.Pdf.PdfDocument doc = new Spire.Pdf.PdfDocument();

            //添加一頁
            PdfPageBase page = doc.Pages.Add();
    //設置字體
            PdfTrueTypeFont font = new PdfTrueTypeFont(new System.Drawing.Font("Microsoft Yahei", 20f), true);
            PdfTrueTypeFont font1 = new PdfTrueTypeFont(new System.Drawing.Font("Microsoft Yahei", 11f), true);

            //創建一個PdfGrid對象
            PdfGrid grid = new PdfGrid();

        //這一段的內容是在表格只玩显示一些數據 根據坐標定位 第一個是內容,第二個是字體,第三個是顏色,第四第五是坐標
            page.Canvas.DrawString("XXXXXXXX管理中心回單",
                    font,
                    new PdfSolidBrush(System.Drawing.Color.Black), 130, 10);
            page.Canvas.DrawString("編號:31231",
                    font1,
                    new PdfSolidBrush(System.Drawing.Color.Black), 380, 60);
            page.Canvas.DrawString("經辦人:XXXX",
                    font1,
                    new PdfSolidBrush(System.Drawing.Color.Black), 60, 250);
            page.Canvas.DrawString("打印日期:2020/06/15",
                    font1,
                    new PdfSolidBrush(System.Drawing.Color.Black), 380, 250);
            //設置單元格邊距
            grid.Style.CellPadding = new PdfPaddings(1, 1, 4, 4);

            //設置表格默認字體
            grid.Style.Font = new PdfTrueTypeFont(new System.Drawing.Font("Microsoft Yahei", 12f), true);

            //添加4行4列
            PdfGridRow row1 = grid.Rows.Add();
            PdfGridRow row2 = grid.Rows.Add();
            PdfGridRow row3 = grid.Rows.Add();
            PdfGridRow row4 = grid.Rows.Add();
            PdfGridRow row5 = grid.Rows.Add();
            PdfGridRow row6 = grid.Rows.Add();
            grid.Columns.Add(4);

            //設置列寬
            foreach (PdfGridColumn col in grid.Columns)
            {
                col.Width = 120f;
            }

            //寫入數據 第一行第一個格式的值,第一行第二個格子的值 
            row1.Cells[0].Value = "收款單位";
            row1.Cells[1].Value = "{DW}";
            row2.Cells[0].Value = "收款單位";
            row2.Cells[1].Value = "{DW}";
            row3.Cells[0].Value = "匯款時間";
            row3.Cells[1].Value = "2016/06/02";
            row3.Cells[2].Value = "金額小寫";
            row3.Cells[3].Value = "¥231";
            row4.Cells[0].Value = "金額合計大寫";
            row4.Cells[1].Value = "大蘇打實打實";
            row5.Cells[0].Value = "用途:" +
                "付XXXX2020年04月至2020年04月";
            row6.Cells[0].Value = "提示:回單可重複打印,請勿重複XXX";

            //row5.Cells[0].Height = (float)20;
            //水平和垂直合併單元格 從那個格子開始合併幾個(包含當前格子)
            row1.Cells[1].ColumnSpan = 3;
            row2.Cells[1].ColumnSpan = 3;
            row4.Cells[1].ColumnSpan = 3;
            row5.Cells[0].ColumnSpan = 2;
            row5.Cells[2].ColumnSpan = 2;
            row6.Cells[0].ColumnSpan = 2;
            row6.Cells[2].ColumnSpan = 2;
        //這個是垂直合併,但是我之前合併的沒有效果
            row5.Cells[1].RowSpan = 2;

            //設置單元格內文字對齊方式
            row1.Cells[0].StringFormat = new PdfStringFormat(PdfTextAlignment.Center);
            row1.Cells[1].StringFormat = new PdfStringFormat(PdfTextAlignment.Center);
            row2.Cells[0].StringFormat = new PdfStringFormat(PdfTextAlignment.Center);
            row2.Cells[1].StringFormat = new PdfStringFormat(PdfTextAlignment.Center);
            row3.Cells[0].StringFormat = new PdfStringFormat(PdfTextAlignment.Center);
            row3.Cells[1].StringFormat = new PdfStringFormat(PdfTextAlignment.Center);
            row3.Cells[2].StringFormat = new PdfStringFormat(PdfTextAlignment.Center);
            row3.Cells[3].StringFormat = new PdfStringFormat(PdfTextAlignment.Center);
            row4.Cells[0].StringFormat = new PdfStringFormat(PdfTextAlignment.Center);
            row4.Cells[1].StringFormat = new PdfStringFormat(PdfTextAlignment.Center);
            row5.Cells[0].StringFormat = new PdfStringFormat(PdfTextAlignment.Center);
            row6.Cells[0].StringFormat = new PdfStringFormat(PdfTextAlignment.Center);
            //設置單元格背景顏色
            //row1.Cells[0].Style.BackgroundBrush = PdfBrushes.Gray;
            //row3.Cells[3].Style.BackgroundBrush = PdfBrushes.Green;
            //row4.Cells[3].Style.BackgroundBrush = PdfBrushes.MediumVioletRed;

            //設置邊框顏色、粗細
            PdfBorders borders = new PdfBorders();
            borders.All = new PdfPen(System.Drawing.Color.Black, 0.1f);
            foreach (PdfGridRow pgr in grid.Rows)
            {
                foreach (PdfGridCell pgc in pgr.Cells)
                {
                    pgc.Style.Borders = borders;
                }
            }
            //保存到文檔
            //在指定為繪入表格
            grid.Draw(page, new PointF(30, 80));
            doc.SaveToFile(@"路徑");
       //這句是在瀏覽器重打開這個PDF  
            System.Diagnostics.Process.Start(@"路徑");
        }

 

  保存我們看一下

  

 

 

2、之後就是關於PDF一些轉換的操作了(PDF轉base64,轉圖片之類的,我把代碼貼到下面)

 

 

 

  • 這個用到了iTextSharp類庫,也可以在Nuget下載到

 

/// <summary>
        /// 圖片轉pdf
        /// </summary>
        /// <param name="jpgfile"></param>
        /// <param name="pdf"></param>
        public static bool ConvertJPG2PDF(string jpgfile, string pdf)
        {
            try
            {
                var document = new iTextSharp.text.Document(iTextSharp.text.PageSize.A4, 25, 25, 25, 25);
                using (var stream = new FileStream(pdf, FileMode.Create, FileAccess.Write, FileShare.None))
                {
                    PdfWriter.GetInstance(document, stream);
                    document.Open();
                    using (var imageStream = new FileStream(jpgfile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                    {
                        var image = iTextSharp.text.Image.GetInstance(imageStream);
                        if (image.Height > iTextSharp.text.PageSize.A4.Height - 25)
                        {
                            image.ScaleToFit(iTextSharp.text.PageSize.A4.Width - 25, iTextSharp.text.PageSize.A4.Height - 25);
                        }
                        else if (image.Width > iTextSharp.text.PageSize.A4.Width - 25)
                        {
                            image.ScaleToFit(iTextSharp.text.PageSize.A4.Width - 25, iTextSharp.text.PageSize.A4.Height - 25);
                        }
                        image.Alignment = iTextSharp.text.Image.ALIGN_MIDDLE;
                        document.NewPage();
                        document.Add(image);
                    }
                    document.Close();
                }

                return true;

            }
            catch (Exception ex)
            {
                throw ex;
            }
        }
  • 這個用的是(O2S.Components.PDFRender4NET)
/// <summary>
        /// pdf轉img
        /// </summary>
        /// <param name="path">pdf位置</param>
        /// <param name="path2">img位置</param>
        public static void Pdf2Img(string path, string path2)
        {
            PDFFile pdfFile = PDFFile.Open(path);
            //實例化一個PdfDocument類對象,並加載PDF文檔
            using (Spire.Pdf.PdfDocument doc = new Spire.Pdf.PdfDocument())
            {
                doc.LoadFromFile(path);
                //調用方法SaveAsImage()將PDF第一頁保存為Bmp格式
                System.Drawing.Image bmp = doc.SaveAsImage(0);
                // //調用另一個SaveAsImage()方法,並將指定頁面保存保存為Emf、Png            
                System.Drawing.Image emf = doc.SaveAsImage(0, Spire.Pdf.Graphics.PdfImageType.Bitmap);
                System.Drawing.Image zoomImg = new Bitmap((int)(emf.Size.Width * 2), (int)(emf.Size.Height * 2));
                using (Graphics g = Graphics.FromImage(zoomImg))
                {
                    g.ScaleTransform(2.0f, 2.0f);
                    g.DrawImage(emf, new System.Drawing.Rectangle(new System.Drawing.Point(0, 0), emf.Size), new System.Drawing.Rectangle(new System.Drawing.Point(0, 0), emf.Size), GraphicsUnit.Pixel);
                    zoomImg.Save(path2, ImageFormat.Jpeg);
                    
                    zoomImg.Dispose();
                    emf.Dispose();
                    bmp.Dispose();
                }
                doc.Close();
                doc.Dispose();
            }
            
           
        }
  • 這個和上面用的也是同一個(我覺得這個比較好用一些)

 

/// <summary>
                /// 將PDF文檔轉換為圖片的方法
                /// </summary>
                /// <param name="pdfInputPath">PDF文件路徑</param>
                /// <param name="imageOutputPath">圖片輸出路徑</param>
                /// <param name="imageName">生成圖片的名字</param>
                /// <param name="startPageNum">從PDF文檔的第幾頁開始轉換</param>
                /// <param name="endPageNum">從PDF文檔的第幾頁開始停止轉換</param>
                /// <param name="imageFormat">設置所需圖片格式</param>
                /// <param name="definition">設置圖片的清晰度,数字越大越清晰</param>
        public static void ConvertPDF2Image(string pdfInputPath, string imageOutputPath,
      string imageName, int startPageNum, int endPageNum, ImageFormat imageFormat, Definition definition)
        {
            PDFFile pdfFile = null;
            FileStream fs = null;
            try
            {
                fs = new FileStream(pdfInputPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);
                pdfFile = PDFFile.Open(fs);

                if (startPageNum <= 0)
                {
                    startPageNum = 1;
                }
                if (endPageNum > pdfFile.PageCount)
                {
                    endPageNum = pdfFile.PageCount;
                }
                if (startPageNum > endPageNum)
                {
                    int tempPageNum = startPageNum;
                    startPageNum = endPageNum;
                    endPageNum = startPageNum;
                }
                string path = imageOutputPath + imageName + "1" + "." + imageFormat.ToString();
                Logger.WriteLogs("PDFIMG:" + path);
                using (Bitmap pageImage = pdfFile.GetPageImage(0, 56 * (int)definition))
                {
                    if (!File.Exists(path))
                    {
                        using (FileStream f = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite))
                        {
                            pageImage.Save(f, ImageFormat.Jpeg);
                            pageImage.Dispose();
                        }
                    }
                }
                fs.Flush();
            }
            catch (Exception ex)
            {
                throw new Exception(ex.Message);
            }
            finally
            {
                if (pdfFile != null)
                {
                    pdfFile.Dispose();
                }
                else if (fs != null)
                {
                    fs.Close();
                    fs.Dispose();
                }
            }
        }

    public enum Definition
    {
        One = 1, Two = 2, Three = 3, Four = 4, Five = 5, Six = 6, Seven = 7, Eight = 8, Nine = 9, Ten = 10
    }
  • PDF轉Base64
  • /// <summary>
            /// pdf轉base64
            /// </summary>
            /// <param name="filePath"></param>
            /// <returns></returns>
            public static string PdfWord_To_Base64(string filePath)
            {
                try
                {
                    if (!string.IsNullOrWhiteSpace(filePath.Trim()))
                    {
                        
                            FileStream fileStream = new FileStream(filePath, FileMode.OpenOrCreate);
                            byte[] bt = new byte[fileStream.Length];
                            fileStream.Read(bt, 0, bt.Length);
                            fileStream.Close();
                            fileStream.Dispose();
                            return Convert.ToBase64String(bt);
                    }
                    else
                    {
                        return "請輸入正確的路徑";
                    }
    
                }
                catch (Exception ex)
                {
                    throw ex;
                }
            }

    我主要也就用到這些,之後在發現,我在往上加  

  鏈接: https://pan.baidu.com/s/16xEpLBJ3-8fGjNPyvHUSmA

  提取碼: p9qi

  這個是O2S.Components.PDFRender4NET的文件,我看評論里有問的,之前忘記發了,網上有的是收費的,Nuget裏面帶後綴的我沒用過。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

分類
發燒車訊

豐田奕澤怎麼樣?讓它的設計師來解答這個問題吧

具體的策略,用我的概來概括就是“非常6+1”,六個策略+一個特別關注的事情,具體分為品牌策略、車型策略、網絡策略、區域策略、經銷商盈利策略,一個就是跟經銷店溝通支持,提供快速解決。通過非常6+1的落實,Q1結果出來,銷售了16萬,加上關閉了的4款車型,去年同比增長11%,應該說順利實現開門紅。

如果你要問我一汽豐田近期最受關注的一款車是什麼,我絕對會毫不猶豫告訴你——奕澤,作為一汽豐田旗下首款小型SUV,奕澤憑藉設計犀利的造型、雙色的車身等諸多亮點吸引了不少人的關注,而這一次北京車展上奕澤也正式亮相,雖然並沒有上市,但是距離奕澤馳騁在中國大街小巷那一天已經比較近了,而這一次北京車展我們有幸採訪到一汽豐田汽車銷售有限公司的田青久先生以及一汽豐田銷售常務副總經理水谷雅史先生,一起看看他們能夠給我們分享哪些關於奕澤以及一汽豐田的秘密吧!

主題:一汽豐田專訪

時間:2018年4月25日下午2:30

地點:中國國際會展中心E1

受訪人:一汽豐田汽車銷售有限公司 田青久先生

一汽豐田銷售常務副總經理水谷雅史先生

一汽豐田銷售常務副總經理水谷雅史先生

提問:一汽豐田的各位領導好,我想請問一下炫酷的奕澤6月份上市,有哪些亮點抓住用戶呢?

田青久:他的亮點很多:首先,在基礎的駕駛性能上,無論是駕趣、拐彎、剎車技術性,均達到世界領先的水平。前期在珠海,我們經銷商進行的對比試駕活動,我也參与了試駕,確實有着改朝換代的感覺,跟其他的SUV完全不同,我們將儘快的安排在座的媒體老師體驗這款車。

說起來亮點幾大方面:第一,奕動美學的設計,顏值即正義,顏值用太多的話無法說清楚,只有大家真正感受,用我的話就是一見傾心,心動不已;二見鍾情,買它回家。中看不中用也不行,是吧?首先,鑽石動力組合,搭載了Dynamic Force Engine 2.0L “噴汽流控發動機”加上10速變動器。為什麼是鑽石動力組合,它的性能達到了非常高的水平,126千瓦,熱效率達到世界領先40%。這是從中用的角度來說。中看中用之後,安全不安全呢?在安全配置上從起步車型開始,全系搭載了豐田智行安全系統(TSS系統)和10氣囊,媒體老師都很了解,這種配置一般只在豪華車型上會配備。所以,概括來說就是前所未有的顏值,前所未有的駕趣,前所未有的安全。

提問:伴隨着消費升級,一汽豐田2018年會進行怎麼樣的調整,來應對中國汽車市場的變化?

水谷雅史:首先感謝您對一汽豐田的關心,感謝您的提問。接下來我來回答一下您的問題。

首先,中國市場在飛速的變化發展,這是我的認知。通過這一次的車展,我們看到新的國產車不斷出現,以及新的品牌不斷出現,新的市場在充實。同時,我們也感覺危機感。所以,我想未來的汽車市場外資品牌與純國資品牌的競爭會日趨激烈,這是毋庸置疑的。所以,在這樣的競爭環境下,各大企業必須要提升自己商品的魅力與實力才可以。

為此,從今年開始我們開始導入了TNGA豐巢概念下的新車型。昨天大家看到奕澤IZOA這款車型,在外觀、環境貢獻能力、安全都有進化和提升,商品大幅進化,就是激烈的市場,帶給我們的價值。

第二,我們經銷店給我們客戶提供什麼樣的服務,在未來是越來越重要的。未來,我們將吸取海外的服務經驗,將它們引進國內,給客戶提供更好的服務。謝謝您的問題。

提問:我想問田總一個問題,大家都關注奕澤的問題,我想問一下銷量的問題,2018年銷量目標定位69.5萬台,目標設定是基於什麼樣的考慮,今年將採取什麼樣的措施來確保目標的完成?

田青久:我們考慮了幾個因素:

第一,外部環境。媒體老師都知道,中國車市進入穩定低速增長狀態,前些年動輒二位數高速增長時代一去不復返。

第二,內部因素。一汽豐田正處於發展調整期,今年有四款車型退市,奕澤下半年才上市量銷,有拉伸的過程。今年的增量,需要我們通過現有車型營銷努力,提高營銷質量來解決。

那麼我們在制訂目標的時候,不局限於這個目標,而是要挑戰71,乃至於更高,具體涉及到打法、策略的問題。總體策略是12個字:“增量為本,節奏為先,結構為王”,找增量出口,這是本源。節奏上半年贏,全年贏,把車型銷售作為調整,有明星車型。具體的策略,用我的概來概括就是“非常6+1”,六個策略+一個特別關注的事情,具體分為品牌策略、車型策略、網絡策略、區域策略、經銷商盈利策略,一個就是跟經銷店溝通支持,提供快速解決。通過非常6+1的落實,Q1結果出來,銷售了16萬,加上關閉了的4款車型,去年同比增長11%,應該說順利實現開門紅。我相信後幾個季度堅定執行策略,很好的超額完成,在69.5萬基礎上的目標。

提問:我想問水谷雅史先生一個問題,上午一汽豐田中國發布會公布了卡羅拉雙擎E+的上市會,你能否介紹一下這款車以及戰略意義?

水谷雅史:感謝您的提問。今天上午剛剛公布了卡羅拉雙擎E+這款車型,我認為這款車型的戰略意義,未來面向新能源需求的1號車,具體會在2019年正式進行導入。

我認為這款卡羅拉車型,是目前為止豐田在全球銷售的混合動力的進化版、升級版。大家可能都知道,豐田電動化車型在全世界銷售了100多個國家,銷售量達到1100萬台以上。那麼這一次,在原有車型基礎上,進一步技術能力出現了插電式混合動力卡羅拉雙擎E+的這款車型。這款車型未來面對中國能源政策,是一款毫無問題的車型,而且在充電續航能力上,即使沒有油電支持下,只有純電動的情況下,也可以行駛相當長距離。同時大家也知道,卡羅拉這款車型長期受到全球歡迎,結合上新能源技術,強強聯合出來的車型。

我們會好好準備,把這樣的好產品奉獻給中國市場。這裏我再做一個預告,2020年作為新能源政策的對應第二號車型,是EV車型。再跟大家預告一下,這款車型將成為一汽豐田在全球導入第一款奕澤的EV車型,中國是先於全世界的任何一個國家。

提問:移動互聯網增長迅猛,應對新時代變革方面,一汽豐田在数字營銷、粉絲營銷方面,有沒有什麼創新的想法?

田青久:這個問題我來回答,一汽豐田董事長特別提倡營銷創新,有一句話創新到不能再創新。那麼一汽豐田的創新突破點在哪?最後選在数字體驗營銷方面。為什麼選擇這個端口呢?根據統計目前汽車市場消費者75%是80、90后,而這部分客戶從小接觸互聯網,是互聯網的原住民。在中國消費市場上,移動互聯網應用越來越廣泛,所以我們把創新的突破點鎖定在這個窗口上。

具體正在嘗試做兩方面的工作,一是我們開發了数字聯合運營平台,流量聚合,全過程進行可視化管理,支持渠道(經銷商)DCC業務,提高信息量。第二方面打造粉絲營銷矩陣,600萬保有客戶、620家經銷家、3000零件供應商的現有龐大基盤、加上以在座各位為代表的四五百家友好的媒體、廣大的員工,這五個維度加在一起,有好內容產生的話,在粉絲營銷矩陣上一次觸達接近2000萬人群,二次裂變就可以覆蓋上億的人群。

當今時代人人都是自媒體,就是媒體平台,傳播效率不言而喻。同時建立粉絲社群,通過跟粉絲互動,及時了解粉絲需求的效果,進而通過蓄水和放水的功能,實現銷售的轉化。假設按照0.5%轉化率來算,我們有200萬的粉絲,就是有一萬台增量。所以,在這方面积極探索。目前來看是效果是非常好的。今後大家有好主意,歡迎积極跟我說一下,吸收大家的智慧,我們一起來做嘗試。

提問:我想問田總一個問題,去年600萬達成,一汽豐田推出“安享管家”計劃。能否從客戶利好的角度,給我們介紹具體的內容?

田青久:營銷2.0時代汽車銷售與服務之外,確實各個品牌都在研究汽車延伸價值鏈的計劃。我們“安享計劃”,是把客戶在購車、用車過程當中的痛點都覆蓋到,具體安享計劃有八個方面,包括安心二手車、純牌零件、純正精品、安心租車、AAA延保、AAA保險、貼心金融等。只要用戶購車、用車的過程當中有困難,我們都有相應的產品服務來對應到他們。

謝謝。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

分類
發燒車訊

又要到處浪又要省油,有這種好事?這5款車就行

以不到30萬的價格買到一輛混動的大7座SUV,還要什麼入門BBA。再來看看同樣換新顏、尺寸大一號的全新秦pro,新車搭載一套由BYD476ZQA型1。5T發動機、電動機和鋰電池組構成的插電式混動系統后,工信部申報綜合油耗僅為1升/百公里。

前幾天正巧試駕了朋友剛提不久的雷克薩斯CT200h,除了非常不錯的操控感和車身動態表現之外,讓我印象最深刻的就是油耗,在一頓猛踩和駕駛模式來回切換之後,平均油耗也不過7L/百公里,我相信還有其他車主做出了更漂亮的數據。

雖然當初一早就布局混動車型市場的CT200h並沒有因此而大賣,但發展到今天,隨着混動技術的不斷成熟和國內市場政策的風雲變幻,已經有越來越多的消費者被混動車型的諸多優勢所打動,比如省油、安靜、駕駛體驗等等。所以今天就給大家盤點一下,北京車展之後都有哪些混動車型是值得消費者密切關注的。

1、卡羅拉/雷凌pHEV插電混動版

卡羅拉和雷凌可以說是豐田的兩個“開掛”車型,從燃油版到HEV油電混動版(雙擎)從來都不愁賣,而此次北京車展上豐田終於兌現了兩年前的承諾,在國內正式發布了卡羅拉/雷凌pHEV插電混動版車型。

這兩款新車可以說是含着金鑰匙出生的“富二代”,畢竟豐田多年以來經營的品牌形象、技術實力和市場口碑為這二者打下了堅實的銷量基礎,所以目測上市后月銷1萬輛是基本操作,想要入手的朋友們可要盯緊了。

在外形上兩款新車與普通版的差別並不大,僅針對細節進行了微調,比如車頭和車尾部分都加入了藍色元素。此外,由於插電混動版搭載了更大容量的電池組,所以內部空間與普通版車型稍有差異。

值得一提的是,這兩款車屬於中國特供車,意味着自帶銷量BUG,並且其搭載了1.8L自然吸氣發動機+電動機組,同時還配備容量為9-13kWh的鋰離子電池組,純電動條件下的續航里程可超過100km。

這是神馬概念?起碼就目前我國市場上銷售的插電混動車型,大部分純電動續航里程都只在50-80km以內,並且考慮到緊湊級車推出pHEV版本的多是自主品牌車型,而它們的老對手軒逸、朗逸等還沒加入這個市場,所以此次亮相的卡羅拉和雷凌pHEV版本只要定價合適,銷量應該不成問題,別忘了它們還可獲得一部分政策補貼哦(雙擎版本沒有)。

2、領克01 pHEV插電混動版

說領克01 pHEV版之前必先提一提燃油版領克01,這款車型自去年11月上市以來便憑藉優秀的做工和沃爾沃技術的加持迅速圈粉,獲得市場的好評如潮。本以為領克會按部就班、穩紮穩打地推出新能源產品,不過眼看WEY氣勢洶洶地推出了p8之後,領克怕也是坐不太住了,於是便適時地推出了領克01 pHEV版。

在外觀和內飾方面,該車和之前的燃油版車型基本相同,只是在前進氣格柵以及前翼子板處增加了藍色元素進行點綴,表明其新能源型的身份。而尾部右下側還貼有“01 pHEV”的字樣。

動力方面,領克01 pHEV則搭載了1.5T發動機和電動機所組成的插電式混合動力系統,發動機最大凈功率約180馬力,動力電池為鎳鈷錳三元鋰離子電池,官方宣稱該車的百公里油耗僅為1.8L,同時其純電動模式下最大行駛里程為51km。如果想要入手朋友,其量產車型最快會在今年年內上市,所以準備好鈔票就是了。

3、比亞迪唐DM/秦pro

我們都知道,自從前奧迪設計師艾格入主比亞迪之後,旗下打頭陣車型瞬間秒變高富帥,就連之前被人嫌棄的“BYD”都多了一個洋氣的註釋–Build Your Dream!如果你還沒什麼畫面感的話,在此貢獻兩張圖讓大家感受一下:

了解更多點擊視頻:

全新一代唐从里裡外外都整了個遍,多了幾分國際范。首先在外觀上採用了家族最新的Dragon Face設計語言,整體大氣而又十分吸睛;

內飾同樣話題性十足,其中最搶眼的部分當屬中控可90°旋轉的超級大屏,同時全液晶儀錶和藍色氛圍燈增加了車內的科技感。

動力方面,其將搭載2.0T發動機與電動機組成的插電式混動系統,系統綜合最大功率超過500馬力,而高配車型百公里加速僅需4.5秒,咳咳划重點了–奔馳GLC43 AMG的成績為4.9秒。

值得一提的是,新一代唐標配7座布局,其長寬高分別為4870/1940/1720mm,軸距為2820mm,相比上一代車型在軸距和寬度上有所增加,所以能帶來更寬敞的乘坐體驗。另外在此次北京車展,新一代唐公布預售價–補貼后25萬起步。港真!以不到30萬的價格買到一輛混動的大7座SUV,還要什麼入門BBA!

再來看看同樣換新顏、尺寸大一號的全新秦pro,新車搭載一套由BYD476ZQA型1.5T發動機、電動機和鋰電池組構成的插電式混動系統后,工信部申報綜合油耗僅為1升/百公里!並且根據車尾標判斷,新車的官方百公里加速時間或為5.9秒,看來是充分體現了什麼叫“雨露均沾”了。

4、廣汽謳歌CDX混動版

每每提到謳歌這個品牌,總有兩道跨不過的坎,一個是跟長安LOGO抹不去的姻緣,另一個就是跟隔壁家的雷克薩斯比境遇,很顯然以上兩項都是招黑的。不過也必須承認,痛定思痛的謳歌如今似乎也有點重新步入正軌的意思了。

在此次北京車展,廣汽謳歌就帶來了CDX的混動版本,並且從售價上看還是蠻有誠意的。新車共推出了三款車型,售價區間為29.98-35.28萬元,同時新車外觀顏色增加磨砂皓灰和混動版專屬的藍紫色車漆,內飾則增加了紅色內飾可選,而頂配版車型還將增配Acura Watch。

畢竟謳歌也是“本田大法”的受益者,所以混動版車型相比同級別對手的較大優勢就體現在動力系統上。新車將搭載本田的i-MMD混合動力系統,其由一台2.0L的阿特金森循環自然吸氣發動機、由驅動電機和發電機組成的“電動CVT”直驅傳動機構、動力控制單元及鋰電池組等組成。

這套系統可以根據負載和使用條件的不同,在純電動、混合動力和發動機直驅三種模式之間無縫切換。參數方面,2.0L發動機最大功率146馬力,混動系統綜合最大功率215馬力,而官方公布的綜合油耗為5.0L/百公里。所以三十萬買一台省油的豪華SUV,好像很不錯的樣子!

5、雷克薩斯ES混動版

此次北京車展雷克薩斯的展台只放了兩台車,沒錯!只有兩台!在大多數人看來這就是典型的“寒酸”,而在雷粉眼裡這就叫“傲嬌”,因為雷克薩斯選擇把ES混動版的全球首發放在了中國,雖說一直不願意國產,雖說看展的觀眾不一定get到這樣做的用心良苦,但足以說明其對中國市場的看重。

雷克薩斯全新一代ES基於TNGA平台打造,除搭載全新2.0L發動機外,還配有2.5L多級混合動力系統,也就是全新凱美瑞上用的那一套,沒錯,就是傳說中41%熱效率的那一款名機。同時它還搭載了結構更緊湊的全新一代E-CVT电子無極變速系統,以及布局更為合理的鎳氫電池組。另外,最高純電巡航時速由前一代的75km/h提升到120km/h。

如想了解更多,點擊下方視頻:

結語

從當初雷克薩斯CT200h的“天生異質”,到如今各家廠商混合動力車型的日漸成熟,可以說能活下來的、能在市場上銷售的技術都是好技術。不管是自主品牌的比亞迪、長城、吉利,還是合資品牌的兩田,甚至是進口品牌,都在加緊布局新能源戰線,所以這對我們消費者來說無疑是個大利好。至於混動車型值不值得買這樣俗套的話題,我想就不用多糾結了,畢竟買車從來都是按需購買的。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

分類
發燒車訊

玩車的人都在捧的本田發動機,真的這麼神?

然而,這種熟悉的“爆tec”感覺沒有出現在最新的謳歌TLX-L上。謳歌是本田的豪華品牌,一切的用料,技術,做工等都用上高標準。就連本田被人詬病的隔音謳歌也能做得安靜。被拉高轉的本田發動機“爆tec”時的感覺聲音和動力都有着明顯變化,讓人為之興奮,但開着謳歌TLX-L卻沒有這種感覺。

本田,一個無人不知無人不曉的一個汽車品牌。尤其是在廣東地區,更是本田粉活躍的地方,大街上跑的10輛車中,有4輛或5輛是本田車型。

買本田車,除了買質量可靠性外,更多的是為了“買發動機送車”,而坊間也流傳着本田金句VTEC is the best。可想而知,本田發動機技術含量和民望有多高。

本田發動機的看家技術VTEC和i-VTEC技術外,在近年也推出VTEC Turbo這渦輪組合技術。

那本田技術牛在哪?主要除了有可變氣門正時外,還有氣門升程這一技術。氣門升程技術並不是很多發動機有,除了寶馬和英菲尼迪VQ37系列外,還真是寥寥無幾。

開過本田的人未必知道什麼叫“爆tec”,但拉過高轉的人就知道。當發動機轉速拉到5000轉左右的時候,聲音突變,發動機也感覺顫抖一下。這就是“爆tec”給人的感覺。

然而,這種熟悉的“爆tec”感覺沒有出現在最新的謳歌TLX-L上。謳歌是本田的豪華品牌,一切的用料,技術,做工等都用上高標準。就連本田被人詬病的隔音謳歌也能做得安靜。

被拉高轉的本田發動機“爆tec”時的感覺聲音和動力都有着明顯變化,讓人為之興奮,但開着謳歌TLX-L卻沒有這種感覺。難道就因為高端車型而被“技術”隔絕?

其實這種“爆tec”的感覺在自主品牌的渦輪車上有着“更好的體現”。只不過我們吐槽它而已,這一技術就是渦輪遲滯。體驗過大部分自主品牌渦輪車,輕踩油門不去,踩重一點像是被踢出去的感覺。這種感覺比“爆tec”還爽快。

總結:高轉“爆tec”還是低轉渦輪介入明顯,都使得發動機動力輸出沒那麼“線性”。只是拉高轉的時候,亢奮的聲音和動力的凸顯使得“爆tec”的感覺被神化。渦輪介入明顯,其實就是“爆tec”時動力凸顯的一部分,只是沒有亢奮的聲音罷了。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

分類
發燒車訊

Spark文檔閱讀之二:Programming Guides – Quick Start

Quick Start: https://spark.apache.org/docs/latest/quick-start.html

 

在Spark 2.0之前,Spark的編程接口為RDD (Resilient Distributed Dataset)。而在2.0之後,RDDs被Dataset替代。Dataset很像RDD,但是有更多優化。RDD仍然支持,不過強烈建議切換到Dataset,以獲得更好的性能。 RDD文檔: https://spark.apache.org/docs/latest/rdd-programming-guide.html Dataset文檔: https://spark.apache.org/docs/latest/sql-programming-guide.html  

一、最簡單的Spark Shell交互分析

scala> val textFile = spark.read.textFile("README.md")   # 構建一個Dataset
textFile: org.apache.spark.sql.Dataset[String] = [value: string]

scala> textFile.count()  # Dataset的簡單計算
res0: Long = 104 

scala> val linesWithSpark = textFile.filter(line => line.contain("Spark"))  # 由現有Dataset生成新Dataset
res1: org.apache.spark.sql.Dataset[String] = [value: string]
# 等價於:
# res1 = new Dataset()
# for line in textFile:
#     if line.contain("Spark"):
#         res1.append(line)
# linesWithSpark = res1

scala> linesWithSpark.count()
res2: Long = 19

# 可以將多個操作串行起來
scala> textFile.filter(line => line.contain("Spark")).count()
res3: Long = 19

 

進一步的Dataset分析:

scala> textFile.map(line => line.split(" ").size).reduce((a,b) => if (a > b) a else b)
res12: Int = 16
# 其實map和reduce就是兩個普通的算子,不要被MapReduce中一個map配一個reduce、先map后reduce的思想所束縛
# map算子就是對Dataset的元素X計算fun(X),並且將所有f(X)作為新的Dataset返回
# reduce算子其實就是通過兩兩計算fun(X,Y)=Z,將Dataset中的所有元素歸約為1個值

# 也可以引入庫進行計算
scala> import java.lang.Math
import java.lang.Math

scala> textFile.map(line => line.split(" ").size).reduce((a, b) => Math.max(a, b))
res14: Int = 16

# 還可以使用其他算子
scala> val wordCounts = textFile.flatMap(line => line.split(" ")).groupByKey(identity).count()

# flatMap算子也是對Dataset的每個元素X執行fun(X)=Y,只不過map的res是
#     res.append(Y),如[[Y11, Y12], [Y21, Y22]],結果按元素區分
# 而flatMap是
#     res += Y,如[Y11, Y12, Y21, Y22],各元素結果合在一起

# groupByKey算子將Dataset的元素X作為參數傳入進行計算f(X),並以f(X)作為key進行分組,返回值為KeyValueGroupedDataset類型
# 形式類似於(key: k; value: X1, X2, ...),不過KeyValueGroupedDataset不是一個Dataset,value列表也不是一個array
# 注意:這裏的textFile和textFile.flatMap都是Dataset,不是RDD,groupByKey()中可以傳func;如果以sc.textFile()方法讀文件,得到的是RDD,groupByKey()中間不能傳func

# identity就是函數 x => x,即返回自身的函數

# KeyValueGroupedDataset的count()方法返回(key, len(value))列表,結果是Dataset類型

scala> wordCounts.collect()
res37: Array[(String, Long)] = Array((online,1), (graphs,1), ...
# collect操作:將分佈式存儲在集群上的RDD/Dataset中的所有數據都獲取到driver端

 

數據的cache:

scala> linesWithSpark.cache()  # in-memory cache,讓數據在分佈式內存中緩存
res38: linesWithSpark.type = [value: string]

scala> linesWithSpark.count()
res41: Long = 19

 

二、最簡單的獨立Spark任務(spark-submit提交)

需提前安裝sbt,sbt是scala的編譯工具(Scala Build Tool),類似java的maven。 brew install sbt   1)編寫SimpleApp.scala

import org.apache.spark.sql.SparkSession

object SimpleApp {
    def main(args: Array[String]) {
        val logFile = "/Users/dxm/work-space/spark-2.4.5-bin-hadoop2.7/README.md"
        val spark = SparkSession.builder.appName("Simple Application").getOrCreate()
        val logData = spark.read.textFile(logFile).cache()
        val numAs = logData.filter(line => line.contains("a")).count()  # 包含字母a的行數
        val numBs = logData.filter(line => line.contains("b")).count()  # 包含字母b的行數
        println(s"Lines with a: $numAs, Lines with b: $numBs")
        spark.stop()
    }
}

 

2)編寫sbt依賴文件build.sbt

name := "Simple Application"

version := "1.0"

scalaVersion := "2.12.10"

libraryDependencies += "org.apache.spark" %% "spark-sql" % "2.4.5"

 

其中,”org.apache.spark” %% “spark-sql” % “2.4.5”這類庫名可以在網上查到,例如https://mvnrepository.com/artifact/org.apache.spark/spark-sql_2.10/1.0.0

 

3)使用sbt打包 目錄格式如下,如果SimpleApp.scala和build.sbt放在一個目錄下會編不出來

$ find .
.
./build.sbt
./src
./src/main
./src/main/scala
./src/main/scala/SimpleApp.scala

 

sbt目錄格式要求見官方文檔 https://www.scala-sbt.org/1.x/docs/Directories.html

src/
  main/
    resources/
       <files to include in main jar here>
    scala/
       <main Scala sources>
    scala-2.12/
       <main Scala 2.12 specific sources>
    java/
       <main Java sources>
  test/
    resources
       <files to include in test jar here>
    scala/
       <test Scala sources>
    scala-2.12/
       <test Scala 2.12 specific sources>
    java/
       <test Java sources>

 

使用sbt打包

# 打包
$ sbt package
...
[success] Total time: 97 s (01:37), completed 2020-6-10 10:28:24
# jar包位於 target/scala-2.12/simple-application_2.12-1.0.jar

 

4)提交並執行Spark任務

$ bin/spark-submit --class "SimpleApp" --master spark://xxx:7077 ../scala-tests/SimpleApp/target/scala-2.12/simple-application_2.12-1.0.jar
# 報錯:Caused by: java.lang.ClassNotFoundException: scala.runtime.LambdaDeserialize
# 參考:https://stackoverflow.com/questions/47172122/classnotfoundexception-scala-runtime-lambdadeserialize-when-spark-submit
# 這是spark版本和scala版本不匹配導致的

 

查詢spark所使用的scala的版本

$ bin/spark-shell --master spark://xxx:7077

scala> util.Properties.versionString
res0: String = version 2.11.12

 

修改build.sbt: scalaVersion := “2.11.12” 從下載頁也可驗證,下載的spark 2.4.5使用的是scala 2.11  

 

重新sbt package,產出位置變更為target/scala-2.11/simple-application_2.11-1.0.jar 再次spark-submit,成功

 

$ bin/spark-submit --class "SimpleApp" --master spark://xxx:7077 ../scala-tests/SimpleApp/target/scala-2.11/simple-application_2.11-1.0.jar 
Lines with a: 61, Lines with b: 30

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

分類
發燒車訊

MySQL 性能優化之慢查詢

性能優化的思路

  1. 首先需要使用慢查詢功能,去獲取所有查詢時間比較長的SQL語句
  2. 其次使用explain命令去查詢由問題的SQL的執行計劃(腦補鏈接:點我直達1,點我直達2)
  3. 最後可以使用show profile[s] 查看由問題的SQL的性能使用情況
  4. 優化SQL語句

介紹

  數據庫查詢快慢是影響項目性能的一大因素,對於數據庫,我們除了要優化SQL,更重要的是得先找到需要優化的SQL語句

  MySQL數據庫有一個“慢查詢日誌”功能,用來記錄查詢時間超過某個設定值的SQL,這將極大程度幫助我們快速定位到問題所在,以便對症下藥

至於查詢時間的多少才算慢,每個項目、業務都有不同的要求。
    比如傳統企業的軟件允許查詢時間高於某個值,但是把這個標準方在互聯網項目或者訪問量大的網站上,估計就是一個Bug,甚至可能升級為一個功能缺陷。

  MySQL的慢查詢日誌功能,默認是關閉的,需要手動開啟

開啟慢查詢功能

查看是否開啟慢查詢功能

 

 

參數說明:

  • slow_query_log:是否開啟慢查詢,on為開啟,off為關閉;
  • log-slow-queries:舊版(5.6以下版本)MySQL數據庫慢查詢存儲路徑,可以不設置該參數,系統則會給一個缺省的文件:host_name-slow.log
  • long_query_time:慢查詢閥值,當查詢時間多於設置的閥值時,記錄日誌,單位為秒。

臨時開啟滿查詢功能

  在MySQL執行SQL語句設置,但是如果重啟MySQL的話會失效。

set global slow_query_log=on;
set global long_query_time=1;

永久性開啟慢查詢

  修改:/etc/my.cnf,添加以下內容,然後重啟MySQL服務

[mysqld]
lower_case_table_names=1
slow_query_log=ON
slow_query_log_file=/usr/local/mysql/data/chenyanbindeMacBook-Pro-slow.log
long_query_time=1

 

查看滿查詢啟動狀態

演示慢查詢

  為了演示方便,我們讓sql睡眠3秒!

格式說明:

  • 第一行,SQL查詢執行的具體時間
  • 第二行,執行SQL查詢的連接信息,用戶和連接IP
  • 第三行,記錄了一些我們比較有用的信息,
    • Query_timme,這條SQL執行的時間,越長則越慢
    • Lock_time,在MySQL服務器階段(不是在存儲引擎階段)等待表鎖時間
    • Rows_sent,查詢返回的行數
    • Rows_examined,查詢檢查的行數,越長就越浪費時間
  • 第四行,設置時間戳,沒有實際意義,只是和第一行對應執行時間。
  • 第五行,執行的SQL語句記錄信息

分析滿查詢日誌

MySQL自帶的mysqldumpslow

 

 

 

參數說明:

  • -s, 是表示按照何種方式排序,c、t、l、r分別是按照記錄次數、時間、查詢時間、返回的記錄數來排序,ac、at、al、ar,表示相應的倒敘;
  • -t, 是top n的意思,即為返回前面多少條的數據;
  • -g, 後邊可以寫一個正則匹配模式,大小寫不敏感的;

MySQL性能fenix語句show profile(重要

介紹

  • Query Profiler是MySQL自帶的一種query診斷分析工具,通過它可以分析出一條SQL語句性能瓶頸在什麼地方。
  • 通常使用explain,以及slow query log都無法做到精確分析,但是Query profiler卻可以定位出一條SQL執行的各種資源消耗情況,比如CPU、IO等,以及該SQL執行所耗費的時間等。不過該工具只有在MySQL5.0.37以上版本中才有實現
  • 默認的情況下,MySQL的該功能沒有打開,需要自己手動打開

語句使用

  • show profileshow profiles語句可以展示當前會話(退出session后,profiling重置為0)中執行語句的資源使用情況。
  • show profiles:以列表形式显示最近發送到服務器上執行的語句的資源使用情況,显示的記錄數由變量:profiling_history_size控制,默認15條
  • show profile:只是最近一條語句執行的消息資源佔用信息,默認實現Status和Duration兩列

開啟Profile功能

  • Profile功能由MySQL會話變量:profiling控制,默認是OFF關閉狀態。
  • 查看是否開啟了Profile功能
select @@profiling;

show variables like '%profil%';

 

打開profiling功能

set profiling=1;

 

show profile用法

SHOW PROFILE [type [, type] …… ] [FOR QUERY n] [LIMIT row_count [OFFSET offset]]

type: { ALL | BLOCK IO | CONTEXT SWITCHES | CPU | IPC | MEMORY | PAGE FAULTS | SOURCE | SWAPS }

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

分類
發燒車訊

【Spring】BeanDefinition&PostProcessor不了解一下嗎?

水稻:這两天看了BeanDefinition和BeanFactoryPostProcessor還有BeanPostProcessor的源碼。要不要了解一下

菜瓜:six six six,大佬請講

水稻:上次我們說SpringIOC容器是一個典型的工廠模式

  • 假如我們把Spring比作一個生產模型的大工廠,那麼.class文件就是原材料。而BeanDefinition就是創建模型的模具。不管是傳統的XML還是後面的註解,Spring在啟動的時候都會創建一個掃描器去掃描指定目錄下的.class文件,並根據文件的註解,實現的接口以及成員變量將其封裝一個個的BeanDefinition。
    • 比較重要的屬性有id,class,構造函數封裝類,屬性封裝類,factoryMethod等
  • 在對象初始化之前Spring會完成BeanDefinition對象的解析並將其裝入List容器beanDefinitionNames中,然後開始遍歷該容器並根據BeanDefinition創建對象

菜瓜:sodasinei,BeanDefinition我了解了。它是創建bean的模板,類似於java創建對象依賴的class一樣。那還有兩個很長的單詞是啥呢?

水稻:忽略掉後面老長的後綴,我們看BeanFactory和Bean是不是很親切。PostProcessor被翻譯成後置處理器,暫且我們把它看成是處理器就行

  • BeanFactory是bean工廠,它可以獲取並修改BeanDefinition的屬性,進而影響後面創建的對象。
  • Bean就是Spring的對象,這些個處理器才是真正處理bean對象的各個環節的工序,包括屬性,註解,方法

菜瓜:有了模糊的概念,不明覺厲

水稻:來,看demo

package com.vip.qc.postprocessor;

import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Component;

/**
 * 獲取初始化好的BeanFactory,此時還未進行bean的實例化
 *
 * @author QuCheng on 2020/6/14.
 */
@Component
public class BeanFactoryPostProcessorT implements BeanFactoryPostProcessor {

    public static final String BEAN_NAME = "processorT";

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        BeanDefinition initializingBeanT = beanFactory.getBeanDefinition(BEAN_NAME);
        MutablePropertyValues propertyValues = initializingBeanT.getPropertyValues();
        String pName = "a";
        System.out.println("BeanFactoryPostProcessor a " + propertyValues.getPropertyValue(pName) + " -> 1");
        propertyValues.addPropertyValue(pName, "1");
    }
}


package com.vip.qc.postprocessor;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;

/**
 * @author QuCheng on 2020/6/14.
 */
@Component
public class BeanPostProcessorT implements BeanPostProcessor {

    public static final String beanNameT = "processorT";

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (beanNameT.equals(beanName)) {
            ProcessorT processorT = ((ProcessorT) bean);
            System.out.println("BeanPostProcessor BeforeInitialization  a:" + processorT.getA() + "-> 3");
            processorT.setA("3");
        }
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (beanNameT.equals(beanName)){
            ProcessorT processorT = ((ProcessorT) bean);
            System.out.println("BeanPostProcessor AfterInitialization  a:" + processorT.getA() + "-> 4");
            processorT.setA("4");
        }
        return bean;
    }

}


package com.vip.qc.postprocessor;

import org.springframework.stereotype.Component;

/**
 * @author QuCheng on 2020/6/14.
 */
@Component
public class ProcessorT {

    public ProcessorT() {
        System.out.println("ProcessorT 無參構造 a:" + a + "-> 2" );
        a = "2";
    }

    private String a;

    public String getA() {
        return a;
    }

    public void setA(String a) {
        this.a = a;
    }

    @Override
    public String toString() {
        return "ProcessorT{" +
                "a='" + a + '\'' +
                '}';
    }
}

// 測試類
@Test
public void test() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("com.vip.qc.postprocessor");
    ProcessorT processorT = (ProcessorT) context.getBean("processorT");
    System.out.println(processorT);
}

// 結果
BeanFactoryPostProcessor a null -> 1
ProcessorT 無參構造 a:null-> 2
BeanPostProcessor BeforeInitialization a:1-> 3
BeanPostProcessor AfterInitialization a:3-> 4
ProcessorT{a='4'}
  • BeanFactoryPostProcessor在對象還未初始化前可以拿到對象的BeanDefinition對其設置屬性值
  • 過程中我們分別對屬性a設置了1,2,3,4的值。最後我們拿到的值為4

菜瓜:好像看懂了。BeanFactoryPostProcessor可以拿到BeanFactory對象,獲取裏面所有的BeanDefinition並可對其進行干預。BeanPostProcessor其實是在bean已經被創建完成之後進行加工操作

水稻:沒錯。這是我們自己進行干預的demo。限於篇幅有限,你可以去看一下Spring自己對於這兩個接口的實現源碼。比較重要的推薦下面幾個

  • ConfigurationClassPostProcessor 實現BeanFactoryPostProcessor子接口
    • 完成對@Configuration、@Component、@ComponentScan、@Bean、@Import、@ImportSource註解的搜集和解析
    • @Bean註解會被封裝成所在Bean的BeanDefinition中的factoryMethod屬性中,單獨進行實例化
  • CommonAnnotationBeanPostProcessor 實現 BeanPostProcessor
    • 完成@PostConstruct@PreDestroy@Resource註解的搜集和解析工作
    • @PostConstruct會在對象初始化且屬性渲染完成後進行
    • @Resource註解(參照下面)
  • AutowiredAnnotationBeanPostProcessor 實現 BeanPostProcessor
    • 完成@Autowired@Value註解的搜集和解析工作
    • 在對象初始化完成之後會先進行註解的搜集,然後進行屬性渲染調用populateBean方法,使用策略模式調用實現接口對註解進行解析,有@Autowired和@Value註解會調用getBean方法發起對依賴屬性的注入
  • AbstractAutoProxyCreator的入口類也是實現的BeanPostProcessor

菜瓜:你放心,我不會看的。這麼複雜的東西,聽着都費勁

水稻:不愧是你!有機會聊bean的生命周期的時候咱們還會說到這些東西。到時候再刷一遍

 

總結:

  • BeanDefinition是spring容器創建對象的模板,定義了bean創建的細節
  • BeanFactoryPostProcessor可以拿到整個容器對象,當然也能修改BeanDefinition,所以能直接操作bean的創建
  • BeanPostProcessor執行的時候bean已經創建完成了,我們可以拿到想要的對象進行干預和設值等操作

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益