Files
iotlcm/server/internal/logic/app/app_work_order.go
T

596 lines
18 KiB
Go

package app
import (
"context"
"regexp"
"strings"
"unicode/utf8"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/appidentity"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
const (
appWorkOrderCreatorTerminal = 1
appWorkOrderCreatorEnterprise = 2
appWorkOrderStatusAssigned = 1
appWorkOrderStatusProcessing = 2
appWorkOrderStatusResolved = 3
appWorkOrderStatusClosed = 4
appWorkOrderStatusCanceled = 5
appWorkOrderOperationCreate = 0
)
type sAppWorkOrder struct{}
func NewAppWorkOrder() *sAppWorkOrder {
return &sAppWorkOrder{}
}
func init() {
service.RegisterAppWorkOrder(NewAppWorkOrder())
}
func (s *sAppWorkOrder) Upload(ctx context.Context) (output *model.SysOssUploadModel, err error) {
if err = appEnsureFeatureAccess(ctx, "workorder:create"); err != nil {
if err = appEnsureFeatureAccess(ctx, "workorder:detail"); err != nil {
return nil, err
}
}
request := g.RequestFromCtx(ctx)
if request == nil {
return nil, gerror.New("上传请求无效")
}
file := request.GetUploadFile("file")
if file == nil {
return nil, gerror.New("请选择附件")
}
if file.Size > 10*1024*1024 {
return nil, gerror.New("单个附件不能超过10MB")
}
originalName, err := appWorkOrderOriginalName(request.GetForm("originalName").String())
if err != nil {
return nil, err
}
output, err = service.SysOss().Upload(ctx, &model.SysOssUploadParam{FileType: "file"})
if err != nil || originalName == "" {
return output, err
}
columns := dao.SysOss.Columns()
if _, err = dao.SysOss.Ctx(ctx).
Where(columns.OssId, output.OssId).
Where(columns.TenantId, output.TenantId).
Data(do.SysOss{OriginalName: originalName}).Update(); err != nil {
return nil, gerror.Wrap(err, "保存工单附件原名失败")
}
output.OriginalName = originalName
return output, nil
}
func (s *sAppWorkOrder) List(ctx context.Context, param *model.AppWorkOrderListParam) (items []*model.AppWorkOrderModel, total int, err error) {
if err = appEnsureFeatureAccess(ctx, "workorder:list"); err != nil {
return nil, 0, err
}
if param == nil {
param = &model.AppWorkOrderListParam{}
}
if param.Page <= 0 {
param.Page = 1
}
if param.PageSize <= 0 {
param.PageSize = 20
}
if param.PageSize > 100 {
param.PageSize = 100
}
user, err := appCurrentUser(ctx)
if err != nil {
return nil, 0, err
}
mod := s.ownerModel(ctx, user)
if param.Scope != "" && param.Scope != "all" && param.Scope != "submitted" && param.Scope != "received" {
return nil, 0, gerror.New("工单范围不正确")
}
if param.Scope == "submitted" {
mod = mod.Where(dao.TWoMain.Columns().CreatorUserId, user.UserId)
}
if param.Scope == "received" {
if appIsTerminalUser(ctx, user) {
return nil, 0, gerror.New("终端账号不能查看待处理工单")
}
columns := dao.TWoMain.Columns()
mod = mod.WhereNot(columns.CreatorUserId, user.UserId).Where("("+columns.HandlerUserId+" = ? OR ("+columns.HandlerDeptId+" = ? AND ? > 0))", user.UserId, user.DeptId, user.DeptId)
}
if param.CurrentStatus < 0 || param.CurrentStatus > 5 {
return nil, 0, gerror.New("工单状态不正确")
}
if param.CurrentStatus > 0 {
mod = mod.Where(dao.TWoMain.Columns().CurrentStatus, param.CurrentStatus)
}
if param.WoType != nil {
if *param.WoType < 0 || *param.WoType > 5 {
return nil, 0, gerror.New("工单类型不正确")
}
mod = mod.Where(dao.TWoMain.Columns().WoType, *param.WoType)
}
keyword := strings.TrimSpace(param.Keyword)
if keyword == "" {
keyword = strings.TrimSpace(param.PageInfo.Keyword)
}
if keyword != "" {
like := "%" + keyword + "%"
columns := dao.TWoMain.Columns()
mod = mod.Where(
"("+columns.WoNo+" LIKE ? OR "+columns.Title+" LIKE ? OR "+columns.Content+" LIKE ?)",
like, like, like,
)
}
total, err = mod.Count()
if err != nil {
return nil, 0, gerror.Wrap(err, "查询工单数量失败")
}
if total == 0 {
return []*model.AppWorkOrderModel{}, 0, nil
}
var rows []*entity.TWoMain
if err = mod.
OrderDesc(dao.TWoMain.Columns().CreatedAt).
OrderDesc(dao.TWoMain.Columns().Id).
Page(param.Page, param.PageSize).
Scan(&rows); err != nil {
return nil, 0, gerror.Wrap(err, "查询工单列表失败")
}
items = make([]*model.AppWorkOrderModel, 0, len(rows))
for _, row := range rows {
if row != nil {
items = append(items, appWorkOrderToModel(row))
}
}
return items, total, nil
}
func (s *sAppWorkOrder) Detail(ctx context.Context, param *model.AppWorkOrderDetailParam) (res *model.AppWorkOrderModel, err error) {
if err = appEnsureFeatureAccess(ctx, "workorder:detail"); err != nil {
return nil, err
}
if param == nil || strings.TrimSpace(param.WoNo) == "" {
return nil, gerror.New("工单编号不能为空")
}
user, err := appCurrentUser(ctx)
if err != nil {
return nil, err
}
var row *entity.TWoMain
if err = s.ownerModel(ctx, user).
Where(dao.TWoMain.Columns().WoNo, strings.TrimSpace(param.WoNo)).
Limit(1).
Scan(&row); err != nil {
return nil, gerror.Wrap(err, "查询工单详情失败")
}
if row == nil || row.Id == 0 {
return nil, gerror.New("工单不存在或无权查看")
}
res = appWorkOrderToModel(row)
var communications []*entity.TWoCommunication
communicationModel := dao.TWoCommunication.Ctx(ctx).
Where(dao.TWoCommunication.Columns().TenantId, user.TenantId).
Where(dao.TWoCommunication.Columns().WoNo, row.WoNo)
if param.BeforeId > 0 {
communicationModel = communicationModel.WhereLT(dao.TWoCommunication.Columns().Id, param.BeforeId)
}
if err = communicationModel.OrderDesc(dao.TWoCommunication.Columns().Id).Limit(21).
Scan(&communications); err != nil {
return nil, gerror.Wrap(err, "查询工单沟通记录失败")
}
res.HasMoreCommunications = len(communications) > 20
if res.HasMoreCommunications {
communications = communications[:20]
}
for left, right := 0, len(communications)-1; left < right; left, right = left+1, right-1 {
communications[left], communications[right] = communications[right], communications[left]
}
res.Communications = appWorkOrderCommunicationsToModels(communications)
urls := append([]string{}, res.AttachmentUrls...)
for _, communication := range communications {
if communication != nil && communication.AttachmentUrls != nil {
urls = append(urls, communication.AttachmentUrls.Var().Strings()...)
}
}
res.AttachmentNames, err = appWorkOrderAttachmentNames(ctx, user.TenantId, urls)
if err != nil {
return nil, err
}
res.Actions = appAvailableWorkOrderActions(ctx, row, user)
res.IsCreator = row.CreatorUserId == user.UserId
var records []*entity.TWoRecord
if err = dao.TWoRecord.Ctx(ctx).Where(dao.TWoRecord.Columns().TenantId, user.TenantId).Where(dao.TWoRecord.Columns().WoNo, row.WoNo).OrderAsc(dao.TWoRecord.Columns().Id).Scan(&records); err != nil {
return nil, gerror.Wrap(err, "查询工单进度失败")
}
for _, record := range records {
res.Records = append(res.Records, &model.AppWorkOrderProgressModel{Id: record.Id, OperationType: record.OperationType, OperationText: appWorkOrderOperationText(record), BeforeStatus: record.BeforeStatus, AfterStatus: record.AfterStatus, CreatedAt: record.CreatedAt})
}
return res, nil
}
var appWorkOrderPhonePattern = regexp.MustCompile(`^1[3-9]\d{9}$`)
func appWorkOrderContactPhone(phone string) (string, error) {
value := strings.TrimSpace(phone)
if value == "" {
return "", gerror.New("请输入创建人手机号")
}
if !appWorkOrderPhonePattern.MatchString(value) {
return "", gerror.New("请输入正确的手机号")
}
return value, nil
}
func (s *sAppWorkOrder) Create(ctx context.Context, param *model.AppWorkOrderCreateParam) (res *model.AppWorkOrderModel, err error) {
if err = appEnsureFeatureAccess(ctx, "workorder:create"); err != nil {
return nil, err
}
if param == nil {
return nil, gerror.New("工单参数不能为空")
}
title := strings.TrimSpace(param.Title)
content := strings.TrimSpace(param.Content)
if title == "" {
return nil, gerror.New("工单标题不能为空")
}
if content == "" {
return nil, gerror.New("问题描述不能为空")
}
if param.WoType < 0 || param.WoType > 5 {
return nil, gerror.New("工单类型不正确")
}
user, err := appCurrentUser(ctx)
if err != nil {
return nil, err
}
creatorPhone, err := appWorkOrderContactPhone(param.CreatorPhone)
if err != nil {
return nil, err
}
if utf8.RuneCountInString(title) > 100 || utf8.RuneCountInString(content) > 2000 {
return nil, gerror.New("标题最多100字,问题描述最多2000字")
}
if err = s.validateRelatedWorkOrder(ctx, user, param); err != nil {
return nil, err
}
if err = appValidateWorkOrderAttachments(param.AttachmentUrls); err != nil {
return nil, err
}
if err = s.validateAttachmentOwnership(ctx, user, param.AttachmentUrls); err != nil {
return nil, err
}
now := gtime.Now()
woNo := appCardBatchNo("WO", user.UserId, now)
createdDept := user.DeptId
if createdDept <= 0 {
createdDept = consts.AppTerminalDeptId
}
creatorType := appWorkOrderCreatorEnterprise
if appIsTerminalUser(ctx, user) {
creatorType = appWorkOrderCreatorTerminal
}
creatorName := strings.TrimSpace(appFirstNotEmpty(user.NickName, user.UserName))
handlerDeptId, handlerUserId, err := appWorkOrderHandler(ctx, user, createdDept)
if err != nil {
return nil, err
}
var created *entity.TWoMain
err = dao.TWoMain.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
_, err := dao.TWoMain.Ctx(ctx).TX(tx).Data(do.TWoMain{
WoNo: woNo,
WoType: param.WoType,
RelatedId: param.RelatedId,
Title: title,
Content: content,
Attachment: appWorkOrderEncodeAttachments(param.AttachmentUrls),
CreatorType: creatorType,
CreatorDeptId: createdDept,
CreatorUserId: user.UserId,
CreatorName: creatorName,
CreatorPhone: creatorPhone,
AssignDeptId: handlerDeptId,
AssignUserId: handlerUserId,
CurrentStatus: appWorkOrderStatusProcessing,
HandlerDeptId: handlerDeptId,
HandlerUserId: handlerUserId,
TenantId: user.TenantId,
CreatedDept: createdDept,
CreatedBy: user.UserId,
CreatedAt: now,
UpdatedBy: user.UserId,
UpdatedAt: now,
}).Insert()
if err != nil {
return gerror.Wrap(err, "创建工单失败")
}
communicationData := do.TWoCommunication{
WoNo: woNo,
CommType: 1,
SenderDeptId: createdDept,
SenderUserId: user.UserId,
SenderName: creatorName,
Content: content,
TenantId: user.TenantId,
CreatedDept: createdDept,
CreatedBy: user.UserId,
CreatedAt: now,
UpdatedBy: user.UserId,
UpdatedAt: now,
}
if len(param.AttachmentUrls) > 0 {
communicationData.AttachmentUrls = gjson.New(param.AttachmentUrls)
}
if _, err = dao.TWoCommunication.Ctx(ctx).TX(tx).Data(communicationData).Insert(); err != nil {
return gerror.Wrap(err, "创建工单沟通记录失败")
}
if _, err = dao.TWoRecord.Ctx(ctx).TX(tx).Data(do.TWoRecord{
WoNo: woNo,
OperationType: appWorkOrderOperationCreate,
OperationDeptId: createdDept,
OperationUserId: user.UserId,
BeforeStatus: 0,
AfterStatus: appWorkOrderStatusProcessing,
TenantId: user.TenantId,
CreatedDept: createdDept,
CreatedBy: user.UserId,
CreatedAt: now,
UpdatedBy: user.UserId,
UpdatedAt: now,
}).Insert(); err != nil {
return gerror.Wrap(err, "创建工单操作记录失败")
}
if err := dao.TWoMain.Ctx(ctx).TX(tx).
Where(dao.TWoMain.Columns().TenantId, user.TenantId).
Where(dao.TWoMain.Columns().CreatorUserId, user.UserId).
Where(dao.TWoMain.Columns().WoNo, woNo).
Scan(&created); err != nil {
return gerror.Wrap(err, "读取新建工单失败")
}
return nil
})
if err != nil {
return nil, err
}
return appWorkOrderToModel(created), nil
}
func (s *sAppWorkOrder) ownerModel(ctx context.Context, user *entity.SysUser) *gdb.Model {
columns := dao.TWoMain.Columns()
mod := dao.TWoMain.Ctx(ctx).
Where(columns.TenantId, user.TenantId)
terminal, identityErr := appidentity.IsTerminalUser(ctx, user)
if identityErr != nil {
g.Log().Errorf(ctx, "工单用户身份查询失败: %v", identityErr)
return mod.Where("1 = 0")
}
if terminal {
return mod.Where(columns.CreatorUserId, user.UserId)
}
scope, err := appResolveEnterpriseDataScope(ctx, user)
if err != nil {
g.Log().Errorf(ctx, "work order data scope: %v", err)
return mod.Where("1 = 0")
}
if scope.All {
return mod
}
condition := ""
args := []any{}
if !scope.Configured {
condition = columns.CreatorDeptId + " = ? OR " + columns.CreatorUserId + " = ?"
args = append(args, user.DeptId, user.UserId)
} else {
if len(scope.DeptIds) > 0 {
condition = columns.CreatorDeptId + " IN (?)"
args = append(args, scope.DeptIds)
}
if len(scope.UserIds) > 0 {
if condition != "" {
condition += " OR "
}
condition += columns.CreatorUserId + " IN (?)"
args = append(args, scope.UserIds)
}
}
if condition != "" {
condition += " OR "
}
condition += columns.HandlerUserId + " = ?"
args = append(args, user.UserId)
if user.DeptId > 0 {
condition += " OR " + columns.HandlerDeptId + " = ?"
args = append(args, user.DeptId)
}
return mod.Where("("+condition+")", args...)
}
func appWorkOrderHandler(ctx context.Context, user *entity.SysUser, createdDept int64) (deptId int64, userId int64, err error) {
if user == nil || strings.TrimSpace(user.TenantId) == "" || createdDept <= 0 {
return 0, 0, gerror.New("当前账号未配置有效的工单所属部门")
}
columns := dao.SysDept.Columns()
var currentDept *entity.SysDept
if err = dao.SysDept.Ctx(ctx).
Fields(columns.DeptId, columns.ParentId, columns.Leader, columns.Status).
Where(columns.TenantId, user.TenantId).
Where(columns.DeptId, createdDept).
WhereNull(columns.DeletedAt).
Limit(1).
Scan(&currentDept); err != nil {
return 0, 0, gerror.Wrap(err, "查询工单所属部门失败")
}
if currentDept == nil || currentDept.DeptId <= 0 || currentDept.Status != "0" {
return 0, 0, gerror.New("工单所属部门不存在或已停用")
}
if currentDept.ParentId == 0 {
return currentDept.DeptId, currentDept.Leader, nil
}
if currentDept.ParentId == currentDept.DeptId {
return 0, 0, gerror.New("工单所属部门的上级配置异常")
}
var parentDept *entity.SysDept
if err = dao.SysDept.Ctx(ctx).
Fields(columns.DeptId, columns.Leader, columns.Status).
Where(columns.TenantId, user.TenantId).
Where(columns.DeptId, currentDept.ParentId).
WhereNull(columns.DeletedAt).
Limit(1).
Scan(&parentDept); err != nil {
return 0, 0, gerror.Wrap(err, "查询上级工单处理部门失败")
}
if parentDept == nil || parentDept.DeptId <= 0 || parentDept.Status != "0" {
return 0, 0, gerror.New("上级工单处理部门不存在或已停用,请联系管理员")
}
return parentDept.DeptId, parentDept.Leader, nil
}
func appWorkOrderToModel(row *entity.TWoMain) *model.AppWorkOrderModel {
if row == nil {
return nil
}
return &model.AppWorkOrderModel{
Id: row.Id,
Actions: []string{},
ResolveTime: row.ResolveTime,
CloseTime: row.CloseTime,
SatisfactionLevel: row.SatisfactionLevel,
EvalContent: row.EvalContent,
EvalTime: row.EvalTime,
WoNo: row.WoNo,
WoType: row.WoType,
WoTypeText: appWorkOrderTypeText(row.WoType),
RelatedId: row.RelatedId,
Title: row.Title,
Content: row.Content,
AttachmentUrls: decodeAttachmentUrls(row.Attachment),
CreatorType: row.CreatorType,
CreatorName: row.CreatorName,
CreatorPhone: row.CreatorPhone,
CurrentStatus: row.CurrentStatus,
CurrentStatusText: appWorkOrderStatusText(row.CurrentStatus),
StatusType: appWorkOrderStatusType(row.CurrentStatus),
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
}
}
func appWorkOrderCommunicationsToModels(rows []*entity.TWoCommunication) []*model.AppWorkOrderCommModel {
items := make([]*model.AppWorkOrderCommModel, 0, len(rows))
for _, row := range rows {
if row == nil {
continue
}
items = append(items, &model.AppWorkOrderCommModel{
Id: row.Id,
WoNo: row.WoNo,
CommType: row.CommType,
CommTypeText: appWorkOrderCommTypeText(row.CommType),
SenderName: row.SenderName,
Content: row.Content,
AttachmentUrls: row.AttachmentUrls,
CreatedAt: row.CreatedAt,
})
}
return items
}
func decodeAttachmentUrls(value string) []string {
value = strings.TrimSpace(value)
if value == "" {
return nil
}
var urls []string
if err := gjson.DecodeTo(value, &urls); err == nil && (strings.HasPrefix(value, "[") || value == "null") {
return urls
}
return []string{value}
}
func appWorkOrderTypeText(value int) string {
switch value {
case 0:
return "卡片"
case 1:
return "模组"
case 2:
return "设备"
case 3:
return "订单"
case 4:
return "余额"
case 5:
return "提现"
default:
return "其他"
}
}
func appWorkOrderStatusText(value int) string {
switch value {
case appWorkOrderStatusAssigned:
return "已分派"
case appWorkOrderStatusProcessing:
return "处理中"
case appWorkOrderStatusResolved:
return "已解决"
case appWorkOrderStatusClosed:
return "已完结"
case appWorkOrderStatusCanceled:
return "已取消"
default:
return "待处理"
}
}
func appWorkOrderStatusType(value int) string {
switch value {
case appWorkOrderStatusResolved, appWorkOrderStatusClosed:
return "success"
case appWorkOrderStatusCanceled:
return "danger"
case appWorkOrderStatusProcessing:
return "primary"
default:
return "warning"
}
}
func appWorkOrderCommTypeText(value int) string {
if value == 1 {
return "反馈"
}
return "回复"
}