feat:完善项目

This commit is contained in:
henry
2021-11-08 11:09:27 +08:00
parent 85b58968d1
commit 1502076841
20 changed files with 397 additions and 32 deletions

View File

@ -39,7 +39,8 @@ type Account struct{}
* "msg": "ok"
* "data": {
* "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOiIxNjM4NjA0NDYwIiwiaWF0IjoiMTYzNjAxMjQ2MCIsInVpZCI6IjIwOTU2MTg2ODk5ODEyMjI5MTIifQ.Q4_peBb9aeGaZAfUFMMzn21cbfhY6_DEocI9xlj9v9g",
* "effect_time": 2592000
* "effect_time": 2592000,
* "ws_url": "",
* }
* }
*/
@ -48,9 +49,9 @@ func (a *Account) Login(c *gin.Context) {
Account string `json:"account" form:"account" binding:"required"`
Password string `json:"password" form:"password" binding:"required"`
Captcha struct {
Key string `json:"key" form:"key" binding:"required"`
Value string `json:"value" form:"value" binding:"required"`
} `json:"captcha" form:"captcha" binding:"required"`
Key string `json:"key" form:"key"`
Value string `json:"value" form:"value"`
} `json:"captcha" form:"captcha"`
}{}
if err := bind(form)(c); err != nil {
APIFailure(err.(error))(c)

View File

@ -12,6 +12,31 @@ type Config struct{}
* @apiDefine Config 配置管理
*/
func (*Config) List(c *gin.Context) {
form := &struct {
Kind int `json:"kind" form:"kind"`
PageForm
}{}
if err := bind(form)(c); err != nil {
APIFailure(err.(error))(c)
return
}
data, err := config.NewInstance()().List(form.Kind, form.Page, form.PageSize)
APIResponse(err, data)(c)
}
func (*Config) Edit(c *gin.Context) {
form := &struct {
Params map[string]interface{} `json:"params" form:"params"`
}{}
if err := bind(form)(c); err != nil {
APIFailure(err.(error))(c)
return
}
err := config.NewInstance()().Form(form.Params)
APIResponse(err)(c)
}
/**
* @api {get} /api/v1/config/area 区域信息
* @apiVersion 1.0.0
@ -47,7 +72,7 @@ func (*Config) Area(c *gin.Context) {
APIFailure(err.(error))(c)
return
}
data := config.NewInstance()(getSession()(c).(*service.Session)).Area(form.Key)
data := config.NewArea()(getSession()(c).(*service.Session)).List(form.Key)
APIResponse(nil, data)(c)
}

46
app/api/websocket.go Normal file
View File

@ -0,0 +1,46 @@
package api
import (
"ArmedPolice/app/handle"
"ArmedPolice/app/service"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"net/http"
)
type Websocket struct{}
var upGrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
func (*Websocket) Ws(c *gin.Context) {
conn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
APIFailure(err)(c)
return
}
session := getSession()(c).(*service.Session)
client := service.NewWebsocket(session.UIDToString(), conn)
service.HubMessage.RegisterHandle(client)
go client.Write()
}
func (*Websocket) Pong(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "ping"})
}
func (*Websocket) Publish(c *gin.Context) {
key := c.Query("key")
service.HubMessage.EmitHandle(&service.HubEmit{
ID: key,
Msg: handle.NewWorkNotice("你有一条待办事项"),
})
APISuccess()(c)
}

View File

@ -12,6 +12,7 @@ type WorkInstance struct {
Breakdown string `gorm:"column:breakdown;type:varchar(150);default:null;comment:故障" json:"breakdown"`
Priority WorkInstancePriority `gorm:"column:priority;type:tinyint(1);default:1;comment:工单优先级" json:"priority"`
Schedule uint64 `gorm:"column:schedule;type:int(11);default:1;comment:工单进度" json:"schedule"`
IsAssist WorkInstanceAssist `orm:"column:is_assist;type:tinyint(1);default:0;comment:协助状态" json:"is_assist"` // 当前阶段协助状态,确认是否需要下一阶段协助
Status WorkInstanceStatus `gorm:"column:status;type:tinyint(1);default:0;comment:工单状态" json:"status"`
Remark string `gorm:"column:remark;type:varchar(255);default:null;comment:备注信息" json:"remark"`
ModelDeleted
@ -50,6 +51,16 @@ const (
WorkInstanceStatusForComplete
)
// WorkInstanceAssist 协助状态
type WorkInstanceAssist int
const (
// WorkInstanceAssistForNot 否
WorkInstanceAssistForNot WorkInstanceAssist = iota
// WorkInstanceAssistForYes 是
WorkInstanceAssistForYes
)
func (m *WorkInstance) TableName() string {
return "work_instance"
}

View File

@ -5,11 +5,12 @@ import "strings"
// WorkSchedule 工单流程数据模型
type WorkSchedule struct {
Model
Title string `gorm:"column:title;type:varchar(30);default:null;comment:标题" json:"title"`
Stage int `orm:"column:stage;type:tinyint(1);default:1;comment:阶段" json:"stage"`
Step int `orm:"column:step;type:tinyint(1);default:1;comment:步骤1阶段-1步骤" json:"step"`
Target WorkScheduleTarget `orm:"column:target;type:tinyint(1);default:1;comment:对象类型" json:"target"`
TargetValue string `orm:"column:target_value;type:tinyint(1);default:1;comment:对象信息" json:"target_value"`
Title string `gorm:"column:title;type:varchar(30);default:null;comment:标题" json:"title"`
Stage int `orm:"column:stage;type:tinyint(1);default:1;comment:阶段" json:"stage"`
Step int `orm:"column:step;type:tinyint(1);default:1;comment:步骤1阶段-1步骤" json:"step"`
Target WorkScheduleTarget `orm:"column:target;type:tinyint(1);default:1;comment:对象类型" json:"target"`
TargetValue string `orm:"column:target_value;type:tinyint(1);default:1;comment:对象信息" json:"target_value"`
IsCountersign WorkScheduleCountersign `orm:"column:is_countersign;type:tinyint(1);default:0;comment:是否会签" json:"is_countersign"`
ModelDeleted
ModelAt
}
@ -24,10 +25,24 @@ const (
WorkScheduleTargetForRole
)
// WorkScheduleCountersign 工单会签模式
type WorkScheduleCountersign int
const (
// WorkScheduleCountersignForNot 不是会签模式
WorkScheduleCountersignForNot WorkScheduleCountersign = iota
// WorkScheduleCountersignForYes 是会签模式
WorkScheduleCountersignForYes
)
func (m *WorkSchedule) TableName() string {
return "work_schedule"
}
func (m *WorkSchedule) CountersignStatus() bool {
return m.IsCountersign == WorkScheduleCountersignForYes
}
func (m *WorkSchedule) GetTargetValueAttribute() []string {
return strings.Split(m.TargetValue, ",")
}

View File

@ -2,7 +2,6 @@ package account
import (
model2 "ArmedPolice/app/common/model"
"ArmedPolice/app/handle"
"ArmedPolice/app/model"
"ArmedPolice/app/service"
"ArmedPolice/config"
@ -18,14 +17,15 @@ type (
InstanceLoginResponse struct {
Token string `json:"token"`
EffectTime int `json:"effect_time"`
WsUrl string `json:"ws_url"`
}
)
func (c *Instance) Login(account, password, captchaKey, captchaValue, ip string) (interface{}, error) {
// 验证验证码
if pass, _ := handle.NewCaptcha().Validate(&handle.CaptchaImage{Key: captchaKey, Captcha: captchaValue}); !pass {
return nil, errors.New("验证码错误")
}
//if pass, _ := handle.NewCaptcha().Validate(&handle.CaptchaImage{Key: captchaKey, Captcha: captchaValue}); !pass {
// return nil, errors.New("验证码错误")
//}
mSysUser := model.NewSysUser()
isExist, err := mSysUser.GetByAccountOrMobile(account)
@ -67,13 +67,15 @@ func (c *Instance) Login(account, password, captchaKey, captchaValue, ip string)
service.Publish(config.EventForRedisHashProduce, config.RedisKeyForAccount, key, session)
service.Publish(config.EventForAccountLoginProduce, session.TenantID, session.UID, ip)
return &InstanceLoginResponse{Token: session.Token, EffectTime: config.SettingInfo.TokenEffectTime}, nil
return &InstanceLoginResponse{Token: session.Token, EffectTime: config.SettingInfo.TokenEffectTime,
WsUrl: config.SystemConfig[config.WsDomain].(string)}, nil
}
// Logout 退出请求
func (c *Instance) Logout() error {
if c.UID > 0 {
service.Publish(config.EventForRedisHashDestroy, config.RedisKeyForAccount, utils.UintToString(c.UID))
service.HubMessage.UnregisterHandle(service.NewWebsocket(c.UIDToString(), nil))
}
return nil
}

View File

@ -0,0 +1,24 @@
package config
import (
"ArmedPolice/app/service"
"ArmedPolice/config"
)
type Area struct{ *service.Session }
type AreaHandle func(session *service.Session) *Area
// List 区域信息
func (c *Area) List(key string) map[string]string {
if key == "" {
key = config.DefaultChinaAreaCode
}
return config.SettingAreaInfo[key]
}
func NewArea() AreaHandle {
return func(session *service.Session) *Area {
return &Area{session}
}
}

View File

@ -1,24 +1,65 @@
package config
import (
"ArmedPolice/app/service"
model2 "ArmedPolice/app/common/model"
"ArmedPolice/app/controller/basic"
"ArmedPolice/app/model"
"ArmedPolice/config"
"ArmedPolice/serve/orm"
"errors"
"time"
"gorm.io/gorm"
)
type Instance struct{ *service.Session }
type Instance struct{}
type InstanceHandle func(session *service.Session) *Instance
type InstanceHandle func() *Instance
// Area 区域信息
func (c *Instance) Area(key string) map[string]string {
if key == "" {
key = config.DefaultChinaAreaCode
func (c *Instance) List(kind, page, pageSize int) (*basic.PageDataResponse, error) {
mSysConfig := model.NewSysConfig()
where := []*model2.ModelWhereOrder{
&model2.ModelWhereOrder{Order: model2.NewOrder("kind", model2.OrderModeToAsc)},
}
return config.SettingAreaInfo[key]
if kind > 0 {
where = append(where, &model2.ModelWhereOrder{Where: model2.NewWhere("kind", kind)})
}
out := make([]*model2.SysConfig, 0)
var count int64
if err := model2.Pages(mSysConfig.SysConfig, &out, page, pageSize, &count, where...); err != nil {
return nil, err
}
return &basic.PageDataResponse{Data: out, Count: count}, nil
}
func (c *Instance) Form(params map[string]interface{}) error {
if len(params) <= 0 {
return nil
}
return orm.GetDB().Transaction(func(tx *gorm.DB) error {
mSysConfig := model.NewSysConfig()
now := time.Now()
for k, v := range params {
if _, has := config.SystemConfig[k]; !has {
return errors.New("UnKnown Config Key " + k)
}
if err := model2.UpdatesWhere(mSysConfig.SysConfig, map[string]interface{}{
"value": v, "updated_at": now,
}, []*model2.ModelWhere{model2.NewWhere("key", k)}, tx); err != nil {
return err
}
config.SystemConfig[k] = v
}
return nil
})
}
func NewInstance() InstanceHandle {
return func(session *service.Session) *Instance {
return &Instance{session}
return func() *Instance {
return &Instance{}
}
}

View File

@ -6,7 +6,9 @@ import (
"ArmedPolice/app/model"
"ArmedPolice/app/service"
"ArmedPolice/serve/orm"
"ArmedPolice/utils"
"errors"
"fmt"
"gorm.io/gorm"
"time"
)
@ -77,14 +79,27 @@ func (c *Person) Examine(id uint64, status int, remark string, isAssist int) err
if _status == model2.WorkProgressStatusForRefuse {
goto FINISH
}
if newNextScheduleInfo, err = mWorkSchedule.NextSchedule(); err != nil {
// 下一流程信息
if newNextScheduleInfo, err = mWorkSchedule.NextSchedule(model2.WorkInstanceAssist(isAssist) == model2.WorkInstanceAssistForYes); err != nil {
return err
}
// 无下一流程,工单直接完成
if newNextScheduleInfo == nil {
if newNextScheduleInfo == nil || newNextScheduleInfo.ID <= 0 {
goto FINISH
}
workUpdates["status"] = model2.WorkInstanceStatusForOngoing
workUpdates["schedule"] = newNextScheduleInfo.ID
if newNextScheduleInfo.IsNext {
workUpdates["is_assist"] = isAssist
}
// 推送通知
go utils.TryCatch(func() {
for _, u := range newNextScheduleInfo.Reviewer {
// Socket通知
fmt.Println(u)
}
})
FINISH:
if err = model2.Updates(mWorkInstance.WorkInstance, workUpdates, tx); err != nil {
return err

20
app/handle/notice.go Normal file
View File

@ -0,0 +1,20 @@
package handle
import "encoding/json"
type WorkNotice struct {
Prefix string `json:"prefix"`
Message interface{} `json:"message"`
}
func (this *WorkNotice) ToBytes() []byte {
out, _ := json.Marshal(this)
return out
}
func NewWorkNotice(msg interface{}) *WorkNotice {
return &WorkNotice{
Prefix: "work",
Message: msg,
}
}

View File

@ -5,9 +5,14 @@ import (
"ArmedPolice/app/service"
"ArmedPolice/config"
"ArmedPolice/lib"
"ArmedPolice/utils"
)
func Init() {
// 启动SocketHub
go utils.TryCatch(func() {
service.NewHub().Run()
})
// 载入数据配置
lib.LoadConfig("./json/area.json", &config.SettingAreaInfo)
// RedisHash存储/移除监听

5
app/logic/inotice.go Normal file
View File

@ -0,0 +1,5 @@
package logic
type INotice interface {
ToBytes() []byte
}

View File

@ -12,7 +12,8 @@ type WorkSchedule struct {
// WorkScheduleInfo 工单流程信息
type WorkScheduleInfo struct {
ID uint64 `json:"id"`
Reviewer []uint64 `json:"reviewer"`
IsNext bool `json:"is_next"`
Reviewer []string `json:"reviewer"`
}
// ValidateAuth 验证权限
@ -46,8 +47,32 @@ func (m *WorkSchedule) ValidateAuth(uid uint64) (bool, error) {
return false, nil
}
func (m *WorkSchedule) NextSchedule() (*WorkScheduleInfo, error) {
return nil, nil
// NextSchedule 下一流程
func (m *WorkSchedule) NextSchedule(isAssets bool) (*WorkScheduleInfo, error) {
next := NewWorkSchedule()
isExist, err := model.FirstWhere(next.WorkSchedule, model.NewWhere("stage", m.Stage),
model.NewWhere("step", m.Stage+1))
if err != nil {
return nil, err
} else if isExist { // 直接抛出当前阶段下一流程信息
return &WorkScheduleInfo{ID: next.ID, Reviewer: next.GetTargetValueAttribute()}, err
}
// 进入下一阶段
// 是否需要下一阶段协助
// 不需要,直接抛出结束
if !isAssets {
return nil, nil
}
// 下一阶段第一流程信息
if isExist, err = model.FirstWhere(next.WorkSchedule, model.NewWhere("stage", m.Stage+1),
model.NewWhere("step", 1)); err != nil {
return nil, err
} else if !isExist {
return nil, nil
}
return &WorkScheduleInfo{ID: next.ID, IsNext: true, Reviewer: next.GetTargetValueAttribute()}, err
}
func NewWorkSchedule() *WorkSchedule {

108
app/service/websocket.go Normal file
View File

@ -0,0 +1,108 @@
package service
import (
"ArmedPolice/app/logic"
"ArmedPolice/serve/logger"
"github.com/gorilla/websocket"
)
type Websocket struct {
ID string
conn *websocket.Conn
send chan []byte
}
type Hub struct {
clients map[string]*Websocket
broadcast chan logic.INotice
emit chan *HubEmit
register chan *Websocket
unregister chan *Websocket
}
type HubEmit struct {
ID string
Msg logic.INotice
}
var HubMessage *Hub
func (this *Hub) Run() {
for {
select {
case client := <-this.register:
this.clients[client.ID] = client
logger.InfoF("客户端【%s】发起链接", client.ID)
case client := <-this.unregister:
if _, ok := this.clients[client.ID]; ok {
delete(this.clients, client.ID)
close(client.send)
}
case message := <-this.broadcast:
for _, client := range this.clients {
client.send <- message.ToBytes()
}
case iMsg := <-this.emit:
client, has := this.clients[iMsg.ID]
if has {
client.send <- iMsg.Msg.ToBytes()
}
}
}
}
func (this *Hub) EmitHandle(iMsge *HubEmit) {
this.emit <- iMsge
}
func (this *Hub) BroadcastHandle(msg logic.INotice) {
this.broadcast <- msg
}
func (this *Hub) RegisterHandle(ws *Websocket) {
this.register <- ws
}
func (this *Hub) UnregisterHandle(ws *Websocket) {
this.unregister <- ws
}
func NewHub() *Hub {
HubMessage = &Hub{
clients: make(map[string]*Websocket),
broadcast: make(chan logic.INotice),
emit: make(chan *HubEmit),
register: make(chan *Websocket),
unregister: make(chan *Websocket),
}
return HubMessage
}
func (this *Websocket) Write() {
defer func() {
this.conn.Close()
}()
for {
select {
case message, ok := <-this.send:
if !ok {
return
}
logger.InfoF("发送到客户端【%s】信息%s", this.ID, string(message))
_ = this.conn.WriteMessage(websocket.TextMessage, message)
}
}
}
func (this *Websocket) Read() {
}
func NewWebsocket(id string, conn *websocket.Conn) *Websocket {
return &Websocket{
ID: id,
conn: conn,
send: make(chan []byte),
}
}

View File

@ -1,4 +1,4 @@
name: 教育
name: 武警
# domain 域名
domain: http://192.168.31.195:8030/
# token有效时间

View File

@ -8,6 +8,7 @@ var SystemConfig = map[string]interface{}{}
const (
Name string = "name" // 名称
Domain string = "domain" // 域名
WsDomain string = "ws_domain" // ws域名
TokenEffectTime string = "token_effect_time" // Token有效时间
MultipleLogin string = "multiple_login" // 是否开启多地登录
)

1
go.mod
View File

@ -10,6 +10,7 @@ require (
github.com/elastic/go-elasticsearch/v7 v7.15.1
github.com/gin-gonic/gin v1.7.4
github.com/go-redis/redis v6.15.9+incompatible
github.com/gorilla/websocket v1.4.2 // indirect
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
github.com/lestrrat-go/strftime v1.0.5 // indirect

2
go.sum
View File

@ -169,6 +169,8 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=

View File

@ -40,10 +40,18 @@ func (this *Router) registerAPI() {
apiPrefix + "/v1/account/login",
apiPrefix + "/v1/account/logout",
apiPrefix + "/v1/captcha",
apiPrefix + "/v1/config",
}...)))
v1 := g.Group("/v1")
// Websocket socket接口管理
{
_api := new(api.Websocket)
v1.GET("/ws", _api.Ws)
v1.GET("/pong", _api.Pong)
}
// Captcha 验证码接口管理
v1.GET("/captcha", new(api.Captcha).Captcha)
// Upload 上传接口管理
v1.POST("/upload", new(api.Upload).Upload)
// Account 接口管理
accountV1 := v1.Group("/account")
@ -56,6 +64,8 @@ func (this *Router) registerAPI() {
configV1 := v1.Group("/config")
{
_api := new(api.Config)
configV1.GET("", _api.List)
configV1.POST("/edit", _api.Edit)
configV1.GET("/area", _api.Area)
configV1.POST("/breakdown", _api.Breakdown)
}

View File

@ -2,6 +2,14 @@ package logger
import "github.com/sirupsen/logrus"
func Info(args ...interface{}) {
logger.Log(logrus.InfoLevel, args...)
}
func InfoF(format string, args ...interface{}) {
logger.Logf(logrus.InfoLevel, format, args...)
}
func Warn(args ...interface{}) {
logger.Log(logrus.WarnLevel, args...)
}