北汽集團將成為美國新能源公司Atieva第一大股東

北京汽車集團有限公司(北汽集團)昨(17)日宣布,與美國新能源公司Atieva簽署股份認購協議,北汽集團將收購Atieva公司25.02%的股份。收購完成後,北汽將成為Atieva的第一大股東,雙方預計在第3年推出與奧迪A6L同等級的電動汽車。

由於美國電動車大廠特斯拉也正和北汽股份洽談合作事項,昨日北汽旗下的上市公司福田汽車,股價也跟著漲停。

去年,北汽集團新成立了新能源汽車公司,而此次收購的美國Atieva公司是一家新能源汽車核心系統提供商,曾主要參與過Tesla Roadster純電動跑車、雪佛蘭Volt插電式混合電動車、奧迪R8純電動跑車的開發。

北汽集團方面也表示,此次收購主要是為進一步提升北汽集團及下屬公司在新能源汽車尤其是高端純電動汽車領域的設計、研發和制造的能力和水平。

據悉,北汽由6家股東組成,除北汽集團以51%的股比成為控股股東外,首鋼股份有限公司以18.31%的股比成為第二大股東,其他股東包括北京市國資公司、現代創新控股公司及京能集團,而北京市國資委直屬的投融資平台-北京國有資本經營管理中心也持股5%。

另據外媒稍早報導,北汽可望在2014年第2季在香港IPO上市,籌資額度或達到20億美元。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

新北清潔公司,居家、辦公、裝潢細清專業服務

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

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

※超省錢租車方案

FB行銷專家,教你從零開始的技巧

第二屆新能源車與充電技術中國國際論壇

地點:上海綠地萬豪酒店 /中國上海黃浦區江濱路99號(靠近打浦路)
會議背景:

    新能源汽車是國務院確定的重要戰略性新興產業,是我國最具發展潛力的重要領域之一。近日來中國大面積飽受霧霾天氣困擾的嚴峻形勢,更使得加快新能源汽車的發展迫在眉睫。隨著新一輪新能源車補貼政策的出臺以及首批28個示範運行城市的公佈,新能源汽車產業展現出良好的發展態勢。

    中國政府和車企致力於電動車在全國範圍內的大規模推廣。工信部發佈,我國2015年應有規模將達到50萬輛,到2020年將達到500萬輛。據專家預測,中國十年內將有望成為最大的新能源車市場。為適應電動汽車發展要求,國家將在“十二五”期間,建設充換電站2351座,充電樁22萬股,初步建成覆蓋公司經營區域的智慧充換電服務網路。與此同時,行業在積累了一些寶貴的經驗的同時也出現了在技術路徑、政策導向、基礎設施、商業模式、行業標準等方面急需解決的一些突出問題。

    比亞迪,奇瑞,大眾,通用,富豪等車企都在為滿足新一輪中國客戶對新能源汽車的需求謀劃新局,為更好的享受政府補貼而將新能源汽車技術和生產實現本土化。

  由全國電力技術市場協會機構指導,OPPLAND MEDIA策劃組織的第二屆新能源車與充電技術中國國際論壇將於2014年4月29-30日在上海舉辦。大會網站:

    第二屆新能源車與充電技術中國國際論壇2014旨在加快新能源車推廣,促進中國電動車產業發展,進一步加強行業交流,深入探討技術創新,共同分享中國現有電動車示範城市專案運營經驗,為中國新能源車發展及環境治理出一份力。

    論壇將圍繞“清除環境與政策上的霧霾”為主題,就新能源汽車關鍵技術發展與應用,產業創新研發機制,示範運營與市場推廣、基礎配套設施建設、國際合作與交流等一系列熱點問題展開廣泛地探討與合作,推動新能源汽車產業化、市場化、國際化。本次論壇將邀請國家發展改革委等國家有關部門領導,歐洲汽車工業協會,美國加州大學、美國電動車聯盟等國際組織、中國汽車工業協會、中國汽車工程學會、中國汽車技術研究中心、清華大學、同濟大學等行業組織和研究機構的專家學者,美國加州,挪威、倫敦、北京、上海、天津、廣州、深圳等中外地方政府官員,以及國內外各大汽車集團高層,共同圍繞節能與新能源汽車產業發展的宏觀視角、政策與管理、戰略目標與方向、汽車企業的節能與新能源戰略與行動、新能源汽車發展中的新競爭以及商業探索與經驗交流等議題進行深入研討和交流。

會議板塊:

1. 中國新能源車政策和市場發展
2. 建立中國新能源車標準化體系規範之路
3. 新能源車電池和電機控制系統科技創新
4. 中國電動車充電基礎設施建設新進展

會議熱點:
中國發展新能源車市場應用的路線圖
中國新能源車市場的投資計畫以及帶給全球OEM的機會
中國新能源車財政補貼從地方政府到車企的政策更新
下一輪新能源車推廣應用項目概覽
純電動車,混動車等新能源車技術交流
下一代新能源車新電池組件和關鍵技術
中國充電基礎設施和車企之間的協調發展
BMS電池管理系統迎合中國新能源車增長的精細化管理
新能源車快速發展的產業需求和規劃
統一存在于車企、政府和供應商之間的不同標準

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

新北清潔公司,居家、辦公、裝潢細清專業服務

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

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

※超省錢租車方案

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

網頁設計最專業,超強功能平台可客製化

日企推水陸兩用電動汽車FOMM Concept One

目前限制全球電動汽車發展的主要瓶頸,在於電池成本和續航力問題。日前有日本廠商推出一款名為FOMM Concept One的超小型電動車,雖說續航力僅有100km,但是卻有另外一點吸引人的地方,那就是如果碰到水災還能變成小艇,且價格非常親民。據說不到日幣100萬圓(約合新台幣29.7萬元)。

這款小型電動汽車,雖然不是專門的水陸兩用車,但遇到洪水等緊急情況,可以在水面漂浮24小時,也能在水面上以時速3.8公里左右的速度移動。車內裝有可拆卸的電池,充滿電後最多可在陸地上行駛100公里。這款4人座汽車全長約2.5公尺,重量僅460公斤,計劃從明年10月開始先在水災較多的泰國銷售。

FOMM公司由日本大同工業(DAIDO Kogyo)、日本特殊陶業(NGK)所共同建立,FOMM Concept One為幾家公司合作下的第一個產物。根據FOMM的規劃, Concept One的開發成本不低,但2014年4-6月將有一比龐大資金注入,倘若一切都順利預計2015年9月將可在泰國進行量產,至於第一年銷售量目標是5000輛。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

將電動車行程提升超10% 英公司開發MSYS動力系統

據英國媒體報導,英國動力系統設計公司宣佈,該公司正在開發一套名為MSYS的電動汽車動力系統,可以將電動汽車行駛里程增加10%至15%。MSYS系統預計將於2016年投產。

在英國即將舉辦的未來動力系統會議上,英國動力系統設計公司將發佈高效電動汽車動力系統報告。該動力系統提供了一種新的汽車換擋路徑,而且不會引起扭矩中斷。

該公司技術總監阿萊克斯•泰利•博達爾表示,該技術可提供55千瓦的持續電力供應,超過2000牛/米的扭矩,其電力動力系統效率可達91%。

阿萊克斯還透露,MSYS系統避免了其他傳動方法的弊端。雙離合器變速箱(DCT)在離合器開啟或關閉狀態時,都會持續消耗能量。而在換擋時,自動變速器(AMT)會受損於扭矩中斷。此外,基於行星齒輪變速器的自動裝置,則增加了複雜性和成本,並會拖慢速度。

MSYS系統將提供三種速度傳動裝置,使其換擋和快速換擋都能運用重疊換擋技術。隨著技術的完善,多重比例的選擇對電動汽車的變速會非常有益。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

FB行銷專家,教你從零開始的技巧

Blazor帶我重玩前端(一)

寫在前面

曾經我和前端朋友聊天的時候,我說我希望有一天可以用C#寫前端,不過當時更多的是美好的想象,而現在這一切正變得真實……

 

什麼是Blazor

我們知道瀏覽器可以正確解釋並執行JavaScript代碼,那麼瀏覽器是如何執行C#代碼的呢?答案是通過WebAssembly。通過WebAssembly,我們可以讓瀏覽器運行很多的高級語言,如 C#、C、C++、GO等,並使他們運行在基於內存安全的沙箱環境中。如下圖所示:

作為一個已經五六年沒有寫過前端的.NET程序員,遇到Blazor實在是幸運中的幸運。它又讓我可以很愉快的寫前端了,而且還是用C#去寫,我也就不用再分出精力去學習其他的JS框架了。

我們可以認為Blazor是.NET對WebAssembly的實現。通過使用Blazor,我們可以使用C#語言來取代JS去開發交互式Web UI。

值得一提的是,Blazor是由Browser和Razor這兩個單詞合併而成的,意思就是Blazor可以基於客戶端執行Razor視圖后將HTML呈現給瀏覽器。所以想要更好的理解Blazor,就要首先更好的了解瀏覽器和Razor。

Blazor有以下幾個優點:
  • 使用C#來取代JavaScript創建豐富的交互式UI
  • 基於.NET及其生態編寫服務器端和客戶端應用程序邏輯
  • 糅合現有HTML和CSS技術,提供了廣泛的瀏覽器支持,包括移動瀏覽器(注意:Blazor取代的是基於JavaScript的UI交互,而其他部分如HTML、CSS,這些是我們的技術基礎)
  • 與現代託管平台(例如Docker)集成。
  • Blazor是開源的,其源碼位置在GitHub上

另外需要注意的,Blazor和Silverlight不可混為一談,Blazor是基於開放標準而構建的,本身不需要任何額外插件。而Silverlight帶有太多自有特性,所以不得不在瀏覽器上安裝插件以更好的支持其運行。

什麼是WebAssembly

概覽

WebAssembly是一種二進制格式的指令集,其設計目標是能夠在解釋或者將其編譯為本地機器代碼並執行他們的機器上運行,這類似於我們.NET編譯后的IL。

WebAssembly可以作為編譯高級編程語言的可移植目標,通過節省大小和加載時間,充分利用各種平台(移動平台和IOT平台)上的通用應用功能,使得WebAssembly可以以接近於本機(接近於本機的英語單詞是:near-native,在語言學里意思是精通語言的人,所說的話和說母語的人沒有什麼區別)的運行速度運行。

支持

WebAssembly已經獲得了大部分瀏覽器的支持。詳細內容可以移步至Can I Use

手寫一個例子

接下來我們看一個例子,方便起見,我們直接使用在線的WebAssembly編譯工具,地址是:https://mbebenita.github.io/WasmExplorer/。目前,這個工具只支持C和C++。不過也沒有什麼關係,我們寫一個簡單的方法用於測試即可。

  • 首先我們定義了一個計算兩個數和的方法:
1 int Addition(int a, int b)
2 {
3   return a + b; 
4 }
  • 然後點擊COMPILE

在中間的框里會生成WAT(即WebAssembly文本格式)的代碼,最右邊的是二進制了。中間的代碼部分可以幫助我們查看在編譯的過程中發生了什麼,會看到生成了一個名為_Z8Additionii的function,其中8表示這個方法名的長度,後面的i表示有多個參數,接下來我們會去調用它。

 1 (module
 2  (table 0 anyfunc)
 3  (memory $0 1)
 4  (export "memory" (memory $0))
 5  (export "_Z8Additionii" (func $_Z8Additionii))
 6  (func $_Z8Additionii (; 0 ;) (param $0 i32) (param $1 i32) (result i32)
 7   (i32.add
 8    (get_local $1)
 9    (get_local $0)
10   )
11  )
12 )
  • 點擊Download,下載.WAT文件

  • 接下來我們再寫一個HTML網頁出來,就用那種最簡單的HTML代碼,代碼如下:
 1 <HTML>
 2 <HEAD>
 3     <TITLE>WebAssembly Sample: Call C++ Code</TITLE>
 4     <script type="text/javascript">
 5         let addition = fetch('test.wasm')
 6             .then(response => response.arrayBuffer())
 7             .then(buffer => WebAssembly.compile(buffer))
 8             .then(module => { return new WebAssembly.Instance(module) })
 9             .then(instance => { addition = instance.exports._Z8Additionii }); 
10     </script>
11 </HEAD>
12 <BODY BGCOLOR="FFFFFF">
13     <h1>WebAssembly Sample: Call C++ Code</h1>
14 </BODY>
15 </HTML>
  • 最終的效果圖

通過以上示例,我們基本上對Blazor和WebAssembly的部分運行機制有了一個比較清晰的認識了,接下來,我們繼續討論有關Blazor的內容。

參考鏈接:

https://webassembly.org/

https://webassembly.github.io/spec/js-api/index.html

https://caniuse.com/#search=wasm

https://webassembly.github.io/spec/js-api/index.html


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

【其他文章推薦】

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

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

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

網頁設計最專業,超強功能平台可客製化

完美解決asp.net core 3.1 兩個AuthenticationScheme(cookie,jwt)共存在一個項目中,基於領域驅動設計(DDD)超輕量級快速開發架構

內容

在我的項目中有mvc controller(view 和 razor Page)同時也有webapi,那麼就需要網站同時支持2種認證方式,web頁面的需要傳統的cookie認證,webapi則需要使用jwt認證方式,兩種默認情況下不能共存,一旦開啟了jwt認證,cookie的登錄界面都無法使用,原因是jwt是驗證http head “Authorization” 這屬性.所以連login頁面都無法打開.

解決方案

實現web通過login頁面登錄,webapi 使用jwt方式獲取認證,支持refreshtoken更新過期token,本質上背後都使用cookie認證的方式,所以這樣的結果是直接導致token沒用,認證不是通過token唯一的作用就剩下refreshtoken了

通過nuget 安裝組件包

Microsoft.AspNetCore.Authentication.JwtBearer

下面是具體配置文件內容

//Jwt Authentication
      services.AddAuthentication(opts =>
      {
        //opts.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        //opts.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
      })
      //這裡是關鍵,添加一個Policy來根據http head屬性或是/api來確認使用cookie還是jwt chema
        .AddPolicyScheme(settings.App, "Bearer or Jwt", options =>
        {
          options.ForwardDefaultSelector = context =>
          {
            var bearerAuth = context.Request.Headers["Authorization"].FirstOrDefault()?.StartsWith("Bearer ") ?? false;
            // You could also check for the actual path here if that's your requirement:
            // eg: if (context.HttpContext.Request.Path.StartsWithSegments("/api", StringComparison.InvariantCulture))
            if (bearerAuth)
              return JwtBearerDefaults.AuthenticationScheme;
            else
              return CookieAuthenticationDefaults.AuthenticationScheme;
          };
        })
//這裏和傳統的cookie認證一致       .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
       {
         options.LoginPath = "/Identity/Account/Login";
         options.LogoutPath = "/Identity/Account/Logout";
         options.AccessDeniedPath = "/Identity/Account/AccessDenied";
         options.Cookie.Name = "CustomerPortal.Identity";
         options.SlidingExpiration = true;
         options.ExpireTimeSpan = TimeSpan.FromSeconds(10); //Account.Login overrides this default value
       })
        .AddJwtBearer(x =>
      {
        x.RequireHttpsMetadata = false;
        x.SaveToken = true;
        x.TokenValidationParameters = new TokenValidationParameters
        {
          ValidateIssuerSigningKey = true,
          IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Configuration["Jwt:Key"])),
          ValidateIssuer = true,
          ValidateAudience = true,
          ValidateLifetime = true,
          ValidIssuer = Configuration["Jwt:Issuer"],
          ValidAudience = Configuration["Jwt:Issuer"],
        };
      });

 //這裏需要對cookie做一個配置
      services.ConfigureApplicationCookie(options =>
      {
        // Cookie settings
        options.Cookie.Name = settings.App;
        options.Cookie.HttpOnly = true;
        options.ExpireTimeSpan = TimeSpan.FromSeconds(10);
        options.LoginPath = "/Identity/Account/Login";
        options.LogoutPath = "/Identity/Account/Logout";
        options.Events = new CookieAuthenticationEvents()
        {
          OnRedirectToLogin = context =>
          {
           //這裏區分當訪問/api 如果cookie過期那麼 不重定向到login登錄界面
            if (context.Request.Path.Value.StartsWith("/api"))
            {
              context.Response.Clear();
              context.Response.StatusCode = 401;
              return Task.FromResult(0);
            }
            context.Response.Redirect(context.RedirectUri);
            return Task.FromResult(0);
          }
        };
        //options.AccessDeniedPath = "/Identity/Account/AccessDenied";
      });        

startup.cs

下面userscontroller 認證方式

重點:我簡化了refreshtoken的實現方式,原本規範的做法是通過第一次登錄返回一個token和一個唯一的隨機生成的refreshtoken,下次token過期后需要重新發送過期的token和唯一的refreshtoken,同時後台還要比對這個refreshtoken是否正確,也就是說,第一次生成的refreshtoken必須保存到數據庫里,這裏我省去了這個步驟,這樣做是不嚴謹的的.

[ApiController]
  [Route("api/users")]
  public class UsersEndpoint : ControllerBase
  {
    private readonly ILogger<UsersEndpoint> _logger;
    private readonly ApplicationDbContext _context;
    private readonly UserManager<ApplicationUser> _manager;
    private readonly SignInManager<ApplicationUser> _signInManager;
    private readonly SmartSettings _settings;
    private readonly IConfiguration _config;

    public UsersEndpoint(ApplicationDbContext context,
      UserManager<ApplicationUser> manager,
      SignInManager<ApplicationUser> signInManager,
      ILogger<UsersEndpoint> logger,
      IConfiguration config,
      SmartSettings settings)
    {
      _context = context;
      _manager = manager;
      _settings = settings;
      _signInManager = signInManager;
      _logger = logger;
      _config = config;
    }
    [Route("authenticate")]
    [AllowAnonymous]
    [HttpPost]
    public async Task<IActionResult> Authenticate([FromBody] AuthenticateRequest model)
    {
      try
      {
        //Sign user in with username and password from parameters. This code assumes that the emailaddress is being used as the username. 
        var result = await _signInManager.PasswordSignInAsync(model.UserName, model.Password, true, true);

        if (result.Succeeded)
        {
          //Retrieve authenticated user's details
          var user = await _manager.FindByNameAsync(model.UserName);

          //Generate unique token with user's details
          var accessToken = await GenerateJSONWebToken(user);
          var refreshToken = GenerateRefreshToken();
          //Return Ok with token string as content
          _logger.LogInformation($"{model.UserName}:JWT登錄成功");
          return Ok(new { accessToken = accessToken, refreshToken = refreshToken });
        }
        return Unauthorized();
      }
      catch (Exception e)
      {
        return StatusCode(500, e.Message);
      }
    }
    [Route("refreshtoken")]
    [AllowAnonymous]
    [HttpPost]
    public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenRequest model)
    {
      var principal = GetPrincipalFromExpiredToken(model.AccessToken);
      var nameId = principal.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value;
      var user = await _manager.FindByNameAsync(nameId);
      await _signInManager.RefreshSignInAsync(user);

        //Retrieve authenticated user's details
        //Generate unique token with user's details
        var accessToken = await GenerateJSONWebToken(user);
        var refreshToken = GenerateRefreshToken();
        //Return Ok with token string as content
        _logger.LogInformation($"{user.UserName}:RefreshToken");
        return Ok(new { accessToken = accessToken, refreshToken = refreshToken });


    }

    private async Task<string> GenerateJSONWebToken(ApplicationUser user)
    {
      //Hash Security Key Object from the JWT Key
      var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
      var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

      //Generate list of claims with general and universally recommended claims
      var claims = new List<Claim>  {
           new Claim(ClaimTypes.NameIdentifier, user.UserName),
           new Claim(ClaimTypes.Name, user.UserName),
                new Claim(JwtRegisteredClaimNames.Sub, user.Email),
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                new Claim(ClaimTypes.NameIdentifier, user.Id),
                //添加自定義claim
                new Claim(ClaimTypes.GivenName, string.IsNullOrEmpty(user.GivenName) ? "" : user.GivenName),
                new Claim(ClaimTypes.Email, user.Email),
                new Claim("http://schemas.microsoft.com/identity/claims/tenantid", user.TenantId.ToString()),
                new Claim("http://schemas.microsoft.com/identity/claims/avatars", string.IsNullOrEmpty(user.Avatars) ? "" : user.Avatars),
                new Claim(ClaimTypes.MobilePhone, user.PhoneNumber)
      };
      //Retreive roles for user and add them to the claims listing
      var roles = await _manager.GetRolesAsync(user);
      claims.AddRange(roles.Select(r => new Claim(ClaimsIdentity.DefaultRoleClaimType, r)));
      //Generate final token adding Issuer and Subscriber data, claims, expriation time and Key
      var token = new JwtSecurityToken(_config["Jwt:Issuer"]
          , _config["Jwt:Issuer"],
          claims,
          null,
          expires: DateTime.Now.AddDays(30),
          signingCredentials: credentials
      );

      //Return token string
      return new JwtSecurityTokenHandler().WriteToken(token);
    }

    public string GenerateRefreshToken()
    {
      var randomNumber = new byte[32];
      using (var rng = RandomNumberGenerator.Create())
      {
        rng.GetBytes(randomNumber);
        return Convert.ToBase64String(randomNumber);
      }
    }

    private ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
    {
      var tokenValidationParameters = new TokenValidationParameters
      {
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_config["Jwt:Key"])),
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidIssuer = _config["Jwt:Issuer"],
        ValidAudience = _config["Jwt:Issuer"],
      };

      var tokenHandler = new JwtSecurityTokenHandler();
      SecurityToken securityToken;
      var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out securityToken);
      var jwtSecurityToken = securityToken as JwtSecurityToken;
      if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
      {
        throw new SecurityTokenException("Invalid token");
      }

      return principal;
    }
....
}
}

ControllerBase

下面是測試

獲取token

 refreshtoken

 

獲取數據

 

 這裏獲取數據的時候,其實可以不用填入token,因為調用authenticate或refreshtoken是已經記錄了cookie到客戶端,所以在postman測試的時候都可以不用加token也可以訪問

 推廣一下我的開源項目

基於領域驅動設計(DDD)超輕量級快速開發架構

https://www.cnblogs.com/neozhu/p/13174234.html

源代碼

https://github.com/neozhu/smartadmin.core.urf

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

【其他文章推薦】

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

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

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

※幫你省時又省力,新北清潔一流服務好口碑

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

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

Spring Boot 2.x基礎教程:Spring Data JPA的多數據源配置

上一篇我們介紹了在使用JdbcTemplate來做數據訪問時候的多數據源配置實現。接下來我們繼續學習如何在使用Spring Data JPA的時候,完成多數據源的配置和使用。

添加多數據源的配置

先在Spring Boot的配置文件application.properties中設置兩個你要鏈接的數據庫配置,比如這樣:

spring.datasource.primary.jdbc-url=jdbc:mysql://localhost:3306/test1
spring.datasource.primary.username=root
spring.datasource.primary.password=123456
spring.datasource.primary.driver-class-name=com.mysql.cj.jdbc.Driver

spring.datasource.secondary.jdbc-url=jdbc:mysql://localhost:3306/test2
spring.datasource.secondary.username=root
spring.datasource.secondary.password=123456
spring.datasource.secondary.driver-class-name=com.mysql.cj.jdbc.Driver

# 日誌打印執行的SQL
spring.jpa.show-sql=true
# Hibernate的DDL策略
spring.jpa.hibernate.ddl-auto=create-drop

這裏除了JPA自身相關的配置之外,與JdbcTemplate配置時候的數據源配置完全是一致的

說明與注意

  1. 多數據源配置的時候,與單數據源不同點在於spring.datasource之後多設置一個數據源名稱primarysecondary來區分不同的數據源配置,這個前綴將在後續初始化數據源的時候用到。
  2. 數據源連接配置2.x和1.x的配置項是有區別的:2.x使用spring.datasource.secondary.jdbc-url,而1.x版本使用spring.datasource.secondary.url。如果你在配置的時候發生了這個報錯java.lang.IllegalArgumentException: jdbcUrl is required with driverClassName.,那麼就是這個配置項的問題。

初始化數據源與JPA配置

完成多數據源的配置信息之後,就來創建個配置類來加載這些配置信息,初始化數據源,以及初始化每個數據源要用的JdbcTemplate。

由於JPA的配置要比JdbcTemplate的負責很多,所以我們將配置拆分一下來處理:

  1. 單獨建一個多數據源的配置類,比如下面這樣:
@Configuration
public class DataSourceConfiguration {

    @Primary
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }

}

可以看到內容跟JdbcTemplate時候是一模一樣的。通過@ConfigurationProperties可以知道這兩個數據源分別加載了spring.datasource.primary.*spring.datasource.secondary.*的配置。@Primary註解指定了主數據源,就是當我們不特別指定哪個數據源的時候,就會使用這個Bean真正差異部分在下面的JPA配置上。

  1. 分別創建兩個數據源的JPA配置。

Primary數據源的JPA配置:

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        entityManagerFactoryRef="entityManagerFactoryPrimary",
        transactionManagerRef="transactionManagerPrimary",
        basePackages= { "com.didispace.chapter38.p" }) //設置Repository所在位置
public class PrimaryConfig {

    @Autowired
    @Qualifier("primaryDataSource")
    private DataSource primaryDataSource;

    @Autowired
    private JpaProperties jpaProperties;
    @Autowired
    private HibernateProperties hibernateProperties;

    private Map<String, Object> getVendorProperties() {
        return hibernateProperties.determineHibernateProperties(jpaProperties.getProperties(), new HibernateSettings());
    }

    @Primary
    @Bean(name = "entityManagerPrimary")
    public EntityManager entityManager(EntityManagerFactoryBuilder builder) {
        return entityManagerFactoryPrimary(builder).getObject().createEntityManager();
    }

    @Primary
    @Bean(name = "entityManagerFactoryPrimary")
    public LocalContainerEntityManagerFactoryBean entityManagerFactoryPrimary (EntityManagerFactoryBuilder builder) {
        return builder
                .dataSource(primaryDataSource)
                .packages("com.didispace.chapter38.p") //設置實體類所在位置
                .persistenceUnit("primaryPersistenceUnit")
                .properties(getVendorProperties())
                .build();
    }

    @Primary
    @Bean(name = "transactionManagerPrimary")
    public PlatformTransactionManager transactionManagerPrimary(EntityManagerFactoryBuilder builder) {
        return new JpaTransactionManager(entityManagerFactoryPrimary(builder).getObject());
    }

}

Secondary數據源的JPA配置:

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        entityManagerFactoryRef="entityManagerFactorySecondary",
        transactionManagerRef="transactionManagerSecondary",
        basePackages= { "com.didispace.chapter38.s" }) //設置Repository所在位置
public class SecondaryConfig {

    @Autowired
    @Qualifier("secondaryDataSource")
    private DataSource secondaryDataSource;

    @Autowired
    private JpaProperties jpaProperties;
    @Autowired
    private HibernateProperties hibernateProperties;

    private Map<String, Object> getVendorProperties() {
        return hibernateProperties.determineHibernateProperties(jpaProperties.getProperties(), new HibernateSettings());
    }

    @Bean(name = "entityManagerSecondary")
    public EntityManager entityManager(EntityManagerFactoryBuilder builder) {
        return entityManagerFactorySecondary(builder).getObject().createEntityManager();
    }

    @Bean(name = "entityManagerFactorySecondary")
    public LocalContainerEntityManagerFactoryBean entityManagerFactorySecondary (EntityManagerFactoryBuilder builder) {
        return builder
                .dataSource(secondaryDataSource)
                .packages("com.didispace.chapter38.s") //設置實體類所在位置
                .persistenceUnit("secondaryPersistenceUnit")
                .properties(getVendorProperties())
                .build();
    }

    @Bean(name = "transactionManagerSecondary")
    PlatformTransactionManager transactionManagerSecondary(EntityManagerFactoryBuilder builder) {
        return new JpaTransactionManager(entityManagerFactorySecondary(builder).getObject());
    }

}

說明與注意

  • 在使用JPA的時候,需要為不同的數據源創建不同的package來存放對應的Entity和Repository,以便於配置類的分區掃描
  • 類名上的註解@EnableJpaRepositories中指定Repository的所在位置
  • LocalContainerEntityManagerFactoryBean創建的時候,指定Entity所在的位置
  • 其他主要注意在互相注入時候,不同數據源不同配置的命名,基本就沒有什麼大問題了

測試一下

完成了上面之後,我們就可以寫個測試類來嘗試一下上面的多數據源配置是否正確了,比如下面這樣:

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class Chapter38ApplicationTests {

    @Autowired
    private UserRepository userRepository;
    @Autowired
    private MessageRepository messageRepository;

    @Test
    public void test() throws Exception {
        userRepository.save(new User("aaa", 10));
        userRepository.save(new User("bbb", 20));
        userRepository.save(new User("ccc", 30));
        userRepository.save(new User("ddd", 40));
        userRepository.save(new User("eee", 50));

        Assert.assertEquals(5, userRepository.findAll().size());

        messageRepository.save(new Message("o1", "aaaaaaaaaa"));
        messageRepository.save(new Message("o2", "bbbbbbbbbb"));
        messageRepository.save(new Message("o3", "cccccccccc"));

        Assert.assertEquals(3, messageRepository.findAll().size());
    }

}

說明與注意

  • 測試驗證的邏輯很簡單,就是通過不同的Repository往不同的數據源插入數據,然後查詢一下總數是否是對的
  • 這裏省略了Entity和Repository的細節,讀者可以在下方代碼示例中下載完整例子對照查看

代碼示例

本文的相關例子可以查看下面倉庫中的chapter3-8目錄:

  • Github:https://github.com/dyc87112/SpringBoot-Learning/
  • Gitee:https://gitee.com/didispace/SpringBoot-Learning/

如果您覺得本文不錯,歡迎Star支持,您的關注是我堅持的動力!

相關閱讀

  • Spring Boot 1.x基礎教程:多數據源配置

本文首發:Spring Boot 2.x基礎教程:Spring Data JPA的多數據源配置,轉載請註明出處。
歡迎關注我的公眾號:程序猿DD,獲得獨家整理的學習資源和日常乾貨推送。
如果您對我的其他專題內容感興趣,直達我的個人博客:didispace.com。

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

【其他文章推薦】

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

新北清潔公司,居家、辦公、裝潢細清專業服務

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

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

※超省錢租車方案

FB行銷專家,教你從零開始的技巧

手把手教你基於SqlSugar4編寫一個可視化代碼生成器(生成實體,以SqlServer為例,文末附源碼)

  在開發過程中免不了創建實體類,字段少的表可以手動編寫,但是字段多還用手動創建的話不免有些浪費時間,假如一張表有100多個字段,手寫有些不現實。

這時我們會藉助一些工具,如:動軟代碼生成器、各種ORM框架自帶的代碼生成器等等,都可以使用。

我們現在就基於SqlSugar(ORM框架)自己動手製造一個輪子,以SqlServer為例。我們先看一下成品效果,

 

使用流程:

  配置好數據庫鏈接,點擊【鏈接數據庫】獲取指定服務器上的數據庫名,點擊數據庫名,動態獲取數據庫下面的所有表,

點擊數據表,如果生成過了的會自動獲取生成的實體,如果沒有生成過,點擊【生成實體】自動生成显示,直接複製即可使用。

注:server=xxx.xxx.x.xxx這裏如果是本地沒有配置的話直接server=.即可。

 

 

 

 

開發環境:

編譯器:Visual Studio 2017

運行環境:windows7 x64

數據庫:SqlServer2012

 

代碼實現步驟:

一、創建一個ASP.NET Web應用,命名為GenerateEntity

 

 

 

 

 

 

二、應用SqlSugar動態鏈接庫

 

 

 

三、編寫代碼

這裏分為前端和後端,前端頁面展示,後端後台邏輯(注:由於我們是代碼展示,所以就不搞三層架構、工廠模式這些,直接在控制器中完成,有需要的同學可以根據項目需求進行更改

內部實現邏輯:

  • 在頁面上配置數據庫鏈接,點擊【鏈接數據庫】按鈕獲取指定數據庫的所有數據庫名显示在左邊;
  • 點擊左邊的數據庫名稱,動態獲取指定數據庫下面所有的表显示出來;
  • 點擊表名,生成過的就显示生成的實體,沒有的則點擊【生成實體】按鈕生成(支持生成單表和數據庫表全部生成);

這裏我直接貼出代碼,直接拷貝即可使用:

前端html頁面

@{
    ViewBag.Title = "Home Page";
}

<script src="~/Scripts/jquery-3.3.1.js"></script>

<div style="margin-top:10px;font-family:'Microsoft YaHei';font-size:18px; ">
    <div style="height:100px;width:100%;border:1px solid gray;padding:10px">
        <div>
            <span>鏈接數據庫:</span>
            <input style="width:800px;max-width:800px;" id="Link" value="server=xxx.xxx.x.xxx;uid=sa;pwd=xxx" />
            <a href="javascript:void(0)" onclick="LinkServer()">鏈接數據庫</a>
        </div>
        <div style="margin-top:10px">
            <span>數據庫名:</span>
            <input style="color:red;font-weight:600" id="ServerName" />

            <span>表名:</span>
            <input style="color:red;font-weight:600" id="TableName" />

            <span>生成類型:</span>
            <select id="type">
                <option value="0">生成單個表</option>
                <option value="1">生成所有表</option>
            </select>
            <a  href="javascript:void(0)" onclick="GenerateEntity()" style="margin-left:20px;font-weight:600;">生成實體</a>
            <br />

        </div>
    </div>
    <div style="height:720px;width:100%;">
        <div style="height:100%;width:40%;float:left; border:1px solid gray;font-size:20px">

            <div id="leftserver" style="float:left;border:1px solid gray;height:100%;width:40%;padding:10px;overflow: auto;">

            </div>
            <div id="lefttable" style="float:left;border:1px solid gray;height:100%;width:60%;padding:10px;overflow: auto;">

            </div>
        </div>
        <div  style="height:100%;width:60%;float:left;border:1px solid gray;overflow: auto;">
            <textarea style="width:100%;height:100%;max-width:10000px" id="righttable"></textarea>
        </div>
    </div>
</div>

<script type="text/javascript">

    //鏈接數據庫
    function LinkServer() {
        $.ajax({
            url: "/Home/LinkServer",
            data: { Link: $("#Link").val() },
            type: "POST",
            async: false,
            dataType: "json",
            success: function (data) {
                if (data.res) {
                    if (data.info != "") {
                        $("#leftserver").html("");
                        var leftserver = "<span>數據庫名</span><hr />";
                        var info = eval("(" + data.info + ")");
                        for (var i = 0; i < info.length; i++) {
                            leftserver += "<a onclick=\"leftserver('" + info[i].Name + "')\">" + info[i].Name + "</a><br />";
                        }

                        $("#leftserver").html(leftserver);
                    }
                }
                else {
                    alert(data.msg);
                }
            }
        });
    }

    //查詢指定數據庫的表
    function leftserver(Name) {
        $("#ServerName").val(Name)
        $.ajax({
            url: "/Home/GetTable",
            data: { Link: $("#Link").val(), Name: Name },
            type: "POST",
            async: false,
            dataType: "json",
            success: function (data) {
                if (data.res) {
                    if (data.info != "") {
                        $("#lefttable").html("");
                        var lefttable = "<span>表名</span><hr />";
                        var info = eval("(" + data.info + ")");
                        for (var i = 0; i < info.length; i++) {
                            lefttable += "<a onclick=\"lefttable('" + info[i].Name + "')\">" + info[i].Name + "</a><br />";
                        }

                        $("#lefttable").html(lefttable);
                    }
                }
                else {
                    alert(data.msg);
                }
            }
        });
    }

    //查詢指定數據庫的表
    function lefttable(Name) {
        $("#TableName").val(Name);
        $.ajax({
            url: "/Home/GetGenerateEntity",
            data: { TableName: Name },
            type: "POST",
            async: false,
            dataType: "json",
            success: function (data) {
                if (data.res) {
                    document.getElementById("righttable").innerHTML = data.info;
                }
                else {
                    alert(data.msg);
                }
            }
        });
    }

    //生成實體
    function GenerateEntity() {

        $.ajax({
            url: "/Home/GenerateEntity",
            data: {
                Link: $("#Link").val(),
                Name: $("#ServerName").val(),
                TableName: $("#TableName").val(),
                type: $("#type").val()
            },
            type: "POST",
            async: false,
            dataType: "json",
            success: function (data) {
                if (data.res) {
                    document.getElementById("righttable").innerHTML = data.info;
                }
                else {
                    alert(data.msg);
                }
            }
        });
    }

</script>

 

後端控制器數據

using SqlSugar;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Mvc;

namespace GenerateEntity.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        public ActionResult About()
        {
            ViewBag.Message = "Your application description page.";

            return View();
        }

        public ActionResult Contact()
        {
            ViewBag.Message = "Your contact page.";

            return View();
        }


     
        //鏈接數據庫
        public JsonResult LinkServer(string Link)
        {
            ResultInfo result = new ResultInfo();
            try
            {
                //配置數據庫連接
                SqlSugarClient db = new SqlSugarClient(
                                    new ConnectionConfig()
                                    {
                                        ConnectionString = ""+ Link + ";database=master",
                                        DbType = DbType.SqlServer,//設置數據庫類型
                                    IsAutoCloseConnection = true,//自動釋放數據務,如果存在事務,在事務結束后釋放
                                    InitKeyType = InitKeyType.Attribute //從實體特性中讀取主鍵自增列信息
                                });
                string sql = @"SELECT top 100000 Name FROM Master..SysDatabases ORDER BY Name";  //查詢所有鏈接的所有數據庫名
                var strList = db.SqlQueryable<databaseName>(sql).ToList();
                result.info = Newtonsoft.Json.JsonConvert.SerializeObject(strList);
                result.res = true;
                result.msg = "鏈接成功!";
            }
            catch (Exception ex)
            {
                result.msg = ex.Message;
            }

            return Json(result, JsonRequestBehavior.AllowGet);
        }

        //根據數據庫名查詢所有表
        public JsonResult GetTable(string Link,string Name)
        {

            ResultInfo result = new ResultInfo();
            try
            {
                //配置數據庫連接
                SqlSugarClient db = new SqlSugarClient(
                                    new ConnectionConfig()
                                    {
                                        ConnectionString = "" + Link + ";database="+ Name + "",
                                        DbType = DbType.SqlServer,//設置數據庫類型
                                        IsAutoCloseConnection = true,//自動釋放數據務,如果存在事務,在事務結束后釋放
                                        InitKeyType = InitKeyType.Attribute //從實體特性中讀取主鍵自增列信息
                                    });

                string sql = @"SELECT top 10000 Name FROM SYSOBJECTS WHERE TYPE='U' ORDER BY Name";  //查詢所有鏈接的所有數據庫名
                var strList = db.SqlQueryable<databaseName>(sql).ToList();
                result.info = Newtonsoft.Json.JsonConvert.SerializeObject(strList);
                result.res = true;
                result.msg = "查詢成功!";
            }
            catch (Exception ex)
            {
                result.msg = ex.Message;
            }

            return Json(result, JsonRequestBehavior.AllowGet);
        }

        //生成實體
        public JsonResult GenerateEntity(string Link, string Name,string TableName,string type)
        {

            ResultInfo result = new ResultInfo();
            try
            {
                //配置數據庫連接
                SqlSugarClient db = new SqlSugarClient(
                                    new ConnectionConfig()
                                    {
                                        ConnectionString = "" + Link + ";database=" + Name + "",
                                        DbType = DbType.SqlServer,//設置數據庫類型
                                        IsAutoCloseConnection = true,//自動釋放數據務,如果存在事務,在事務結束后釋放
                                        InitKeyType = InitKeyType.Attribute //從實體特性中讀取主鍵自增列信息
                                    });

                string path = "C:\\Demo\\2";

                if (type == "0")
                {
                    path = "C:\\Demo\\2";
                    db.DbFirst.Where(TableName).CreateClassFile(path);
                    result.info = System.IO.File.ReadAllText(@"" + path + "\\" + TableName + ".cs" + "", Encoding.UTF8);
                }
                else if (type == "1")
                {
                    path = "C:\\Demo\\3";
                    db.DbFirst.IsCreateAttribute().CreateClassFile(path);
                    result.info = "";
                }

                
                
                result.res = true;
                result.msg = "生成成功!";
            }
            catch (Exception ex)
            {
                result.msg = ex.Message;
            }

            return Json(result, JsonRequestBehavior.AllowGet);
        }

        //生成全部表時查看
        public JsonResult GetGenerateEntity(string TableName)
        {

            ResultInfo result = new ResultInfo();
            try
            {
                string path = "C:\\Demo\\3";
                result.info = System.IO.File.ReadAllText(@"" + path + "\\" + TableName + ".cs" + "", Encoding.UTF8);
                result.res = true;
                result.msg = "查詢成功!";
            }
            catch (Exception ex)
            {
                result.msg = ex.Message;
                try
                {
                    if (result.msg.Contains("未能找到文件"))
                    {
                       string path = "C:\\Demo\\2";
                        result.info = System.IO.File.ReadAllText(@"" + path + "\\" + TableName + ".cs" + "", Encoding.UTF8);
                        result.res = true;
                        result.msg = "查詢成功!";
                    }
                }
                catch (Exception)
                {
                    result.msg = ex.Message;
                }
            }

            return Json(result, JsonRequestBehavior.AllowGet);
        }

        //數據庫名
        public class databaseName
        {
            public string Name { get; set; }
        }

        //封裝返回信息數據
        public class ResultInfo
        {
            public ResultInfo()
            {
                res = false;
                startcode = 449;
                info = "";
            }
            public bool res { get; set; }  //返回狀態(true or false)
            public string msg { get; set; }  //返回信息
            public int startcode { get; set; }  //返回http的狀態碼
            public string info { get; set; }  //返回的結果(res為true時返回結果集,res為false時返回錯誤提示)
        }

    }
}

 

 

 

這樣一套可視化代碼生成器就出來了,我們把他發布到IIS上面,然後設置為瀏覽器標籤(收藏),這樣就可以快捷使用了。

我們運行一下看看,是不是感覺很方便呀!

 

 

 

歡迎關注訂閱我的微信公眾平台【熊澤有話說】,更多好玩易學知識等你來取
作者:熊澤-學習中的苦與樂
公眾號:熊澤有話說
出處: https://www.cnblogs.com/xiongze520/p/13181241.html
創作不易,版權歸作者和博客園共有,轉載或者部分轉載、摘錄,請在文章明顯位置註明作者和原文鏈接。  

 

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

【其他文章推薦】

新北清潔公司,居家、辦公、裝潢細清專業服務

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

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

※超省錢租車方案

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

網頁設計最專業,超強功能平台可客製化

程序員如何高效學Python,如何高效用Python掙錢

    本人在1年半之前,不熟悉Python(不過有若干年Java開發基礎),由於公司要用Python,所以學習了一通。現在除了能用Python做本職工作外,還出了本Python書,《基於股票大數據分析的Python入門實戰 視頻教學版》,京東鏈接:https://item.jd.com/69241653952.html,還在某網站錄製了視頻課,後面還有其它線上線下課的機會。

    

    本人的感受是,哪怕上班用不到Python,程序員也應該學Python,因為Python能給大家帶來更多的主業副業機會,而且現在做Python的人還沒Java多。在本文里將結合本人的經驗,一方面分享下如何高效學Python,另一方面分享下用Python掙錢的經驗。

1 先說Python的尷尬地位,首先要明確掙錢方式

    尷尬體現在哪裡?一些大廠雖然有專門做Python的高薪崗位,但一般會直接找些深度學習機器學習方向的碩士博士,而且是要名校的,而小公司一般限於成本的原因,無法直接設置單做Python的崗位,而且也就是做些技術含量較低的應用, 比如做個爬蟲或者簡單調個機器學習的庫,所以一般是讓做其它語言的人順帶做掉。

    而且Python用庫和方法的形式包裝掉了一些很複雜的算法,程序員一般只需要調用方法就可以實現基本的機器學習和深度學習之類的活,而在大廠里高薪的Python崗,絕非是簡單地調用Python,而是需要深入了解算法,從而根據業務定製模型,所以一般社會上的程序員很難通過自學,達到大廠里高薪Python程序員的標準。

    總之,你在工作后通過自學Python,未必能達到大廠高薪職位的標準,因為由於你數學基礎不行,未必能深入算法,而一般公司也不會單獨開設Python崗位,所以對應地,學Python之前大家應該明確靠Python的掙錢方式。

    1 主業上,還得以Java等語言為主,但如果你能在簡歷和面試中證明自己很精通一般的Python爬蟲、數據分析和機器學習等方面的應用,絕對能幫你更好地找到工作,並且個人提升也會很快。

    2 雖然Python底層包含的深度學習等方面的算法很難,但用Python做案例並不難,大家可以通過Python做些副業的活。

2 再說Python該怎麼學,該學哪些技能?

    第一步,了解Python的基本語法,比如集合,讀寫文件,讀寫數據庫和異常處理等,如果大家有Java等語言的開發基礎,這塊很簡單,本人也就用了2個星期。但正是因為簡單,所以這些技能很不值錢,別人學起來也快。

    第二步,了解數據分析三劍客,具體來說就是Numpy, Pandas和matplotlib,用Numpy和Pandas清洗數據,用Pandas的DataFrame存儲數據,再用matplotlib繪製柱狀圖餅圖之類的圖形。

    第三步,了解爬蟲技能,這裏除了需要了解自帶的urllib庫之外,還需要了解一種框架,比如Scrapy,需要到能根據需求定製爬蟲代碼的程序。

    其實數據分析和爬蟲相關的語法技能,也不複雜,本人用1個半月也就達到能幹活的程度了,相信大家應該更快。而且,學到這種程序,應該就可以去做些案例以此掙錢了,比如寫分析xx網站的案例,錄成視頻去賣了,而且也能完成公司里大多數數據獲取和數據分析的功能需求了。

    第四步去了解機器學習庫,具體而言就去學習sklearn庫,這個庫里不僅包含了線性回歸嶺回歸和SVM分類等機器學習算法,還包含了波士頓房價、鳶尾花和手寫體識別等的數據集,而且由於已經包裝了相關算法,用sklearn庫學習機器學習的過程並不難,不需要過多的數學知識。學好這個庫,外帶結合爬蟲和數據分析的技能,就更在某個領域幹活掙錢了,比如本人在股票分析領域出了本書,並且也出了些視頻,後繼還可以繼續深入股票量化分析領域。

    第五步,可以去了解深度學習,無非是人工神經網絡,自然語言分析,圖像識別等,這方面雖然包含的數學知識更複雜,但由於也經過包裝,所以直接用接口也不難。這方面學好以後,雖然說高不成低不就,即沒法進大廠,同時小公司也用不到,但用這些知識準備些案例,出書講課錄視頻,甚至做企業培訓,還是能帶來一定的收益的。

    在學上述知識的時候,千萬不能只學語法,因為沒用,一定得結合實例,同時把這些知識變現的時候,也不能單講語法,也是要準備若干案例,比如像我這樣的股票分析,或者是scrapy+數據分析+深度學習的xx網站數據分析案例,這些技能雖然很高大上,但其實做到調用接口實踐案例的程度就能掙錢,如果再有機緣以此進入大廠,那就真的前途無量了。

3 可以先從公眾號做起

    之前講的是如何學,學什麼,這裏就開始講如何掙錢。當然最簡單的就是建個公眾號,在上面發文,吸引粉絲,這個門檻相對低。

    但注意如果僅僅發表入門級的文章,比如numpy庫怎麼用,怎麼用matplotlib庫繪製基本圖形,這絕對不夠,因為此類文章太多,哪些文章能吸引人?

    1 綜合應用類,比如scrapy+數據分析三劍客。

    2 實戰案例類,比如用scrapy爬個網站數據,然後分析。

    3 專業領域類,比如量化分析股票,分析房價等。

    4 深度學習機器學習這些領域現在還很火,這些領域如果把某個算法通俗易懂地講透也行,或者這些方面給寫案例,比如用自然語言分析技術分析某網站的評論等。

    如果能定期發表此類文章,公眾號一定能聚集到不少粉絲,同樣也可以做視頻的up主。

4 更可以出本屬於你的書

    如何讓別人認為你是python某個領域的大牛?要麼有大廠架構師職位加持,這不是每個人都能達到,或者是著名博主公眾號主,但似乎這也需要經歷來積澱,不過如果你在python數據分析和機器學習等方面出本書,那說服力自然就上來了。

    出書可以偏重案例,比如講爬蟲數據分析的書,在合法的前提下給出爬取分析若干知名網頁的案例,如果講機器學習的書,甚至可以結合sklearn庫自帶的數據集,講清楚常用算法的案例應用,如果有時間有機會,我甚至打算再出版本基於python股票量化的書。

    相對而言,寫一本講述包括語法、結合小案例講(機器學習等)庫的用法和結合綜合案例講機器學習算法和數據分析綜合應用的書,並不難。對於一個有5年開發經驗的程序員而言,從零基礎積累個半年,就完全可以達到出書的地步,如果資歷稍微弱些,只有2,3年開發經歷,估計學個1年也應該可以達到出書的地步。

    還是這句話,出書掙的錢不多,但絕對能證明你在python某個領域的能力,小到聯繫副業,大到以此找工作,一定能幫到你。具體操作的話,可以直接在清華出版社,机械工業出版社,人民郵電出版社和电子工業出版社的官網找聯繫方式,然後直接和編輯溝通,至於一些有中介性質的圖書公司,大家自己看着辦。

5 也可以做其它副業

    包括到各大視頻網站去錄製數據分析、爬蟲、機器學習和深度學習等方面的系列課,也可以找你所在城市的線下培訓班去講課,如果你有相關大公司背景,有自己的書,或者業內知名,你就可以聯繫些做企業培訓的公司。這樣做下個半年後,月入1萬應該不是問題。

    在做各種副業的時候,一般來說也是要偏重案例,比如你有若干個深度學習的案例外帶相關算法的說明,再加上些好的文案,應該很能吸引人。當然也可以直接找項目做,目前python方面比較熱門的項目可能還是用爬蟲,但這塊做的時候就要非常慎重了,不能做違法的事情,而當前用到深度學習機器學習技術的項目倒不多,可能因為這些應用更集中在大公司吧。

6 總結:下班后不能總放鬆,更得找點事干

    說實話,python方面的活,哪怕門檻最低的做公眾號,要做好也不簡單,更何況出書了。至於,自己聯繫平台出視頻,或者做線下培訓或者做項目,就不僅得靠技術,更得靠人脈了,經營這類活需要的時間更多。

    不過掙錢拿有容易的,況且,如果下班后總是看手機或者混日子,可能一天天就很快過去了,與其下班做些沒收益的消遣,還不如學些python幹些活,這樣多少好歹也有收益,或者指不定無心插柳柳成蔭,你經營python一段時間后,或者真就以此進了大廠,或者也通過各種途徑成為業內知名人事,拓展了不少副業渠道,也算是不負好時光吧。

    感謝大家看完此文,如果感覺有一定道理,請點贊此文。如果要轉載,也請全文轉載,別刪節本人辛苦寫成的文章。

 

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

大文件上傳、斷點續傳、秒傳、beego、vue

大文件上傳

0、項目源碼地址

源碼地址 :https://github.com/zhuchangwu/large-file-upload

前端基於 vue-simple-uploader (感謝這個大佬)實現: https://github.com/simple-uploader/vue-uploader/blob/master/README_zh-CN.md

vue-simple-uploader底層封裝了uploader.js : https://github.com/simple-uploader/Uploader/blob/develop/README_zh-CN.md

1、如何唯一標識一個文件?

文件的信息後端會存儲在mysql數據庫表中。

在上傳之前,前端通過 spark-md5.js 計算文件的md5值以此去唯一的標示一個文件。

spark-md5.js 地址:https://github.com/satazor/js-spark-md5

README.md中有spark-md5.js的使用demo,可以去看看。

2、斷點續傳是如何實現的?

斷點續傳可以實現這樣的功能,比如用戶上傳200M的文件,當用戶上傳完199M時,斷網了,有了斷點續傳的功能,我們允許RD再次上傳時,能從第199M的位置重新上傳。

實現原理:

實現斷點續傳的前提是,大文件切片上傳。然後前端得問後端哪些chunk曾經上傳過,讓前端跳過這些上傳過的chunk就好了。

前端的上傳器(uploader.js)在上傳時會先發送一個GET請求,這個請求不會攜帶任何chunk數據,作用就是向後端詢問哪些chunk曾經上傳過。 後端會將這些數據保存在mysql數據庫表中。比如按這種格式:1:2:3:5表示,曾經上傳過的分片有1,2,3,5。第四片沒有被上傳,前端會跳過1,2,3,5。 僅僅會將第四個chunk發送給後端。

3、秒傳是如何實現的?

秒傳實現的功能是:當RD重複上傳一份相同的文件時,除了第一次上傳會正常發送上傳請求后,其他的上傳都會跳過真正的上傳,直接显示秒成功。

實現方式:

後端存儲着當前文件的相關信息。為了實現秒傳,我們需要搞一個字段(isUploaded)表示當前md5對應的文件是否曾經上傳過。 後端在處理 前端的上傳器(uploader.js)發送的第一個GET請求時,會將這個字段發送給前端,比如 isUploaded = true。前端看到這個信息后,直接跳過上傳,显示上傳成功。

4、上傳暫停是如何實現的?

上傳的暫停:並不是去暫停一個已經發送出去的正在進行數據傳輸的http請求~

而是暫停發送起發送下一個http請求。

就我們的項目而言,因為我們的文件本來就是先切片,對於我們來說,暫停文件的上傳,本質上就是暫停發送下一個chunk。

5、前端上傳併發數是多少?

前端的uploader.js中默認會三條線程啟動併發上傳,前端會在同一時刻併發 發送3個chunk,後端就會相應的為每個請求開啟三個協程處理上傳的過來的chunk。

在我們的項目中,會將前端併發數調整成了1。原因如下:

因為考慮到了斷點續傳的實現,後端需要記錄下曾經上傳過哪些切片。(這個記錄在mysql的數據庫表中,以 ”1:2:3:4:5“ )這種格式記錄。

Mysql5.7默認的存儲引擎是innoDB,默認的隔離級別是RR。如果我們將前端的併發數調大,就會出現下面的異常情況:

1. goroutine1 獲取開啟事物,讀取當前上傳到記錄是 1:2 (未提交事物)
2. goroutine1 在現有的記錄上加上自己處理的分片3,並和現有的1:2拼接在一起成1:2:3 (未提交事物)
3. goroutine2 獲取開啟事物,(因為RR,所以它讀不到1:2:3)讀取當前上傳到記錄是 1:2 (未提交事物)
4. goroutine1 提交事物,將1:2:3寫回到mysql
5. goroutine2 在現有的記錄上加上自己處理的分片4,並和現有的1:2拼接在一起成1:2:4 (提交事物)

可以看到,如果前端併發上傳,後端就會出現分片丟失的問題。 故前端將併發數置為1。

6、單個chunk上傳失敗怎麼辦?

前端會重傳chunk?

由於網絡問題,或者時後端處理chunk時出現的其他未知的錯誤,會導致chunk上傳失敗。

uploaded.js 中有如下的配置項, 每次uploader.js 在上傳每一個切片實際上都是在發送一次post請求,後端根據這個post請求是會給前端一個狀態嗎。 uploader.js 就是根據這個狀態碼去判斷是失敗了還是成功了,如果失敗了就會重新發送這個上傳的請求。

那uploader.js是如何知道有哪些狀態嗎是它應該重傳chunk的標記呢? 看看下面uploader.js需要的options 就明白了,其中的permantErrors中配置的狀態碼標示:當遇到這個狀態碼時整個上傳直接失敗~

successStatuses中配置的狀態碼錶示chunk是上傳成功的~。 其他的狀態嗎uploader.js 就會任務chunk上傳的有問題,於是重新上傳~

        options: {
          target: 'http://localhost:8081/file/upload',
          maxChunkRetries: 3,
          permanentErrors:[502], // 永久性的上傳失敗~,會認為整個文件都上傳失敗了
          successStatuses:[200], // 當前chunk上傳成功后的狀態嗎
          ...
        }

7、超過重傳次數后,怎麼辦?

比如我們設置出錯后重傳的次數為3,那麼無論當前分片是第幾片,整個文件的上傳狀態被標記為false,這就意味着會終止所有的上傳。

肯定不會出現這種情況:chunk1重傳3次后失敗了,chunk2還能再去上傳,這樣的話數據肯定不一致了。

8、如何控制上傳多大的文件?

目前了解到nginx端的限制上單次上傳不能超過1M。

前端會對大文件進行切片突破nginx的限制。

        options: {
          target: 'http://localhost:8081/file/upload',
          chunkSize: 512000, // 單次上傳 512KB 
        }     

如果後續和nginx負責的同學達成一致,可以把這個值進行調整。前端可以後續將這個chunk的閾值加大。

9、如何保證上傳文件的百分百正確?

在上傳文件前,前端會計算出當前RD選擇的這個文件的 md5 值。

當後端檢測到所有的分片全部上傳完畢,這時會merge所有分片匯聚成單個文件。計算這個文件的md5 同 RD在前端提供的文件的md5值比對。 比對結果一致說明RD正確的完成了上傳。結果不一致,說明文件上傳失敗了~返回給前端任務失敗,提示RD重新上傳。

10、其他細節問題:

如何判斷文件上傳失敗了,給RD展示紅色?

如何控制上傳什麼類型的文件?

如何控制不能上傳空文件?

上面說過了,當 uploader.js 遇到了permanentErrors這種狀態碼時會認為文件上傳失敗了。

前端想在上傳失敗后,將進度條轉換成紅色,其實改一下CSS樣式就好了,問題就在於,根據什麼去修改?在哪裡去修改?

前端會將每一個file封裝成一個組件:如下圖中的files就是file的集合

整個的fileList會將會被渲染成下面這樣。

我們上傳的文件被vue-simple-uploader的作者封裝成一個file.vue組件,這個對象中會有個配置參數, 比如它會長下面這樣。

     options: {
        target: 'http://localhost:8081/file/upload',
        statusText: {
          success: '上傳成功',
          error: '上傳出錯,請重試',
          typeError: '暫不支持上傳您添加的文件格式',
          uploading: '上傳中',
          emptyError:'不能上傳空文件',
          paused: '請確認文件後點擊上傳',
          waiting: '等待中'
        }
      }
    },

我們將上面的配置添加給Uploader.js

      const uploader = new Uploader(this.options)

在file組件中有如下計算屬性的,分別是status和statusText

    computed: {
      // 計算出一個狀態信息
      status () {
        const isUploading = this.isUploading // 是否正在上傳
        const isComplete = this.isComplete // 是否已經上傳完成
        const isError = this.error // 是否出錯了
        const isTypeError = this.typeError // 是否出錯了
        const paused = this.paused // 是否暫停了
        const isEmpty = this.emptyError // 是否暫停了
        // 哪個屬性先不為空,就返回哪個屬性
        if (isComplete) {
          return 'success'
        } else if (isError) {
          return 'error'
        } else if (isUploading) {
          return 'uploading'
        } else if (isTypeError) {
          return 'typeError'
        } else if (isEmpty) {
          return 'emptyError'
        } else if (paused) {
          return 'paused'
        } else {
          return 'waiting'
        }
      },
      // 狀態文本提示信息
      statusText () {
        // 獲取到計算出的status屬性(相當於是個key,具體的值在下面的fileStatusText中獲取到)
        const status = this.status
        // 從file的uploader對象中獲取到 fileStatusText,也就是用自己定義的名字
        const fileStatusText = this.file.uploader.fileStatusText
        let txt = status
        if (typeof fileStatusText === 'function') {
          txt = fileStatusText(status, this.response)
        } else {
          txt = fileStatusText[status]
        }
        return txt || status
      },
    },

status綁定在html上

	<div class="uploader-file" :status="status">

對應的CSS樣式入下:

  .uploader-file[status="error"] .uploader-file-progress {
    background: #ffe0e0;
  }

綜上:有了上面代碼的編寫,我們可以直接像下面這樣控制就好了

  file.typeError = true // 表示文件的類型不符合我們的預期,不允許RD上傳
  file.error = true // 表示文件上傳失敗了
  file.emptyError = true // 表示文件為空,不允許上傳

11、後端數據庫表設計

CREATE TABLE `file_upload_detail` (                                                                               
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',                                                           
  `username` varchar(64) NOT NULL COMMENT '上傳文件的用戶賬號',                                                            
  `file_name` varchar(64) NOT NULL COMMENT '上傳文件名',                                                               
  `md5` varchar(255) NOT NULL COMMENT '上傳文件的MD5值',                                                                
  `is_uploaded` int(11) DEFAULT '0' COMMENT '是否完整上傳過 \n0:否\n1:是',                                                 
  `has_been_uploaded` varchar(1024) DEFAULT NULL COMMENT '曾經上傳過的分片號',                                             
  `url` varchar(255) DEFAULT NULL COMMENT 'bos中的url,或者是本機的url地址',                                                 
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP  COMMENT '本條記錄創建時間',     
  `update_time` timestamp NULL DEFAULT NULL  COMMENT '本條記錄更新時間',                                                  
  `total_chunks` int(11) DEFAULT NULL COMMENT '文件的總分片數',                                                          
  PRIMARY KEY (`id`)                                                                                              
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8                                                             

12、關於什麼時候mergechunk

在本文中給出的demo中,merge是後端處理完成所有的chunk后,像前端返回 merge=1,這個表示來實現的。

前端拿着這個字段去發送/merge請求去合併所有的chunk。

值得注意的地方是:這個請求是在uploader.js認為所有的分片全部成功上傳后,在單個文件成功上傳的回調中執行的。我想了一下,感覺這麼搞其實不太友好,萬一merge的過程中失敗了,或者是某個chunk丟失了,chunk中的數據缺失,最終merge的產物的md5值其實並不等於原文件。當這種情況發生的時候,其實上傳是失敗的。但是後端既然告訴uploader.js 可以合併了,說明後端的upload函數認為任務是成功的。vue-simple-uploader上傳完最後一個chunk得到的狀態碼是200,它也會覺得任務是成功的,於是在前端段展示綠色的上傳成功給用戶看~(然而上傳是失敗的), 這麼看來,整個過程其實控制的不太好~

我現在的實現:直接幹掉merge請求,前端1條線程發送請求,將chunk依次發送到後端。後端檢測到所有的chunk都上傳過來後主動merge,merge完成后馬上校驗文件的md5值是否符合預期。這個處理過程在上傳最後一個chunk的請求中進行,因此可以實現的控制前端上傳成功還是失敗的樣式~
如果偏偏想追求極致的速度,可以考慮將後端更新isUpload字段的SQL換成 “select for update” 他可以鎖住你要更新的數據行
以及這一行上下的間隙,這樣就不會出現併發修改異常。前端也可以重新更換成多線程併發上傳的機制。理論上只要網絡帶寬允許你開啟五條線程,速度就快5倍。至於什麼時候merge,加個if判斷一下,當上傳過的分片數 == totalChunks 就可以merge了。

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

FB行銷專家,教你從零開始的技巧