前面我們完成了一個網頁端的上傳oss程序:https://blog.csdn.net/daily886/article/details/103366145
現在我們把前後端分離
前端使用walk開發,window界面
後臺服務器使用go接收圖片並上傳到oss ,服務器端操作流程:https://blog.csdn.net/daily886/article/details/103409400
客戶端目錄結構如下:
項目根目錄是 goossclient
goossclient的目錄結構
├── conf # 配置文件統一存放目錄
│ ├── config.yaml # 配置文件
├── handler # 控制輸出到瀏覽器
│ ├── handler.go
├── model # 操作模型
│ ├── windowdialog.go # window界面操作模型
├── goossclient.manifest # walk需要使用的 manifest 文件
├── main.go # Go程序唯一入口
下面,我們根據目錄結構,從上往下建立文件夾和文件
建立文件夾和文件 goossclient/conf/config.yaml ,config.yaml 內容如下:
common:
#服務器配置
server:
server: http://xxx.com #服務器地址
port: 6669 #服務器端口
uploadapi: ossupload #服務器上傳接口
allowtype: [ #允許上傳的文件格式, *是所有
jpg,
png,
gif,
# "*", # * 代表所有文件
]
建立文件夾和文件 goossclient/handler/handler.go,handler.go內容如下:
package handler
import (
"errors"
"bytes"
"net/http"
"io/ioutil"
)
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
//http請求
func HttpRequest(api string,contenttype string,buff *bytes.Buffer,method string) (string, error) {
//jsonStr := []byte(json)
//req, err := http.NewRequest(method, api, bytes.NewBuffer(jsonStr))
req, err := http.NewRequest(method, api, buff)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", contenttype) //使用json格式傳參
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
if !(resp.StatusCode == 200) {
return "", errors.New("api服務器出錯")
}
return string(body), nil
}
建立文件夾和文件 goossclient/model/windowdialog.go,windowdialog.go內容如下:
package model
import (
"io"
"os"
"os/exec"
"fmt"
"time"
"bytes"
"strings"
"strconv"
"crypto/md5"
"path/filepath"
"mime/multipart"
"github.com/tidwall/gjson"
"github.com/spf13/viper"
"github.com/lxn/walk"
. "github.com/lxn/walk/declarative"
. "goossclient/handler"
)
/*-------------------window界面-------------------*/
/*
# 不隱藏cmd 窗口
[root@localhost aichatwindow]# CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build
# 隱藏cmd 窗口
[root@localhost aichatwindow]# CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-H windowsgui"
*/
type MyMainWindow struct {
*walk.MainWindow
model *MessageModel
selectedfile *walk.LineEdit
message *walk.ListBox
wv *walk.WebView
//imageview *walk.ImageView
}
func OpenWindow() {
mw := &MyMainWindow{model: NewMessageModel()}
if _, err := (MainWindow{
AssignTo: &mw.MainWindow,
Title: "阿里雲文件上傳",
//Icon: "test.ico", //圖標文件路徑
MinSize: Size{300, 400},
Layout: VBox{},
Children: []Widget{
GroupBox{
MaxSize: Size{500, 500},
Layout: HBox{},
Children: []Widget{
PushButton{ //選擇文件
Text: "打開文件",
OnClicked: mw.selectFile, //點擊事件
},
Label{Text: "選中的文件 "},
LineEdit{
AssignTo: &mw.selectedfile, //選中的文件
},
PushButton{ //上傳
Text: "上傳",
OnClicked: mw.uploadFile, //上傳
},
},
},
ListBox{ //記錄框
AssignTo: &mw.message,
OnCurrentIndexChanged: mw.lb_CurrentIndexChanged, //單擊
OnItemActivated: mw.lb_ItemActivated, //雙擊
},
Composite{
Layout: Grid{Columns: 2, Spacing: 10},
Children: []Widget{
WebView{
//MinSize: Size{1000, 0},
AssignTo: &mw.wv,
},
},
},
},
}.Run());
err != nil {
fmt.Printf("Run err: %+v\n",err)
}
}
func (mw *MyMainWindow) selectFile() {
allowType := viper.GetStringSlice("common.server.allowtype")
fmt.Printf("allowType: %+v\n",allowType)
dlg := new(walk.FileDialog)
dlg.Title = "選擇文件"
//dlg.Filter = "可上傳jpg (*.jpg)|*.jpg|可上傳png (*.png)|*.png|可上傳gif (*.gif)|*.gif|所有文件 (*.*)|*.*"
//判斷可允許上傳的文件
filter := []string{}
filterstring := ""
for _,v := range allowType{
if(v != "*"){
filterstring = "可上傳"+v+" (*."+v+")|*."+v
filter = append(filter,filterstring)
}else{
filterstring = "所有文件"+v+" (*."+v+")|*."+v
filter = append(filter,filterstring)
}
}
dlg.Filter = strings.Join(filter,"|") //切片轉換字符串
fmt.Printf("dlg.Filter: %+v\n",dlg.Filter)
record := getMessage(mw) //獲取記錄
if ok, err := dlg.ShowOpen(mw); err != nil {
mw.selectedfile.SetText("") //通過重定向變量設置TextEdit的Text
_=writeMessage(mw,"Error : File Open",record) //寫入記錄
return
} else if !ok {
mw.selectedfile.SetText("") //通過重定向變量設置TextEdit的Text
_=writeMessage(mw,"cancel",record) //寫入記錄
return
}
s := fmt.Sprintf("Select : %s", dlg.FilePath)
_=writeMessage(mw,s,record) //寫入記錄
mw.selectedfile.SetText(dlg.FilePath) //通過重定向變量設置TextEdit的Text
}
func (mw *MyMainWindow) uploadFile(){
filename := mw.selectedfile.Text() //上傳的文件
record := getMessage(mw) //獲取記錄
fmt.Printf("filename: %+v\n",filename)
if len(filename) == 0{
fmt.Println("select a file")
_=writeMessage(mw,"請選擇文件",record)
return
}
//-----------------------------------------------
//模擬表單上傳文件到服務器接口
bodyBuf := &bytes.Buffer{} //使用二進制緩衝
bodyBuf.Reset() //重置緩衝
bodyWriter := multipart.NewWriter(bodyBuf) //創建表單
//加入token
curtime := time.Now().Unix()
h := md5.New()
io.WriteString(h,strconv.FormatInt(curtime,10))
token := fmt.Sprintf("%x",h.Sum(nil)) //生成token
//模擬表單數據
_ = bodyWriter.WriteField("token", token)
_ = bodyWriter.WriteField("username", "awef")
//關鍵的一步操作 ,模擬上傳文件
//獲取上傳文件名稱
filenamesplit := strings.Split(filename,"\\") //使用 \ 分隔符,將字符串切片
filenamesplit = filenamesplit[len(filenamesplit)-1:] //獲取切片最後一個
newname := strings.Join(filenamesplit,"") //切片轉換爲字符串
fileWriter, err := bodyWriter.CreateFormFile("files", newname) //表單創建一個文件參數
if err != nil {
fmt.Println("error writing to buffer")
_=writeMessage(mw,"error writing to buffer",record)
return
}
//打開文件句柄操作
fh, err := os.Open(filename)
if err != nil {
fmt.Println("error opening file")
_=writeMessage(mw,"error opening file",record)
return
}
defer fh.Close()
//iocopy ,複製源文件到 模擬表單的文件裏
_, err = io.Copy(fileWriter, fh) //fileWriter目標文件 ,fh源文件 ,複製源文件到目標文件
if err != nil {
_=writeMessage(mw,"error io.Copy",record)
return
}
contentType := bodyWriter.FormDataContentType() //獲取表單類型
fmt.Printf("contentType: %+v\n",contentType)
bodyWriter.Close()
//resp, err := http.Post(targetUrl, contentType, bodyBuf)
resp, err := UploadRequest(contentType,bodyBuf)
if err != nil{
fmt.Printf("UploadApi err: %+v\n",err)
_=writeMessage(mw,filename+" 上傳失敗",record)
return
}
fmt.Printf("body : %+v\n",resp)
//var results string
// 使用gjson 獲取返回結果的
coderesult := gjson.Get(resp, "code")
code := coderesult.String() //斷言轉換成字符串
if code != "1"{
_=writeMessage(mw,filename+" 上傳失敗",record)
return
}
record=writeMessage(mw,filename+" 上傳成功",record)
result := gjson.Get(resp, "data.0.0.url")
fmt.Printf("result: %+v\n",result)
resultstring := result.String()//斷言轉換成字符串
_=writeMessage(mw,resultstring,record)
return
}
//消息記錄改變事件
func (mw *MyMainWindow) lb_CurrentIndexChanged() {
fmt.Printf("mw.message.CurrentIndex(): ",mw.message.CurrentIndex())
fmt.Println()
return
}
//消息記錄點擊事件
func (mw *MyMainWindow) lb_ItemActivated() {
fmt.Println("mw.message.CurrentIndex(): ",mw.message.CurrentIndex())
fmt.Println();
fmt.Printf("mw.model.items: %+v ",mw.model.items)
fmt.Println();
index := mw.message.CurrentIndex()
imagename := mw.model.items[index].Name //獲取當前選中名稱
image := mw.model.items[index].Value //獲取當前選中值
is_http := strings.Index(image,"http") //查找字符串位置
is_ie := strings.Index(imagename,"默認瀏覽器") //查找字符串位置
if is_http != -1{ // -1 是找不到
//walk.MsgBox(mw, "Value", value, walk.MsgBoxIconInformation) //提示框
fmt.Printf("image : %+v ",image)
fmt.Println();
if is_ie != -1{
openImageExplorer(image) ////使用默認瀏覽器打開圖片
}else{
openImageWebview(mw,image) ////使用webview打開圖片
}
}
return
}
//使用默認瀏覽器打開圖片
func openImageExplorer(image string){
cmd := exec.Command("explorer", image)
err := cmd.Start()
if err != nil {
fmt.Println(err)
}
return
}
//創建html,使用webview打開圖片
func openImageWebview(mw *MyMainWindow,image string){
go func() {
h := md5.New()
io.WriteString(h,image)
htmlname := fmt.Sprintf("%x",h.Sum(nil)) //生成html名稱
userFile := ""+htmlname+".html"
fileall := getCurrentDirectory() + "/"+userFile //html的絕對路徑
fout, err := os.Create(fileall) //創建html文件
defer fout.Close()
if err != nil {
fmt.Println(err)
return
}
html := `<!DOCTYPE html><html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><title>圖片展示</title></head><body><a href="%s" target="_self">%s</a><br><img src="%s" alt=""></body></html>`
html = fmt.Sprintf(html,image,image,image) //插入圖片
fout.WriteString(html) //把字符串寫入html
mw.wv.SetURL("file:///" + getCurrentDirectory() + "/"+userFile)
}()
}
//獲取當前文件路徑
func getCurrentDirectory() string {
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil {
fmt.Println(err.Error())
}
return strings.Replace(dir, "\\", "/", -1)
}
//消息記錄每個模型
type MessageItem struct {
Name string `json:"name"`
Value string `json:"value"`
}
//消息記錄模型
type MessageModel struct {
walk.ListModelBase
items []MessageItem
}
//新建消息模型
func NewMessageModel() *MessageModel {
m := &MessageModel{items: []MessageItem{}}
return m
}
//插入到消息模型中
func appendMessageModel(mw *MyMainWindow,message string) {
item := MessageItem{
Name: message,
Value: message,
}
mw.model.items = append(mw.model.items,item)
}
//獲取記錄
func getMessage(mw *MyMainWindow) []string{
message := mw.message.Model() //獲取以前的記錄
fmt.Println("message",message)
record := []string{} //記錄
if message != nil {
for _,v := range message.([]string){
record = append(record,v) //插入以前的記錄
}
}
return record
}
//寫入記錄
func writeMessage(mw *MyMainWindow,message string,record []string)[]string{
is_http := strings.Index(message,"http") //查找字符串位置
message_record := message
if is_http != -1{ // -1 是找不到
message_record = "雙擊查看圖片:"+message
//插入默認瀏覽器打開記錄
item := MessageItem{
Name: "雙擊用默認瀏覽器打開圖片"+message,
Value: message,
}
mw.model.items = append(mw.model.items,item)
record = append(record,"雙擊用默認瀏覽器打開圖片"+message) //插入記錄
}
record = append(record,message_record) //插入記錄
appendMessageModel(mw,message) //插入模型
mw.message.SetModel(record) //記錄輸出
return record
}
/*-------------------上傳接口-------------------*/
type OssStruct struct {
Url string `json:"url"`
}
//上傳請求
func UploadRequest(contenttype string,buff *bytes.Buffer) (string,error) {
server := viper.GetString("common.server.server")
port := viper.GetString("common.server.port")
serverapi := viper.GetString("common.server.uploadapi")
api := fmt.Sprintf("%s:%s/%s",server,port,serverapi)
//發送http請求圖靈api , body是http響應
var body, resultErrs = HttpRequest(api,contenttype,buff,"POST")
if resultErrs != nil {
fmt.Printf("HttpRequest err: %+v\n",resultErrs)
}
return body, nil
}
建立文件夾和文件 goossclient/goossclient.manifest,goossclient.manifest內容如下:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity version="1.0.0.0" processorArchitecture="*" name="SomeFunkyNameHere" type="win32"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">True</dpiAware>
</windowsSettings>
</application>
</assembly>
建立文件夾和文件 goossclient/main.go,main.go內容如下:
package main
import (
"fmt"
"github.com/spf13/viper"
"goossclient/model"
)
func main() {
viper.AddConfigPath("conf")
if err := viper.ReadInConfig();err != nil{
fmt.Println(err)
}
model.OpenWindow()
}
初始化模塊
[root@izj6c4jirdug8kh3uo6rdez goossclient]# go mod init goossclient
go: creating new go.mod: module goossclient
打包window客戶端
# 不隱藏cmd 窗口
[root@localhost goossclient]# CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build
# 隱藏cmd 窗口
[root@localhost goossclient]# CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-H windowsgui"
# goossclient.exe 就是 windows 客戶端
[root@izj6c4jirdug8kh3uo6rdez goossclient]# ll
total 15956
drwxr-xr-x 2 root root 4096 Dec 5 14:25 conf
-rw-r--r-- 1 root root 277 Dec 7 11:18 go.mod
-rwxr-xr-x 1 root root 16292352 Dec 7 11:18 goossclient.exe
-rw-r--r-- 1 root root 889 Dec 5 14:25 goossclient.manifest
-rw-r--r-- 1 root root 15081 Dec 7 11:18 go.sum
drwxr-xr-x 2 root root 4096 Dec 5 14:25 handler
-rw-r--r-- 1 root root 215 Dec 7 09:39 main.go
drwxr-xr-x 2 root root 4096 Dec 5 14:25 model
-rw-r--r-- 1 root root 1069 Dec 5 14:30 rsrc.syso
把客戶端移動到window系統下運行 goossclient.exe
客戶端結束
參考walk:https://github.com/lxn/walk