sunsheng 1 tahun lalu
induk
melakukan
b8015227de

+ 1 - 1
src/api/init.go

@@ -16,7 +16,7 @@ func NewHttpService(app *conf.AppConf, dbMongo *db.MongoDB, redisClient *redis.C
 
 	// store := cookie.NewStore([]byte("adhuaxi-server"))
 	// engine.Use(sessions.Sessions("dcsession", store))
-	// engine.Static("/public", "static")
+	engine.Static("/web", "static")
 	config := cors.DefaultConfig()
 	// config.AllowOrigins == []string{"http://google.com", "http://facebook.com"}
 	config.AllowAllOrigins = true

+ 3 - 0
src/api/router.go

@@ -19,6 +19,9 @@ func RegRouters(svc *Service) {
 	root.GETJWT("/admin/user/list", UserList)
 	root.GETJWT("/admin/user/detail/:id", UserDetail)
 	root.POSTJWT("/admin/user/update", UpdateUser)
+	root.POSTJWT("/admin/user/import", ImportUser)
+	root.POSTJWT("/admin/user/export", ExportUser)
+	root.POSTJWT("/admin/user/tmplate", UserTmplate)
 
 	// 获取自己的详情信息
 	root.GETJWT("/user/profile", UserProfile)

+ 13 - 1
src/api/test.go

@@ -16,6 +16,9 @@ import (
 	"go.mongodb.org/mongo-driver/bson/primitive"
 )
 
+// 用户唯一
+// db.collection.createIndex({ loginName: 1 }, { unique: true })
+
 // 新增试题
 // /admin/test/create/:scope
 func CreateTest(c *gin.Context, apictx *ApiSession) (interface{}, error) {
@@ -71,13 +74,22 @@ func DeleteTest(c *gin.Context, apictx *ApiSession) (interface{}, error) {
 }
 
 // 试题列表
-// /admin/test/list/:scope
+// /admin/test/list/:scope?content=xxx&type=判断&page=1&size=10
 func TestList(c *gin.Context, apictx *ApiSession) (interface{}, error) {
 	db := c.Param("scope")
 	if len(db) == 0 {
 		return nil, errors.New("scope不能为空")
 	}
 	page, size, query := UtilQueryPageSize(c)
+	content := c.Query("content")
+	_type := c.Query("type")
+	if len(content) > 0 {
+		query["content"] = bson.M{"$regex": content, "$options": "$i"}
+	}
+	if len(_type) > 0 {
+		query["type"] = bson.M{"$regex": _type, "$options": "$i"}
+	}
+
 	return repo.RepoDbPageSearch(apictx.CreateRepoCtx(), &repo.PageSearchOptions{
 		Db:          db,
 		CollectName: repo.CollectionTest,

+ 0 - 0
src/api/excel.go → src/api/testExcel.go


+ 147 - 0
src/api/user.go

@@ -4,12 +4,15 @@ import (
 	"copter-train/log"
 	"copter-train/utils"
 	"errors"
+	"fmt"
+	"net/url"
 	"time"
 
 	"copter-train/db/model"
 	"copter-train/db/repo"
 
 	"github.com/gin-gonic/gin"
+	"github.com/xuri/excelize/v2"
 	"go.mongodb.org/mongo-driver/bson"
 	"go.mongodb.org/mongo-driver/bson/primitive"
 )
@@ -126,9 +129,17 @@ func UserList(c *gin.Context, apictx *ApiSession) (interface{}, error) {
 	}
 	page, size, query := UtilQueryPageSize(c)
 	role := c.Query("role")
+	name := c.Query("name")
+	nid := c.Query("nid")
 	if len(role) > 0 {
 		query["roles"] = bson.M{"$elemMatch": bson.M{"$eq": role}}
 	}
+	if len(name) > 0 {
+		query["name"] = bson.M{"$regex": name, "$options": "$i"}
+	}
+	if len(nid) > 0 {
+		query["nid"] = bson.M{"$regex": nid, "$options": "$i"}
+	}
 
 	return repo.RepoPageSearch(apictx.CreateRepoCtx(), &repo.PageSearchOptions{
 		CollectName: repo.CollectionUser,
@@ -243,3 +254,139 @@ func IsStudent(c *gin.Context, apictx *ApiSession) (bool, error) {
 	}
 	return false, nil
 }
+
+// 导入用户
+func ImportUser(c *gin.Context, apictx *ApiSession) (interface{}, error) {
+	isAdmin, err := IsAdmin(c, apictx)
+	if err != nil {
+		return nil, err
+	}
+	if !isAdmin {
+		return nil, errors.New("没有权限")
+	}
+	file, _, err := c.Request.FormFile("file")
+	if err != nil {
+		return nil, errors.New("文件错误")
+	}
+	excel, err := excelize.OpenReader(file)
+	if err != nil {
+		return nil, err
+	}
+	excelIndex := excel.GetActiveSheetIndex()
+	sheetName := excel.GetSheetName(excelIndex)
+	users, err := excel.GetRows(sheetName)
+	if err != nil {
+		return nil, err
+	}
+	errors := []string{}
+
+	if len(users) > 0 {
+		for index, us := range users {
+			rowNum := index + 1
+			// 标题行
+			if index == 0 {
+				rowNum++
+				continue
+			}
+			// 表头
+			if index == 1 {
+				rowNum++
+				continue
+			}
+			// 去除可能的空行
+			if len(us[0]) < 3 {
+				rowNum++
+				continue
+			}
+			// 用户名name 编号nid 密码password 角色roles
+			user := &model.User{}
+			user.Name = us[0]
+			user.Nid = us[1]
+			if len(us[0]) == 0 {
+				errors = append(errors, fmt.Sprintf("第%d行错误: %s", rowNum, "用户名不能为空"))
+				continue
+			}
+			if len(us[1]) == 0 {
+				errors = append(errors, fmt.Sprintf("第%d行错误: %s", rowNum, "编号不能为空"))
+				continue
+			}
+			user.Roles = []string{us[2]}
+			user.Password = UtilMd5(us[3])
+			if len(us[3]) == 0 || len(us[3]) == 32 {
+				user.Password = UtilMd5("123456")
+			}
+			user.LoginName = fmt.Sprintf("%s_%s", user.Name, user.Nid)
+
+			user.CreateTime = time.Now()
+			user.UpdateTime = time.Now()
+
+			_, err = repo.RepoAddDoc(apictx.CreateRepoCtx(), repo.CollectionUser, user)
+			if err != nil {
+				errors = append(errors, fmt.Sprintf("第%d行错误: %s", rowNum, "保存数据失败, 请检查数据格式是否正确/用户是否重复"))
+				log.Error(err)
+			}
+
+		}
+	}
+
+	return errors, nil
+}
+
+// 导出用户
+func ExportUser(c *gin.Context, apictx *ApiSession) (interface{}, error) {
+	isAdmin, err := IsAdmin(c, apictx)
+	if err != nil {
+		return nil, err
+	}
+	if !isAdmin {
+		return nil, errors.New("没有权限")
+	}
+	f := excelize.NewFile()
+	index, _ := f.NewSheet("Sheet1")
+	f.SetActiveSheet(index)
+	f.SetDefaultFont("宋体")
+
+	testExcel := NewUserExcel(f)
+	testExcel.Title = "用户信息"
+
+	_, _, query := UtilQueryPageSize(c)
+	// 获取试题列表
+	users := make([]*model.User, 0)
+	err = repo.RepoSeachDocs(apictx.CreateRepoCtx(), &repo.DocSearchOptions{
+		CollectName: repo.CollectionUser,
+		Query:       query,
+		Sort:        bson.M{"createTime": 1},
+	}, &users)
+	if err != nil {
+		return nil, err
+	}
+	testExcel.Content = users
+	testExcel.Draws()
+	fileName := url.PathEscape("用户信息.xlsx")
+	c.Header("Content-Type", "application/octet-stream")
+	c.Header("Content-Disposition", "attachment; filename="+fileName)
+	c.Header("Content-Transfer-Encoding", "binary")
+
+	err = f.Write(c.Writer)
+	if err != nil {
+		return nil, err
+	}
+
+	return nil, nil
+}
+
+// 下载用户模板
+func UserTmplate(c *gin.Context, apictx *ApiSession) (interface{}, error) {
+	filename := "用户信息模板.xlsx"
+	// url.PathEscape将字符串中的特殊字符进行编码,使其符合URL规范
+	filename = url.PathEscape(filename)
+
+	// 设置下载的文件名
+	c.Writer.Header().Add("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
+
+	// 设置文件类型
+	c.Writer.Header().Add("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+
+	c.File(USERS_TMPLATE_FILE)
+	return nil, nil
+}

+ 187 - 0
src/api/userExcel.go

@@ -0,0 +1,187 @@
+package api
+
+import (
+	"copter-train/db/model"
+	"fmt"
+	"strings"
+
+	"github.com/xuri/excelize/v2"
+)
+
+type UserExcel struct {
+	Offset           int
+	Row              int
+	Title            string //标题
+	Excel            *excelize.File
+	SheetName        string
+	AlignCenterStyle int
+	Content          []*model.User
+	RowMap           map[string]int
+	RowWidthArray    []float64
+	RowsHeightArray  []map[int]float64
+}
+
+// 批量设置行高
+func (b *UserExcel) setRowsHeight() {
+	for _, rowHeight := range b.RowsHeightArray {
+		for row, height := range rowHeight {
+			b.Excel.SetRowHeight(b.SheetName, row, height)
+		}
+	}
+
+}
+
+// 获取范围内单元格的宽度 A:F
+func (b *UserExcel) getRangeWidth(r string) float64 {
+	rg := strings.Split(r, ":")
+
+	if len(rg) == 1 {
+		start := b.RowMap[rg[0]]
+		return b.RowWidthArray[start]
+	} else if len(rg) == 2 {
+		start := b.RowMap[rg[0]]
+		end := b.RowMap[rg[1]]
+		rowr := b.RowWidthArray[start : end+1]
+		width := 0.0
+		for _, v := range rowr {
+			width += v
+		}
+		return width
+	}
+	return 0.0
+}
+
+func (b *UserExcel) drawTitle() error {
+	b.Row++
+	startCell := fmt.Sprintf("A%d", b.Row)
+	endCell := fmt.Sprintf("D%d", b.Row)
+
+	b.RowMap = map[string]int{"A": 0, "B": 1, "C": 2, "D": 3}
+
+	b.RowWidthArray = []float64{12, 12, 12, 36}
+	b.Excel.SetColWidth(b.SheetName, "A", "A", 12)
+	b.Excel.SetColWidth(b.SheetName, "B", "B", 12)
+	b.Excel.SetColWidth(b.SheetName, "C", "C", 12)
+	b.Excel.SetColWidth(b.SheetName, "D", "D", 36)
+
+	err := b.Excel.MergeCell(b.SheetName, startCell, endCell)
+	if err != nil {
+		return err
+	}
+
+	style, err := b.Excel.NewStyle(&excelize.Style{
+		Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"},
+		Font:      &excelize.Font{Bold: true, Size: 18}})
+	if err != nil {
+		return err
+	}
+	err = b.Excel.SetCellStyle(b.SheetName, startCell, startCell, style)
+	if err != nil {
+		return err
+	}
+	b.Excel.SetRowHeight(b.SheetName, b.Row, 26)
+
+	b.Excel.SetCellValue(b.SheetName, startCell, b.Title)
+	return nil
+}
+
+func (b *UserExcel) drawTableTitle() error {
+	b.Row++
+	var drawCol = func(prefix string, value string) error {
+		cell := fmt.Sprintf("%s%d", prefix, b.Row)
+		err := b.Excel.SetCellStyle(b.SheetName, cell, cell, b.AlignCenterStyle)
+		if err != nil {
+			return err
+		}
+
+		return b.Excel.SetCellValue(b.SheetName, cell, value)
+	}
+
+	drawCol("A", "用户名")
+	drawCol("B", "编号")
+	drawCol("C", "角色")
+	drawCol("D", "密码")
+	b.Excel.SetRowHeight(b.SheetName, b.Row, 22)
+
+	return nil
+}
+
+func (b *UserExcel) drawTableContent() error {
+	b.Row++
+	var DrawRow = func(rowIndex int, values ...string) float64 {
+		charas := []string{"A", "B", "C", "D"}
+		// 获取该行最大行高
+		max := getRowHeight(values[0], b.getRangeWidth(charas[0]))
+		for i, c := range charas {
+			v := ""
+			if i < len(values) {
+				v = values[i]
+			}
+			b.Excel.SetCellValue(b.SheetName, fmt.Sprintf("%s%d", c, rowIndex), v)
+			val2Cel := fmt.Sprintf("%s%d", c, rowIndex)
+			b.Excel.SetCellStyle(b.SheetName, val2Cel, val2Cel, b.AlignCenterStyle)
+
+			if getRowHeight(v, b.getRangeWidth(c)) > max {
+				max = getRowHeight(v, b.getRangeWidth(c))
+			}
+		}
+		return max
+	}
+
+	users := b.Content
+	if len(users) > 0 {
+		for _, user := range users {
+			rowMaxHeight := DrawRow(b.Row, user.Name, user.Nid, user.Roles[0], user.Password)
+			b.RowsHeightArray = append(b.RowsHeightArray, map[int]float64{b.Row: rowMaxHeight})
+			b.Row++
+		}
+	}
+
+	return nil
+}
+
+func (b *UserExcel) Draws() {
+	b.drawTitle()
+
+	b.drawTableTitle()
+	b.drawTableContent()
+	// 设置行高
+	b.setRowsHeight()
+}
+
+func NewUserExcel(f *excelize.File) *UserExcel {
+
+	border := []excelize.Border{
+		{Type: "top", Style: 1, Color: "000000"},
+		{Type: "left", Style: 1, Color: "000000"},
+		{Type: "right", Style: 1, Color: "000000"},
+		{Type: "bottom", Style: 1, Color: "000000"},
+	}
+
+	styleLeft, _ := f.NewStyle(&excelize.Style{
+		Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center", WrapText: true},
+		Border:    border,
+	})
+
+	b := &UserExcel{
+		Title:            "用户信息",
+		SheetName:        "Sheet1",
+		Excel:            f,
+		Offset:           0,
+		AlignCenterStyle: styleLeft,
+		RowMap:           map[string]int{"A": 0, "B": 1, "C": 2, "D": 3},
+		RowWidthArray:    []float64{12, 12, 12, 20},
+		RowsHeightArray:  make([]map[int]float64, 0),
+	}
+
+	// f.SetPageMargins(b.SheetName, excelize.PageMarginTop(1), excelize.PageMarginLeft(0), excelize.PageMarginRight(0))
+
+	return b
+}
+
+func (b *UserExcel) FormatToEmpty(str *string) {
+	if *str == "0" || *str == "0.000" {
+		*str = "-"
+	}
+
+}

+ 1 - 0
src/api/utils.go

@@ -3,6 +3,7 @@ package api
 import "math"
 
 const TESTS_TMPLATE_FILE = "./file/考核试题模板.xlsx"
+const USERS_TMPLATE_FILE = "./file/用户信息模板.xlsx"
 
 // func getRowHeight(content string, width float64,lineHeight float64) float64 {
 // 第一个参数为 行宽 第二个参数为 行高

+ 7 - 0
src/copter-train.log

@@ -0,0 +1,7 @@
+{"level":"error","timestamp":"2024-02-22 16:42:41","message":"[write exception: write errors: [E11000 duplicate key error collection: copter.users index: loginName_1 dup key: { loginName: \"管理员_20140001\" }]]","service_name":"copter-train"}
+{"level":"error","timestamp":"2024-02-22 16:42:41","message":"[write exception: write errors: [E11000 duplicate key error collection: copter.users index: loginName_1 dup key: { loginName: \"张三_20140002\" }]]","service_name":"copter-train"}
+{"level":"error","timestamp":"2024-02-22 16:42:41","message":"[write exception: write errors: [E11000 duplicate key error collection: copter.users index: loginName_1 dup key: { loginName: \"赵四_20140003\" }]]","service_name":"copter-train"}
+{"level":"error","timestamp":"2024-02-22 16:43:28","message":"[write exception: write errors: [E11000 duplicate key error collection: copter.users index: loginName_1 dup key: { loginName: \"用户名_123\" }]]","service_name":"copter-train"}
+{"level":"error","timestamp":"2024-02-22 16:43:28","message":"[write exception: write errors: [E11000 duplicate key error collection: copter.users index: loginName_1 dup key: { loginName: \"管理员_20140001\" }]]","service_name":"copter-train"}
+{"level":"error","timestamp":"2024-02-22 16:43:28","message":"[write exception: write errors: [E11000 duplicate key error collection: copter.users index: loginName_1 dup key: { loginName: \"张三_20140002\" }]]","service_name":"copter-train"}
+{"level":"error","timestamp":"2024-02-22 16:43:28","message":"[write exception: write errors: [E11000 duplicate key error collection: copter.users index: loginName_1 dup key: { loginName: \"赵四_20140003\" }]]","service_name":"copter-train"}

TEMPAT SAMPAH
src/file/用户信息模板.xlsx


File diff ditekan karena terlalu besar
+ 0 - 0
src/static/assets/Form-3e7c293d.js


TEMPAT SAMPAH
src/static/assets/bg_admin_login-637fc10d.png


+ 1 - 0
src/static/assets/index-0bb3caf9.css

@@ -0,0 +1 @@
+.m4bnzns{width:400px}.m4bnzns .radio_g{width:100%;text-align:center;margin-bottom:24px}.m4bnzns .options_items .ant-input-group-wrapper{margin-bottom:10px}.m4bnzns .options_items .ant-input-group-wrapper:last-child{margin-bottom:0}.m4bnzns .options_items .ant-input-group-addon{width:36px}.m4bnzns .select_inp,.m4bnzns .select_inp input{cursor:pointer}.pbfvr2l .banner_table .cover_img{max-width:300px;height:100px;object-fit:contain}

File diff ditekan karena terlalu besar
+ 0 - 0
src/static/assets/index-11da78d1.js


File diff ditekan karena terlalu besar
+ 0 - 0
src/static/assets/index-15a099cb.js


File diff ditekan karena terlalu besar
+ 0 - 0
src/static/assets/index-1a4d4cdb.css


File diff ditekan karena terlalu besar
+ 0 - 0
src/static/assets/index-3f4d71cb.js


+ 1 - 0
src/static/assets/index-40fb9f4c.css

@@ -0,0 +1 @@
+.mlgg4eb a{display:block}.pq3nqh9{height:100vh}.pq3nqh9 .ant-layout-header{position:fixed;left:0;top:0;width:100%;z-index:3;padding:0 20px;background-color:var(--vt-c-primary)}.pq3nqh9 .page_content{height:100%;width:100%;padding:88px 24px 24px 224px}.pq3nqh9 .page_content .ant-page-header{padding:0 0 15px}.sr2c0hp{position:fixed;left:0;top:0;padding-top:64px;z-index:2;overflow-y:auto;overflow-x:hidden;height:100%}.sr2c0hp.ant-layout-sider{background-color:#fff;border-right:1px solid #f0f0f0}.sr2c0hp .ant-menu-inline{border-right:none}

File diff ditekan karena terlalu besar
+ 0 - 0
src/static/assets/index-432e0d5a.js


File diff ditekan karena terlalu besar
+ 0 - 0
src/static/assets/index-67779153.js


File diff ditekan karena terlalu besar
+ 0 - 0
src/static/assets/index-71883371.js


+ 1 - 0
src/static/assets/index-7cfc17b2.css

@@ -0,0 +1 @@
+.mf25f5b{width:400px}.mf25f5b .radio_g{width:100%;text-align:center;margin-bottom:24px}.mf25f5b .options_items .ant-input-group-wrapper{margin-bottom:10px}.mf25f5b .options_items .ant-input-group-wrapper:last-child{margin-bottom:0}.mf25f5b .options_items .ant-input-group-addon{width:36px}.mf25f5b .select_inp,.mf25f5b .select_inp input{cursor:pointer}.p70m601 .banner_table .cover_img{max-width:300px;height:100px;object-fit:contain}

File diff ditekan karena terlalu besar
+ 0 - 0
src/static/assets/index-89bc26c0.js


+ 1 - 0
src/static/assets/index-d4896fb7.js

@@ -0,0 +1 @@
+import{d as f,k as v,v as r,c as e,l as s,x as o,I as d,B as w}from"./index-432e0d5a.js";import{F as l,R as n}from"./Form-3e7c293d.js";import"./useFlexGapSupport-1b1e93ca.js";const h={wrapperCol:{span:24}},x={wrapperCol:{span:24}},b=f({setup(){const i=v(),m=r({loading:!1,loginType:"default"}),a=r({loginName:"",password:"",role:"student"}),p=r({loginName:[{required:!0,message:"请输入正确的账号",trigger:"change"}],password:[{required:!0,message:"请输入正确的密码",trigger:"change"}],role:[{required:!0,message:"登录角色不能为空",trigger:"change"}]}),{validate:c,validateInfos:u}=l.useForm(a,p);async function g(){c().then(t=>{i.passwordLogin(t)})}return()=>e("div",{class:y},[e("div",{class:"content"},[e("div",{class:"title"},[s("用户登录")]),e("div",{class:"py-30px px-20px"},[e(l,o(h,{name:"basic"}),{default:()=>[e(l.Item,o({name:"loginName"},u.loginName),{default:()=>[e(d,{placeholder:"请输入账号",value:a.loginName,"onUpdate:value":t=>a.loginName=t,maxlength:30},null)]}),e(l.Item,o({name:"password"},u.password),{default:()=>[e(d.Password,{placeholder:"请输入密码",value:a.password,"onUpdate:value":t=>a.password=t,maxlength:18},null)]}),e(l.Item,o({name:"role"},u.role),{default:()=>[e(n.Group,{class:"flex justify-between w-full login_role",value:a.role,"onUpdate:value":t=>a.role=t},{default:()=>[e(n,{value:"student"},{default:()=>[s("学生")]}),e(n,{value:"teacher"},{default:()=>[s("老师")]}),e(n,{value:"admin"},{default:()=>[s("管理员")]})]})]}),e(l.Item,o(x,{style:{marginBottom:0}}),{default:()=>[e(w,{htmlType:"submit",type:"primary",block:!0,class:"login_btn",size:"large",loading:m.loading,onClick:g},{default:()=>[s("登录")]})]})]})])])])}}),y="p1fwmfaj";export{b as default};

+ 1 - 0
src/static/assets/index-e46e85b2.css

@@ -0,0 +1 @@
+.p1fwmfaj{height:100vh;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;background:url(./bg_admin_login-637fc10d.png) no-repeat center/cover}.p1fwmfaj .content{width:100%;max-width:400px;margin:0 auto;border-radius:8px;background-color:#eaf6ff;border:4px solid #fff}.p1fwmfaj .title{border-radius:8px 8px 0 0;padding:18px 0;background-color:var(--vt-c-primary);font-size:20px;text-align:center;font-weight:700;color:#fff;-webkit-letter-spacing:2px;-moz-letter-spacing:2px;-ms-letter-spacing:2px;letter-spacing:2px}.p1fwmfaj .login_role .ant-radio-wrapper:last-child{margin-right:0}

File diff ditekan karena terlalu besar
+ 0 - 0
src/static/assets/useFlexGapSupport-1b1e93ca.js


TEMPAT SAMPAH
src/static/favicon.ico


+ 18 - 0
src/static/index.html

@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="shortcut icon " type="images/x-icon" href="./favicon.ico" />
+    <meta
+      name="viewport"
+      content="width=device-width, initial-scale=1.0,user-scalable=no"
+    />
+    <title>直升机训练后台管理系统</title>
+    <script type="module" crossorigin src="./assets/index-432e0d5a.js"></script>
+    <link rel="stylesheet" href="./assets/index-1a4d4cdb.css">
+  </head>
+  <body>
+    <div id="app"></div>
+    
+  </body>
+</html>

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini