Browse Source

feat: 部门会议管理 + 排期人员分组 + 工时统计增加会议工时 + 发版完成对话框修复

- 新增会议管理模块(Model/Service/Handler/DAO),含plat_meeting/plat_meeting_attendee/plat_meeting_work_hour三表
- 排期统计人员分组(Biobank/LIMS/CellSop/BiobankV4/品质部),仅统计17人
- 工时统计UNION ALL增加plat_meeting_work_hour查询,DayData/PersonRow增加MtHour字段
- 发版完成对话框:去掉projectId为空时的静默退出 + release_version过滤兼容空字符串
- 合同申请模块(Model/DAO/Service/Handler)
- 交付项目管理优化
- 运营事件优化
程健 1 month ago
parent
commit
2a9a6f2731
35 changed files with 3358 additions and 32 deletions
  1. 2 2
      opms_admin/config/config.toml
  2. 36 0
      opms_parent/app/dao/contract/ctr_contract_application.go
  3. 438 0
      opms_parent/app/dao/contract/internal/ctr_contract_application.go
  4. 444 0
      opms_parent/app/dao/opsdev/internal/plat_meeting.go
  5. 429 0
      opms_parent/app/dao/opsdev/internal/plat_meeting_attendee.go
  6. 438 0
      opms_parent/app/dao/opsdev/internal/plat_meeting_work_hour.go
  7. 36 0
      opms_parent/app/dao/opsdev/plat_meeting.go
  8. 36 0
      opms_parent/app/dao/opsdev/plat_meeting_attendee.go
  9. 36 0
      opms_parent/app/dao/opsdev/plat_meeting_work_hour.go
  10. 38 0
      opms_parent/app/handler/contract/ctr_contract_application.go
  11. 60 0
      opms_parent/app/handler/opsdev/delivery_project.go
  12. 143 0
      opms_parent/app/handler/opsdev/plat_meeting.go
  13. 20 0
      opms_parent/app/model/contract/ctr_contract_application.go
  14. 28 0
      opms_parent/app/model/contract/internal/ctr_contract_application.go
  15. 30 0
      opms_parent/app/model/opsdev/internal/plat_meeting.go
  16. 25 0
      opms_parent/app/model/opsdev/internal/plat_meeting_attendee.go
  17. 28 0
      opms_parent/app/model/opsdev/internal/plat_meeting_work_hour.go
  18. 6 0
      opms_parent/app/model/opsdev/operation.go
  19. 15 0
      opms_parent/app/model/opsdev/ops_delivery_project.go
  20. 17 1
      opms_parent/app/model/opsdev/ops_delivery_project_delegate.go
  21. 1 0
      opms_parent/app/model/opsdev/ops_event_task.go
  22. 113 0
      opms_parent/app/model/opsdev/plat_meeting.go
  23. 14 0
      opms_parent/app/model/opsdev/plat_meeting_attendee.go
  24. 14 0
      opms_parent/app/model/opsdev/plat_meeting_work_hour.go
  25. 7 5
      opms_parent/app/model/opsdev/work_hour_stat.go
  26. 78 0
      opms_parent/app/service/contract/ctr_contract_application.go
  27. 194 0
      opms_parent/app/service/opsdev/delivery_project.go
  28. 14 12
      opms_parent/app/service/opsdev/operation.go
  29. 54 1
      opms_parent/app/service/opsdev/ops_event_task.go
  30. 465 0
      opms_parent/app/service/opsdev/plat_meeting.go
  31. 19 9
      opms_parent/app/service/opsdev/work_hour_stat.go
  32. 2 2
      opms_parent/config/config.toml
  33. 2 0
      opms_parent/main.go
  34. BIN
      opms_parent/oms_parent_v2
  35. 76 0
      opms_parent/schema/migration_plat_meeting.sql

+ 2 - 2
opms_admin/config/config.toml

@@ -1,11 +1,11 @@
 # 应用系统设置
 [setting]
     logpath = "./log/admin"
-    bind-addr = "192.168.0.188:8001"
+    bind-addr = "192.168.0.123:8001"
     need-advertise-addr = false
     srv-name = "dashoo.opms.admin-0.0.1-cj"
     env = "dev"
-    websocket-addr = "192.168.0.188:8899"
+    websocket-addr = "192.168.0.123:8899"
 
 [logger.websocket]
     path   = "./log/websocket"

+ 36 - 0
opms_parent/app/dao/contract/ctr_contract_application.go

@@ -0,0 +1,36 @@
+// ============================================================================
+// This is auto-generated by gf cli tool only once. Fill this file as you wish.
+// ============================================================================
+
+package dao
+
+import (
+	"dashoo.cn/opms_parent/app/dao/contract/internal"
+)
+
+// ctrContractApplicationDao is the manager for logic model data accessing
+// and custom defined data operations functions management. You can define
+// methods on it to extend its functionality as you wish.
+type ctrContractApplicationDao struct {
+	internal.CtrContractApplicationDao
+}
+
+var (
+	// CtrContractApplication is globally public accessible object for table ctr_contract_application operations.
+	CtrContractApplication = ctrContractApplicationDao{
+		internal.CtrContractApplication,
+	}
+)
+
+type CtrContractApplicationDao struct {
+	internal.CtrContractApplicationDao
+}
+
+func NewCtrContractApplicationDao(tenant string) *CtrContractApplicationDao {
+	dao := internal.NewCtrContractApplicationDao(tenant)
+	return &CtrContractApplicationDao{
+		dao,
+	}
+}
+
+// Fill with you ideas below.

+ 438 - 0
opms_parent/app/dao/contract/internal/ctr_contract_application.go

@@ -0,0 +1,438 @@
+// ==========================================================================
+// This is auto-generated by gf cli tool. DO NOT EDIT THIS FILE MANUALLY.
+// ==========================================================================
+
+package internal
+
+import (
+	"context"
+	"database/sql"
+	"time"
+
+	"dashoo.cn/opms_parent/app/model/contract"
+	"github.com/gogf/gf/database/gdb"
+	"github.com/gogf/gf/frame/g"
+	"github.com/gogf/gf/frame/gmvc"
+)
+
+// CtrContractApplicationDao is the manager for logic model data accessing
+// and custom defined data operations functions management.
+type CtrContractApplicationDao struct {
+	gmvc.M
+	DB      gdb.DB
+	Table   string
+	Columns ctrContractApplicationColumns
+}
+
+// CtrContractApplicationColumns defines and stores column names for table ctr_contract_application.
+type ctrContractApplicationColumns struct {
+	Id           string // 主键
+	NboId        string // 关联项目
+	NboName      string // 项目名称
+	ContractId   string // 关联合同
+	ContractCode string // 合同编号
+	ContractName string // 合同名称
+	Status       string // 审核状态 10 待登记 20 已登记
+	Remark       string // 备注
+	CreatedBy    string // 创建者
+	CreatedName  string // 创建人
+	CreatedTime  string // 创建时间
+	UpdatedBy    string // 更新者
+	UpdatedName  string // 更新人
+	UpdatedTime  string // 更新时间
+	DeletedTime  string // 删除时间
+}
+
+var (
+	// CtrContractApplication is globally public accessible object for table ctr_contract_application operations.
+	CtrContractApplication = CtrContractApplicationDao{
+		M:     g.DB("default").Model("ctr_contract_application").Safe(),
+		DB:    g.DB("default"),
+		Table: "ctr_contract_application",
+		Columns: ctrContractApplicationColumns{
+			Id:           "id",
+			NboId:        "nbo_id",
+			NboName:      "nbo_name",
+			ContractId:   "contract_id",
+			ContractCode: "contract_code",
+			ContractName: "contract_name",
+			Status:       "status",
+			Remark:       "remark",
+			CreatedBy:    "created_by",
+			CreatedName:  "created_name",
+			CreatedTime:  "created_time",
+			UpdatedBy:    "updated_by",
+			UpdatedName:  "updated_name",
+			UpdatedTime:  "updated_time",
+			DeletedTime:  "deleted_time",
+		},
+	}
+)
+
+func NewCtrContractApplicationDao(tenant string) CtrContractApplicationDao {
+	var dao CtrContractApplicationDao
+	dao = CtrContractApplicationDao{
+		M:     g.DB(tenant).Model("ctr_contract_application").Safe(),
+		DB:    g.DB(tenant),
+		Table: "ctr_contract_application",
+		Columns: ctrContractApplicationColumns{
+			Id:           "id",
+			NboId:        "nbo_id",
+			NboName:      "nbo_name",
+			ContractId:   "contract_id",
+			ContractCode: "contract_code",
+			ContractName: "contract_name",
+			Status:       "status",
+			Remark:       "remark",
+			CreatedBy:    "created_by",
+			CreatedName:  "created_name",
+			CreatedTime:  "created_time",
+			UpdatedBy:    "updated_by",
+			UpdatedName:  "updated_name",
+			UpdatedTime:  "updated_time",
+			DeletedTime:  "deleted_time",
+		},
+	}
+	return dao
+}
+
+// Ctx is a chaining function, which creates and returns a new DB that is a shallow copy
+// of current DB object and with given context in it.
+// Note that this returned DB object can be used only once, so do not assign it to
+// a global or package variable for long using.
+func (d *CtrContractApplicationDao) Ctx(ctx context.Context) *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.Ctx(ctx)}
+}
+
+// As sets an alias name for current table.
+func (d *CtrContractApplicationDao) As(as string) *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.As(as)}
+}
+
+// TX sets the transaction for current operation.
+func (d *CtrContractApplicationDao) TX(tx *gdb.TX) *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.TX(tx)}
+}
+
+// Master marks the following operation on master node.
+func (d *CtrContractApplicationDao) Master() *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.Master()}
+}
+
+// Slave marks the following operation on slave node.
+// Note that it makes sense only if there's any slave node configured.
+func (d *CtrContractApplicationDao) Slave() *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.Slave()}
+}
+
+// Args sets custom arguments for model operation.
+func (d *CtrContractApplicationDao) Args(args ...interface{}) *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.Args(args...)}
+}
+
+// LeftJoin does "LEFT JOIN ... ON ..." statement on the model.
+// The parameter <table> can be joined table and its joined condition,
+// and also with its alias name, like:
+// Table("user").LeftJoin("user_detail", "user_detail.uid=user.uid")
+// Table("user", "u").LeftJoin("user_detail", "ud", "ud.uid=u.uid")
+func (d *CtrContractApplicationDao) LeftJoin(table ...string) *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.LeftJoin(table...)}
+}
+
+// RightJoin does "RIGHT JOIN ... ON ..." statement on the model.
+// The parameter <table> can be joined table and its joined condition,
+// and also with its alias name, like:
+// Table("user").RightJoin("user_detail", "user_detail.uid=user.uid")
+// Table("user", "u").RightJoin("user_detail", "ud", "ud.uid=u.uid")
+func (d *CtrContractApplicationDao) RightJoin(table ...string) *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.RightJoin(table...)}
+}
+
+// InnerJoin does "INNER JOIN ... ON ..." statement on the model.
+// The parameter <table> can be joined table and its joined condition,
+// and also with its alias name, like:
+// Table("user").InnerJoin("user_detail", "user_detail.uid=user.uid")
+// Table("user", "u").InnerJoin("user_detail", "ud", "ud.uid=u.uid")
+func (d *CtrContractApplicationDao) InnerJoin(table ...string) *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.InnerJoin(table...)}
+}
+
+// Fields sets the operation fields of the model, multiple fields joined using char ','.
+// The parameter <fieldNamesOrMapStruct> can be type of string/map/*map/struct/*struct.
+func (d *CtrContractApplicationDao) Fields(fieldNamesOrMapStruct ...interface{}) *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.Fields(fieldNamesOrMapStruct...)}
+}
+
+// FieldsEx sets the excluded operation fields of the model, multiple fields joined using char ','.
+// The parameter <fieldNamesOrMapStruct> can be type of string/map/*map/struct/*struct.
+func (d *CtrContractApplicationDao) FieldsEx(fieldNamesOrMapStruct ...interface{}) *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.FieldsEx(fieldNamesOrMapStruct...)}
+}
+
+// Option sets the extra operation option for the model.
+func (d *CtrContractApplicationDao) Option(option int) *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.Option(option)}
+}
+
+// OmitEmpty sets OPTION_OMITEMPTY option for the model, which automatically filers
+// the data and where attributes for empty values.
+func (d *CtrContractApplicationDao) OmitEmpty() *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.OmitEmpty()}
+}
+
+// Filter marks filtering the fields which does not exist in the fields of the operated table.
+func (d *CtrContractApplicationDao) Filter() *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.Filter()}
+}
+
+// Where sets the condition statement for the model. The parameter <where> can be type of
+// string/map/gmap/slice/struct/*struct, etc. Note that, if it's called more than one times,
+// multiple conditions will be joined into where statement using "AND".
+// Eg:
+// Where("uid=10000")
+// Where("uid", 10000)
+// Where("money>? AND name like ?", 99999, "vip_%")
+// Where("uid", 1).Where("name", "john")
+// Where("status IN (?)", g.Slice{1,2,3})
+// Where("age IN(?,?)", 18, 50)
+// Where(User{ Id : 1, UserName : "john"})
+func (d *CtrContractApplicationDao) Where(where interface{}, args ...interface{}) *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.Where(where, args...)}
+}
+
+// WherePri does the same logic as M.Where except that if the parameter <where>
+// is a single condition like int/string/float/slice, it treats the condition as the primary
+// key value. That is, if primary key is "id" and given <where> parameter as "123", the
+// WherePri function treats the condition as "id=123", but M.Where treats the condition
+// as string "123".
+func (d *CtrContractApplicationDao) WherePri(where interface{}, args ...interface{}) *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.WherePri(where, args...)}
+}
+
+// And adds "AND" condition to the where statement.
+func (d *CtrContractApplicationDao) And(where interface{}, args ...interface{}) *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.And(where, args...)}
+}
+
+// Or adds "OR" condition to the where statement.
+func (d *CtrContractApplicationDao) Or(where interface{}, args ...interface{}) *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.Or(where, args...)}
+}
+
+// Group sets the "GROUP BY" statement for the model.
+func (d *CtrContractApplicationDao) Group(groupBy string) *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.Group(groupBy)}
+}
+
+// Order sets the "ORDER BY" statement for the model.
+func (d *CtrContractApplicationDao) Order(orderBy ...string) *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.Order(orderBy...)}
+}
+
+// Limit sets the "LIMIT" statement for the model.
+// The parameter <limit> can be either one or two number, if passed two number is passed,
+// it then sets "LIMIT limit[0],limit[1]" statement for the model, or else it sets "LIMIT limit[0]"
+// statement.
+func (d *CtrContractApplicationDao) Limit(limit ...int) *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.Limit(limit...)}
+}
+
+// Offset sets the "OFFSET" statement for the model.
+// It only makes sense for some databases like SQLServer, PostgreSQL, etc.
+func (d *CtrContractApplicationDao) Offset(offset int) *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.Offset(offset)}
+}
+
+// Page sets the paging number for the model.
+// The parameter <page> is started from 1 for paging.
+// Note that, it differs that the Limit function start from 0 for "LIMIT" statement.
+func (d *CtrContractApplicationDao) Page(page, limit int) *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.Page(page, limit)}
+}
+
+// Batch sets the batch operation number for the model.
+func (d *CtrContractApplicationDao) Batch(batch int) *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.Batch(batch)}
+}
+
+// Cache sets the cache feature for the model. It caches the result of the sql, which means
+// if there's another same sql request, it just reads and returns the result from cache, it
+// but not committed and executed into the database.
+//
+// If the parameter <duration> < 0, which means it clear the cache with given <name>.
+// If the parameter <duration> = 0, which means it never expires.
+// If the parameter <duration> > 0, which means it expires after <duration>.
+//
+// The optional parameter <name> is used to bind a name to the cache, which means you can later
+// control the cache like changing the <duration> or clearing the cache with specified <name>.
+//
+// Note that, the cache feature is disabled if the model is operating on a transaction.
+func (d *CtrContractApplicationDao) Cache(duration time.Duration, name ...string) *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.Cache(duration, name...)}
+}
+
+// Data sets the operation data for the model.
+// The parameter <data> can be type of string/map/gmap/slice/struct/*struct, etc.
+// Eg:
+// Data("uid=10000")
+// Data("uid", 10000)
+// Data(g.Map{"uid": 10000, "name":"john"})
+// Data(g.Slice{g.Map{"uid": 10000, "name":"john"}, g.Map{"uid": 20000, "name":"smith"})
+func (d *CtrContractApplicationDao) Data(data ...interface{}) *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.Data(data...)}
+}
+
+// All does "SELECT FROM ..." statement for the model.
+// It retrieves the records from table and returns the result as []*model.CtrContractApplication.
+// It returns nil if there's no record retrieved with the given conditions from table.
+//
+// The optional parameter <where> is the same as the parameter of M.Where function,
+// see M.Where.
+func (d *CtrContractApplicationDao) All(where ...interface{}) ([]*contract.CtrContractApplication, error) {
+	all, err := d.M.All(where...)
+	if err != nil {
+		return nil, err
+	}
+	var entities []*contract.CtrContractApplication
+	if err = all.Structs(&entities); err != nil && err != sql.ErrNoRows {
+		return nil, err
+	}
+	return entities, nil
+}
+
+// One retrieves one record from table and returns the result as *model.CtrContractApplication.
+// It returns nil if there's no record retrieved with the given conditions from table.
+//
+// The optional parameter <where> is the same as the parameter of M.Where function,
+// see M.Where.
+func (d *CtrContractApplicationDao) One(where ...interface{}) (*contract.CtrContractApplication, error) {
+	one, err := d.M.One(where...)
+	if err != nil {
+		return nil, err
+	}
+	var entity *contract.CtrContractApplication
+	if err = one.Struct(&entity); err != nil && err != sql.ErrNoRows {
+		return nil, err
+	}
+	return entity, nil
+}
+
+// FindOne retrieves and returns a single Record by M.WherePri and M.One.
+// Also see M.WherePri and M.One.
+func (d *CtrContractApplicationDao) FindOne(where ...interface{}) (*contract.CtrContractApplication, error) {
+	one, err := d.M.FindOne(where...)
+	if err != nil {
+		return nil, err
+	}
+	var entity *contract.CtrContractApplication
+	if err = one.Struct(&entity); err != nil && err != sql.ErrNoRows {
+		return nil, err
+	}
+	return entity, nil
+}
+
+// FindAll retrieves and returns Result by by M.WherePri and M.All.
+// Also see M.WherePri and M.All.
+func (d *CtrContractApplicationDao) FindAll(where ...interface{}) ([]*contract.CtrContractApplication, error) {
+	all, err := d.M.FindAll(where...)
+	if err != nil {
+		return nil, err
+	}
+	var entities []*contract.CtrContractApplication
+	if err = all.Structs(&entities); err != nil && err != sql.ErrNoRows {
+		return nil, err
+	}
+	return entities, nil
+}
+
+// Struct retrieves one record from table and converts it into given struct.
+// The parameter <pointer> should be type of *struct/**struct. If type **struct is given,
+// it can create the struct internally during converting.
+//
+// The optional parameter <where> is the same as the parameter of Model.Where function,
+// see Model.Where.
+//
+// Note that it returns sql.ErrNoRows if there's no record retrieved with the given conditions
+// from table and <pointer> is not nil.
+//
+// Eg:
+// user := new(User)
+// err  := dao.User.Where("id", 1).Struct(user)
+//
+// user := (*User)(nil)
+// err  := dao.User.Where("id", 1).Struct(&user)
+func (d *CtrContractApplicationDao) Struct(pointer interface{}, where ...interface{}) error {
+	return d.M.Struct(pointer, where...)
+}
+
+// Structs retrieves records from table and converts them into given struct slice.
+// The parameter <pointer> should be type of *[]struct/*[]*struct. It can create and fill the struct
+// slice internally during converting.
+//
+// The optional parameter <where> is the same as the parameter of Model.Where function,
+// see Model.Where.
+//
+// Note that it returns sql.ErrNoRows if there's no record retrieved with the given conditions
+// from table and <pointer> is not empty.
+//
+// Eg:
+// users := ([]User)(nil)
+// err   := dao.User.Structs(&users)
+//
+// users := ([]*User)(nil)
+// err   := dao.User.Structs(&users)
+func (d *CtrContractApplicationDao) Structs(pointer interface{}, where ...interface{}) error {
+	return d.M.Structs(pointer, where...)
+}
+
+// Scan automatically calls Struct or Structs function according to the type of parameter <pointer>.
+// It calls function Struct if <pointer> is type of *struct/**struct.
+// It calls function Structs if <pointer> is type of *[]struct/*[]*struct.
+//
+// The optional parameter <where> is the same as the parameter of Model.Where function,
+// see Model.Where.
+//
+// Note that it returns sql.ErrNoRows if there's no record retrieved and given pointer is not empty or nil.
+//
+// Eg:
+// user  := new(User)
+// err   := dao.User.Where("id", 1).Scan(user)
+//
+// user  := (*User)(nil)
+// err   := dao.User.Where("id", 1).Scan(&user)
+//
+// users := ([]User)(nil)
+// err   := dao.User.Scan(&users)
+//
+// users := ([]*User)(nil)
+// err   := dao.User.Scan(&users)
+func (d *CtrContractApplicationDao) Scan(pointer interface{}, where ...interface{}) error {
+	return d.M.Scan(pointer, where...)
+}
+
+// Chunk iterates the table with given size and callback function.
+func (d *CtrContractApplicationDao) Chunk(limit int, callback func(entities []*contract.CtrContractApplication, err error) bool) {
+	d.M.Chunk(limit, func(result gdb.Result, err error) bool {
+		var entities []*contract.CtrContractApplication
+		err = result.Structs(&entities)
+		if err == sql.ErrNoRows {
+			return false
+		}
+		return callback(entities, err)
+	})
+}
+
+// LockUpdate sets the lock for update for current operation.
+func (d *CtrContractApplicationDao) LockUpdate() *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.LockUpdate()}
+}
+
+// LockShared sets the lock in share mode for current operation.
+func (d *CtrContractApplicationDao) LockShared() *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.LockShared()}
+}
+
+// Unscoped enables/disables the soft deleting feature.
+func (d *CtrContractApplicationDao) Unscoped() *CtrContractApplicationDao {
+	return &CtrContractApplicationDao{M: d.M.Unscoped()}
+}

+ 444 - 0
opms_parent/app/dao/opsdev/internal/plat_meeting.go

@@ -0,0 +1,444 @@
+// ==========================================================================
+// This is auto-generated by gf cli tool. DO NOT EDIT THIS FILE MANUALLY.
+// ==========================================================================
+
+package internal
+
+import (
+	"context"
+	"database/sql"
+	"time"
+
+	"dashoo.cn/opms_parent/app/model/opsdev"
+	"github.com/gogf/gf/database/gdb"
+	"github.com/gogf/gf/frame/g"
+	"github.com/gogf/gf/frame/gmvc"
+)
+
+// PlatMeetingDao is the manager for logic model data accessing
+// and custom defined data operations functions management.
+type PlatMeetingDao struct {
+	gmvc.M
+	DB      gdb.DB
+	Table   string
+	Columns platMeetingColumns
+}
+
+// PlatMeetingColumns defines and stores column names for table plat_meeting.
+type platMeetingColumns struct {
+	Id             string // 主键
+	MeetingTitle   string // 会议标题
+	MeetingContent string // 会议内容/议题
+	MeetingDate    string // 会议开始时间
+	MeetingDateEnd string // 会议结束时间
+	Duration       string // 会议时长(小时)
+	OrganizerId    string // 会议负责人ID
+	OrganizerName  string // 会议负责人姓名
+	DeptId         string // 所属部门ID
+	Remark         string // 备注
+	CreatedBy      string // 创建者
+	CreatedName    string // 创建人
+	CreatedTime    string // 创建时间
+	UpdatedBy      string // 更新者
+	UpdatedName    string // 更新人
+	UpdatedTime    string // 更新时间
+	DeletedTime    string // 删除时间
+}
+
+var (
+	// PlatMeeting is globally public accessible object for table plat_meeting operations.
+	PlatMeeting = PlatMeetingDao{
+		M:     g.DB("default").Model("plat_meeting").Safe(),
+		DB:    g.DB("default"),
+		Table: "plat_meeting",
+		Columns: platMeetingColumns{
+			Id:             "id",
+			MeetingTitle:   "meeting_title",
+			MeetingContent: "meeting_content",
+			MeetingDate:    "meeting_date",
+			MeetingDateEnd: "meeting_date_end",
+			Duration:       "duration",
+			OrganizerId:    "organizer_id",
+			OrganizerName:  "organizer_name",
+			DeptId:         "dept_id",
+			Remark:         "remark",
+			CreatedBy:      "created_by",
+			CreatedName:    "created_name",
+			CreatedTime:    "created_time",
+			UpdatedBy:      "updated_by",
+			UpdatedName:    "updated_name",
+			UpdatedTime:    "updated_time",
+			DeletedTime:    "deleted_time",
+		},
+	}
+)
+
+func NewPlatMeetingDao(tenant string) PlatMeetingDao {
+	var dao PlatMeetingDao
+	dao = PlatMeetingDao{
+		M:     g.DB(tenant).Model("plat_meeting").Safe(),
+		DB:    g.DB(tenant),
+		Table: "plat_meeting",
+		Columns: platMeetingColumns{
+			Id:             "id",
+			MeetingTitle:   "meeting_title",
+			MeetingContent: "meeting_content",
+			MeetingDate:    "meeting_date",
+			MeetingDateEnd: "meeting_date_end",
+			Duration:       "duration",
+			OrganizerId:    "organizer_id",
+			OrganizerName:  "organizer_name",
+			DeptId:         "dept_id",
+			Remark:         "remark",
+			CreatedBy:      "created_by",
+			CreatedName:    "created_name",
+			CreatedTime:    "created_time",
+			UpdatedBy:      "updated_by",
+			UpdatedName:    "updated_name",
+			UpdatedTime:    "updated_time",
+			DeletedTime:    "deleted_time",
+		},
+	}
+	return dao
+}
+
+// Ctx is a chaining function, which creates and returns a new DB that is a shallow copy
+// of current DB object and with given context in it.
+// Note that this returned DB object can be used only once, so do not assign it to
+// a global or package variable for long using.
+func (d *PlatMeetingDao) Ctx(ctx context.Context) *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.Ctx(ctx)}
+}
+
+// As sets an alias name for current table.
+func (d *PlatMeetingDao) As(as string) *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.As(as)}
+}
+
+// TX sets the transaction for current operation.
+func (d *PlatMeetingDao) TX(tx *gdb.TX) *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.TX(tx)}
+}
+
+// Master marks the following operation on master node.
+func (d *PlatMeetingDao) Master() *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.Master()}
+}
+
+// Slave marks the following operation on slave node.
+// Note that it makes sense only if there's any slave node configured.
+func (d *PlatMeetingDao) Slave() *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.Slave()}
+}
+
+// Args sets custom arguments for model operation.
+func (d *PlatMeetingDao) Args(args ...interface{}) *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.Args(args...)}
+}
+
+// LeftJoin does "LEFT JOIN ... ON ..." statement on the model.
+// The parameter <table> can be joined table and its joined condition,
+// and also with its alias name, like:
+// Table("user").LeftJoin("user_detail", "user_detail.uid=user.uid")
+// Table("user", "u").LeftJoin("user_detail", "ud", "ud.uid=u.uid")
+func (d *PlatMeetingDao) LeftJoin(table ...string) *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.LeftJoin(table...)}
+}
+
+// RightJoin does "RIGHT JOIN ... ON ..." statement on the model.
+// The parameter <table> can be joined table and its joined condition,
+// and also with its alias name, like:
+// Table("user").RightJoin("user_detail", "user_detail.uid=user.uid")
+// Table("user", "u").RightJoin("user_detail", "ud", "ud.uid=u.uid")
+func (d *PlatMeetingDao) RightJoin(table ...string) *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.RightJoin(table...)}
+}
+
+// InnerJoin does "INNER JOIN ... ON ..." statement on the model.
+// The parameter <table> can be joined table and its joined condition,
+// and also with its alias name, like:
+// Table("user").InnerJoin("user_detail", "user_detail.uid=user.uid")
+// Table("user", "u").InnerJoin("user_detail", "ud", "ud.uid=u.uid")
+func (d *PlatMeetingDao) InnerJoin(table ...string) *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.InnerJoin(table...)}
+}
+
+// Fields sets the operation fields of the model, multiple fields joined using char ','.
+// The parameter <fieldNamesOrMapStruct> can be type of string/map/*map/struct/*struct.
+func (d *PlatMeetingDao) Fields(fieldNamesOrMapStruct ...interface{}) *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.Fields(fieldNamesOrMapStruct...)}
+}
+
+// FieldsEx sets the excluded operation fields of the model, multiple fields joined using char ','.
+// The parameter <fieldNamesOrMapStruct> can be type of string/map/*map/struct/*struct.
+func (d *PlatMeetingDao) FieldsEx(fieldNamesOrMapStruct ...interface{}) *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.FieldsEx(fieldNamesOrMapStruct...)}
+}
+
+// Option sets the extra operation option for the model.
+func (d *PlatMeetingDao) Option(option int) *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.Option(option)}
+}
+
+// OmitEmpty sets OPTION_OMITEMPTY option for the model, which automatically filers
+// the data and where attributes for empty values.
+func (d *PlatMeetingDao) OmitEmpty() *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.OmitEmpty()}
+}
+
+// Filter marks filtering the fields which does not exist in the fields of the operated table.
+func (d *PlatMeetingDao) Filter() *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.Filter()}
+}
+
+// Where sets the condition statement for the model. The parameter <where> can be type of
+// string/map/gmap/slice/struct/*struct, etc. Note that, if it's called more than one times,
+// multiple conditions will be joined into where statement using "AND".
+// Eg:
+// Where("uid=10000")
+// Where("uid", 10000)
+// Where("money>? AND name like ?", 99999, "vip_%")
+// Where("uid", 1).Where("name", "john")
+// Where("status IN (?)", g.Slice{1,2,3})
+// Where("age IN(?,?)", 18, 50)
+// Where(User{ Id : 1, UserName : "john"})
+func (d *PlatMeetingDao) Where(where interface{}, args ...interface{}) *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.Where(where, args...)}
+}
+
+// WherePri does the same logic as M.Where except that if the parameter <where>
+// is a single condition like int/string/float/slice, it treats the condition as the primary
+// key value. That is, if primary key is "id" and given <where> parameter as "123", the
+// WherePri function treats the condition as "id=123", but M.Where treats the condition
+// as string "123".
+func (d *PlatMeetingDao) WherePri(where interface{}, args ...interface{}) *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.WherePri(where, args...)}
+}
+
+// And adds "AND" condition to the where statement.
+func (d *PlatMeetingDao) And(where interface{}, args ...interface{}) *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.And(where, args...)}
+}
+
+// Or adds "OR" condition to the where statement.
+func (d *PlatMeetingDao) Or(where interface{}, args ...interface{}) *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.Or(where, args...)}
+}
+
+// Group sets the "GROUP BY" statement for the model.
+func (d *PlatMeetingDao) Group(groupBy string) *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.Group(groupBy)}
+}
+
+// Order sets the "ORDER BY" statement for the model.
+func (d *PlatMeetingDao) Order(orderBy ...string) *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.Order(orderBy...)}
+}
+
+// Limit sets the "LIMIT" statement for the model.
+// The parameter <limit> can be either one or two number, if passed two number is passed,
+// it then sets "LIMIT limit[0],limit[1]" statement for the model, or else it sets "LIMIT limit[0]"
+// statement.
+func (d *PlatMeetingDao) Limit(limit ...int) *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.Limit(limit...)}
+}
+
+// Offset sets the "OFFSET" statement for the model.
+// It only makes sense for some databases like SQLServer, PostgreSQL, etc.
+func (d *PlatMeetingDao) Offset(offset int) *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.Offset(offset)}
+}
+
+// Page sets the paging number for the model.
+// The parameter <page> is started from 1 for paging.
+// Note that, it differs that the Limit function start from 0 for "LIMIT" statement.
+func (d *PlatMeetingDao) Page(page, limit int) *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.Page(page, limit)}
+}
+
+// Batch sets the batch operation number for the model.
+func (d *PlatMeetingDao) Batch(batch int) *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.Batch(batch)}
+}
+
+// Cache sets the cache feature for the model. It caches the result of the sql, which means
+// if there's another same sql request, it just reads and returns the result from cache, it
+// but not committed and executed into the database.
+//
+// If the parameter <duration> < 0, which means it clear the cache with given <name>.
+// If the parameter <duration> = 0, which means it never expires.
+// If the parameter <duration> > 0, which means it expires after <duration>.
+//
+// The optional parameter <name> is used to bind a name to the cache, which means you can later
+// control the cache like changing the <duration> or clearing the cache with specified <name>.
+//
+// Note that, the cache feature is disabled if the model is operating on a transaction.
+func (d *PlatMeetingDao) Cache(duration time.Duration, name ...string) *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.Cache(duration, name...)}
+}
+
+// Data sets the operation data for the model.
+// The parameter <data> can be type of string/map/gmap/slice/struct/*struct, etc.
+// Eg:
+// Data("uid=10000")
+// Data("uid", 10000)
+// Data(g.Map{"uid": 10000, "name":"john"})
+// Data(g.Slice{g.Map{"uid": 10000, "name":"john"}, g.Map{"uid": 20000, "name":"smith"})
+func (d *PlatMeetingDao) Data(data ...interface{}) *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.Data(data...)}
+}
+
+// All does "SELECT FROM ..." statement for the model.
+// It retrieves the records from table and returns the result as []*model.PlatMeeting.
+// It returns nil if there's no record retrieved with the given conditions from table.
+//
+// The optional parameter <where> is the same as the parameter of M.Where function,
+// see M.Where.
+func (d *PlatMeetingDao) All(where ...interface{}) ([]*opsdev.PlatMeeting, error) {
+	all, err := d.M.All(where...)
+	if err != nil {
+		return nil, err
+	}
+	var entities []*opsdev.PlatMeeting
+	if err = all.Structs(&entities); err != nil && err != sql.ErrNoRows {
+		return nil, err
+	}
+	return entities, nil
+}
+
+// One retrieves one record from table and returns the result as *model.PlatMeeting.
+// It returns nil if there's no record retrieved with the given conditions from table.
+//
+// The optional parameter <where> is the same as the parameter of M.Where function,
+// see M.Where.
+func (d *PlatMeetingDao) One(where ...interface{}) (*opsdev.PlatMeeting, error) {
+	one, err := d.M.One(where...)
+	if err != nil {
+		return nil, err
+	}
+	var entity *opsdev.PlatMeeting
+	if err = one.Struct(&entity); err != nil && err != sql.ErrNoRows {
+		return nil, err
+	}
+	return entity, nil
+}
+
+// FindOne retrieves and returns a single Record by M.WherePri and M.One.
+// Also see M.WherePri and M.One.
+func (d *PlatMeetingDao) FindOne(where ...interface{}) (*opsdev.PlatMeeting, error) {
+	one, err := d.M.FindOne(where...)
+	if err != nil {
+		return nil, err
+	}
+	var entity *opsdev.PlatMeeting
+	if err = one.Struct(&entity); err != nil && err != sql.ErrNoRows {
+		return nil, err
+	}
+	return entity, nil
+}
+
+// FindAll retrieves and returns Result by by M.WherePri and M.All.
+// Also see M.WherePri and M.All.
+func (d *PlatMeetingDao) FindAll(where ...interface{}) ([]*opsdev.PlatMeeting, error) {
+	all, err := d.M.FindAll(where...)
+	if err != nil {
+		return nil, err
+	}
+	var entities []*opsdev.PlatMeeting
+	if err = all.Structs(&entities); err != nil && err != sql.ErrNoRows {
+		return nil, err
+	}
+	return entities, nil
+}
+
+// Struct retrieves one record from table and converts it into given struct.
+// The parameter <pointer> should be type of *struct/**struct. If type **struct is given,
+// it can create the struct internally during converting.
+//
+// The optional parameter <where> is the same as the parameter of Model.Where function,
+// see Model.Where.
+//
+// Note that it returns sql.ErrNoRows if there's no record retrieved with the given conditions
+// from table and <pointer> is not nil.
+//
+// Eg:
+// user := new(User)
+// err  := dao.User.Where("id", 1).Struct(user)
+//
+// user := (*User)(nil)
+// err  := dao.User.Where("id", 1).Struct(&user)
+func (d *PlatMeetingDao) Struct(pointer interface{}, where ...interface{}) error {
+	return d.M.Struct(pointer, where...)
+}
+
+// Structs retrieves records from table and converts them into given struct slice.
+// The parameter <pointer> should be type of *[]struct/*[]*struct. It can create and fill the struct
+// slice internally during converting.
+//
+// The optional parameter <where> is the same as the parameter of Model.Where function,
+// see Model.Where.
+//
+// Note that it returns sql.ErrNoRows if there's no record retrieved with the given conditions
+// from table and <pointer> is not empty.
+//
+// Eg:
+// users := ([]User)(nil)
+// err   := dao.User.Structs(&users)
+//
+// users := ([]*User)(nil)
+// err   := dao.User.Structs(&users)
+func (d *PlatMeetingDao) Structs(pointer interface{}, where ...interface{}) error {
+	return d.M.Structs(pointer, where...)
+}
+
+// Scan automatically calls Struct or Structs function according to the type of parameter <pointer>.
+// It calls function Struct if <pointer> is type of *struct/**struct.
+// It calls function Structs if <pointer> is type of *[]struct/*[]*struct.
+//
+// The optional parameter <where> is the same as the parameter of Model.Where function,
+// see Model.Where.
+//
+// Note that it returns sql.ErrNoRows if there's no record retrieved and given pointer is not empty or nil.
+//
+// Eg:
+// user  := new(User)
+// err   := dao.User.Where("id", 1).Scan(user)
+//
+// user  := (*User)(nil)
+// err   := dao.User.Where("id", 1).Scan(&user)
+//
+// users := ([]User)(nil)
+// err   := dao.User.Scan(&users)
+//
+// users := ([]*User)(nil)
+// err   := dao.User.Scan(&users)
+func (d *PlatMeetingDao) Scan(pointer interface{}, where ...interface{}) error {
+	return d.M.Scan(pointer, where...)
+}
+
+// Chunk iterates the table with given size and callback function.
+func (d *PlatMeetingDao) Chunk(limit int, callback func(entities []*opsdev.PlatMeeting, err error) bool) {
+	d.M.Chunk(limit, func(result gdb.Result, err error) bool {
+		var entities []*opsdev.PlatMeeting
+		err = result.Structs(&entities)
+		if err == sql.ErrNoRows {
+			return false
+		}
+		return callback(entities, err)
+	})
+}
+
+// LockUpdate sets the lock for update for current operation.
+func (d *PlatMeetingDao) LockUpdate() *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.LockUpdate()}
+}
+
+// LockShared sets the lock in share mode for current operation.
+func (d *PlatMeetingDao) LockShared() *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.LockShared()}
+}
+
+// Unscoped enables/disables the soft deleting feature.
+func (d *PlatMeetingDao) Unscoped() *PlatMeetingDao {
+	return &PlatMeetingDao{M: d.M.Unscoped()}
+}

+ 429 - 0
opms_parent/app/dao/opsdev/internal/plat_meeting_attendee.go

@@ -0,0 +1,429 @@
+// ==========================================================================
+// This is auto-generated by gf cli tool. DO NOT EDIT THIS FILE MANUALLY.
+// ==========================================================================
+
+package internal
+
+import (
+	"context"
+	"database/sql"
+	"time"
+
+	"dashoo.cn/opms_parent/app/model/opsdev"
+	"github.com/gogf/gf/database/gdb"
+	"github.com/gogf/gf/frame/g"
+	"github.com/gogf/gf/frame/gmvc"
+)
+
+// PlatMeetingAttendeeDao is the manager for logic model data accessing
+// and custom defined data operations functions management.
+type PlatMeetingAttendeeDao struct {
+	gmvc.M
+	DB      gdb.DB
+	Table   string
+	Columns platMeetingAttendeeColumns
+}
+
+// PlatMeetingAttendeeColumns defines and stores column names for table plat_meeting_attendee.
+type platMeetingAttendeeColumns struct {
+	Id                string // 主键
+	MeetingId         string // 会议ID
+	UserId            string // 参会人员ID
+	UserName          string // 参会人员姓名
+	WorkHourGenerated string // 是否已生成工时: 0=否, 1=是
+	CreatedBy         string // 创建者
+	CreatedName       string // 创建人
+	CreatedTime       string // 创建时间
+	UpdatedBy         string // 更新者
+	UpdatedName       string // 更新人
+	UpdatedTime       string // 更新时间
+	DeletedTime       string // 删除时间
+}
+
+var (
+	// PlatMeetingAttendee is globally public accessible object for table plat_meeting_attendee operations.
+	PlatMeetingAttendee = PlatMeetingAttendeeDao{
+		M:     g.DB("default").Model("plat_meeting_attendee").Safe(),
+		DB:    g.DB("default"),
+		Table: "plat_meeting_attendee",
+		Columns: platMeetingAttendeeColumns{
+			Id:                "id",
+			MeetingId:         "meeting_id",
+			UserId:            "user_id",
+			UserName:          "user_name",
+			WorkHourGenerated: "work_hour_generated",
+			CreatedBy:         "created_by",
+			CreatedName:       "created_name",
+			CreatedTime:       "created_time",
+			UpdatedBy:         "updated_by",
+			UpdatedName:       "updated_name",
+			UpdatedTime:       "updated_time",
+			DeletedTime:       "deleted_time",
+		},
+	}
+)
+
+func NewPlatMeetingAttendeeDao(tenant string) PlatMeetingAttendeeDao {
+	var dao PlatMeetingAttendeeDao
+	dao = PlatMeetingAttendeeDao{
+		M:     g.DB(tenant).Model("plat_meeting_attendee").Safe(),
+		DB:    g.DB(tenant),
+		Table: "plat_meeting_attendee",
+		Columns: platMeetingAttendeeColumns{
+			Id:                "id",
+			MeetingId:         "meeting_id",
+			UserId:            "user_id",
+			UserName:          "user_name",
+			WorkHourGenerated: "work_hour_generated",
+			CreatedBy:         "created_by",
+			CreatedName:       "created_name",
+			CreatedTime:       "created_time",
+			UpdatedBy:         "updated_by",
+			UpdatedName:       "updated_name",
+			UpdatedTime:       "updated_time",
+			DeletedTime:       "deleted_time",
+		},
+	}
+	return dao
+}
+
+// Ctx is a chaining function, which creates and returns a new DB that is a shallow copy
+// of current DB object and with given context in it.
+// Note that this returned DB object can be used only once, so do not assign it to
+// a global or package variable for long using.
+func (d *PlatMeetingAttendeeDao) Ctx(ctx context.Context) *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.Ctx(ctx)}
+}
+
+// As sets an alias name for current table.
+func (d *PlatMeetingAttendeeDao) As(as string) *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.As(as)}
+}
+
+// TX sets the transaction for current operation.
+func (d *PlatMeetingAttendeeDao) TX(tx *gdb.TX) *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.TX(tx)}
+}
+
+// Master marks the following operation on master node.
+func (d *PlatMeetingAttendeeDao) Master() *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.Master()}
+}
+
+// Slave marks the following operation on slave node.
+// Note that it makes sense only if there's any slave node configured.
+func (d *PlatMeetingAttendeeDao) Slave() *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.Slave()}
+}
+
+// Args sets custom arguments for model operation.
+func (d *PlatMeetingAttendeeDao) Args(args ...interface{}) *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.Args(args...)}
+}
+
+// LeftJoin does "LEFT JOIN ... ON ..." statement on the model.
+// The parameter <table> can be joined table and its joined condition,
+// and also with its alias name, like:
+// Table("user").LeftJoin("user_detail", "user_detail.uid=user.uid")
+// Table("user", "u").LeftJoin("user_detail", "ud", "ud.uid=u.uid")
+func (d *PlatMeetingAttendeeDao) LeftJoin(table ...string) *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.LeftJoin(table...)}
+}
+
+// RightJoin does "RIGHT JOIN ... ON ..." statement on the model.
+// The parameter <table> can be joined table and its joined condition,
+// and also with its alias name, like:
+// Table("user").RightJoin("user_detail", "user_detail.uid=user.uid")
+// Table("user", "u").RightJoin("user_detail", "ud", "ud.uid=u.uid")
+func (d *PlatMeetingAttendeeDao) RightJoin(table ...string) *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.RightJoin(table...)}
+}
+
+// InnerJoin does "INNER JOIN ... ON ..." statement on the model.
+// The parameter <table> can be joined table and its joined condition,
+// and also with its alias name, like:
+// Table("user").InnerJoin("user_detail", "user_detail.uid=user.uid")
+// Table("user", "u").InnerJoin("user_detail", "ud", "ud.uid=u.uid")
+func (d *PlatMeetingAttendeeDao) InnerJoin(table ...string) *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.InnerJoin(table...)}
+}
+
+// Fields sets the operation fields of the model, multiple fields joined using char ','.
+// The parameter <fieldNamesOrMapStruct> can be type of string/map/*map/struct/*struct.
+func (d *PlatMeetingAttendeeDao) Fields(fieldNamesOrMapStruct ...interface{}) *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.Fields(fieldNamesOrMapStruct...)}
+}
+
+// FieldsEx sets the excluded operation fields of the model, multiple fields joined using char ','.
+// The parameter <fieldNamesOrMapStruct> can be type of string/map/*map/struct/*struct.
+func (d *PlatMeetingAttendeeDao) FieldsEx(fieldNamesOrMapStruct ...interface{}) *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.FieldsEx(fieldNamesOrMapStruct...)}
+}
+
+// Option sets the extra operation option for the model.
+func (d *PlatMeetingAttendeeDao) Option(option int) *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.Option(option)}
+}
+
+// OmitEmpty sets OPTION_OMITEMPTY option for the model, which automatically filers
+// the data and where attributes for empty values.
+func (d *PlatMeetingAttendeeDao) OmitEmpty() *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.OmitEmpty()}
+}
+
+// Filter marks filtering the fields which does not exist in the fields of the operated table.
+func (d *PlatMeetingAttendeeDao) Filter() *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.Filter()}
+}
+
+// Where sets the condition statement for the model. The parameter <where> can be type of
+// string/map/gmap/slice/struct/*struct, etc. Note that, if it's called more than one times,
+// multiple conditions will be joined into where statement using "AND".
+// Eg:
+// Where("uid=10000")
+// Where("uid", 10000)
+// Where("money>? AND name like ?", 99999, "vip_%")
+// Where("uid", 1).Where("name", "john")
+// Where("status IN (?)", g.Slice{1,2,3})
+// Where("age IN(?,?)", 18, 50)
+// Where(User{ Id : 1, UserName : "john"})
+func (d *PlatMeetingAttendeeDao) Where(where interface{}, args ...interface{}) *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.Where(where, args...)}
+}
+
+// WherePri does the same logic as M.Where except that if the parameter <where>
+// is a single condition like int/string/float/slice, it treats the condition as the primary
+// key value. That is, if primary key is "id" and given <where> parameter as "123", the
+// WherePri function treats the condition as "id=123", but M.Where treats the condition
+// as string "123".
+func (d *PlatMeetingAttendeeDao) WherePri(where interface{}, args ...interface{}) *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.WherePri(where, args...)}
+}
+
+// And adds "AND" condition to the where statement.
+func (d *PlatMeetingAttendeeDao) And(where interface{}, args ...interface{}) *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.And(where, args...)}
+}
+
+// Or adds "OR" condition to the where statement.
+func (d *PlatMeetingAttendeeDao) Or(where interface{}, args ...interface{}) *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.Or(where, args...)}
+}
+
+// Group sets the "GROUP BY" statement for the model.
+func (d *PlatMeetingAttendeeDao) Group(groupBy string) *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.Group(groupBy)}
+}
+
+// Order sets the "ORDER BY" statement for the model.
+func (d *PlatMeetingAttendeeDao) Order(orderBy ...string) *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.Order(orderBy...)}
+}
+
+// Limit sets the "LIMIT" statement for the model.
+// The parameter <limit> can be either one or two number, if passed two number is passed,
+// it then sets "LIMIT limit[0],limit[1]" statement for the model, or else it sets "LIMIT limit[0]"
+// statement.
+func (d *PlatMeetingAttendeeDao) Limit(limit ...int) *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.Limit(limit...)}
+}
+
+// Offset sets the "OFFSET" statement for the model.
+// It only makes sense for some databases like SQLServer, PostgreSQL, etc.
+func (d *PlatMeetingAttendeeDao) Offset(offset int) *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.Offset(offset)}
+}
+
+// Page sets the paging number for the model.
+// The parameter <page> is started from 1 for paging.
+// Note that, it differs that the Limit function start from 0 for "LIMIT" statement.
+func (d *PlatMeetingAttendeeDao) Page(page, limit int) *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.Page(page, limit)}
+}
+
+// Batch sets the batch operation number for the model.
+func (d *PlatMeetingAttendeeDao) Batch(batch int) *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.Batch(batch)}
+}
+
+// Cache sets the cache feature for the model. It caches the result of the sql, which means
+// if there's another same sql request, it just reads and returns the result from cache, it
+// but not committed and executed into the database.
+//
+// If the parameter <duration> < 0, which means it clear the cache with given <name>.
+// If the parameter <duration> = 0, which means it never expires.
+// If the parameter <duration> > 0, which means it expires after <duration>.
+//
+// The optional parameter <name> is used to bind a name to the cache, which means you can later
+// control the cache like changing the <duration> or clearing the cache with specified <name>.
+//
+// Note that, the cache feature is disabled if the model is operating on a transaction.
+func (d *PlatMeetingAttendeeDao) Cache(duration time.Duration, name ...string) *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.Cache(duration, name...)}
+}
+
+// Data sets the operation data for the model.
+// The parameter <data> can be type of string/map/gmap/slice/struct/*struct, etc.
+// Eg:
+// Data("uid=10000")
+// Data("uid", 10000)
+// Data(g.Map{"uid": 10000, "name":"john"})
+// Data(g.Slice{g.Map{"uid": 10000, "name":"john"}, g.Map{"uid": 20000, "name":"smith"})
+func (d *PlatMeetingAttendeeDao) Data(data ...interface{}) *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.Data(data...)}
+}
+
+// All does "SELECT FROM ..." statement for the model.
+// It retrieves the records from table and returns the result as []*model.PlatMeetingAttendee.
+// It returns nil if there's no record retrieved with the given conditions from table.
+//
+// The optional parameter <where> is the same as the parameter of M.Where function,
+// see M.Where.
+func (d *PlatMeetingAttendeeDao) All(where ...interface{}) ([]*opsdev.PlatMeetingAttendee, error) {
+	all, err := d.M.All(where...)
+	if err != nil {
+		return nil, err
+	}
+	var entities []*opsdev.PlatMeetingAttendee
+	if err = all.Structs(&entities); err != nil && err != sql.ErrNoRows {
+		return nil, err
+	}
+	return entities, nil
+}
+
+// One retrieves one record from table and returns the result as *model.PlatMeetingAttendee.
+// It returns nil if there's no record retrieved with the given conditions from table.
+//
+// The optional parameter <where> is the same as the parameter of M.Where function,
+// see M.Where.
+func (d *PlatMeetingAttendeeDao) One(where ...interface{}) (*opsdev.PlatMeetingAttendee, error) {
+	one, err := d.M.One(where...)
+	if err != nil {
+		return nil, err
+	}
+	var entity *opsdev.PlatMeetingAttendee
+	if err = one.Struct(&entity); err != nil && err != sql.ErrNoRows {
+		return nil, err
+	}
+	return entity, nil
+}
+
+// FindOne retrieves and returns a single Record by M.WherePri and M.One.
+// Also see M.WherePri and M.One.
+func (d *PlatMeetingAttendeeDao) FindOne(where ...interface{}) (*opsdev.PlatMeetingAttendee, error) {
+	one, err := d.M.FindOne(where...)
+	if err != nil {
+		return nil, err
+	}
+	var entity *opsdev.PlatMeetingAttendee
+	if err = one.Struct(&entity); err != nil && err != sql.ErrNoRows {
+		return nil, err
+	}
+	return entity, nil
+}
+
+// FindAll retrieves and returns Result by by M.WherePri and M.All.
+// Also see M.WherePri and M.All.
+func (d *PlatMeetingAttendeeDao) FindAll(where ...interface{}) ([]*opsdev.PlatMeetingAttendee, error) {
+	all, err := d.M.FindAll(where...)
+	if err != nil {
+		return nil, err
+	}
+	var entities []*opsdev.PlatMeetingAttendee
+	if err = all.Structs(&entities); err != nil && err != sql.ErrNoRows {
+		return nil, err
+	}
+	return entities, nil
+}
+
+// Struct retrieves one record from table and converts it into given struct.
+// The parameter <pointer> should be type of *struct/**struct. If type **struct is given,
+// it can create the struct internally during converting.
+//
+// The optional parameter <where> is the same as the parameter of Model.Where function,
+// see Model.Where.
+//
+// Note that it returns sql.ErrNoRows if there's no record retrieved with the given conditions
+// from table and <pointer> is not nil.
+//
+// Eg:
+// user := new(User)
+// err  := dao.User.Where("id", 1).Struct(user)
+//
+// user := (*User)(nil)
+// err  := dao.User.Where("id", 1).Struct(&user)
+func (d *PlatMeetingAttendeeDao) Struct(pointer interface{}, where ...interface{}) error {
+	return d.M.Struct(pointer, where...)
+}
+
+// Structs retrieves records from table and converts them into given struct slice.
+// The parameter <pointer> should be type of *[]struct/*[]*struct. It can create and fill the struct
+// slice internally during converting.
+//
+// The optional parameter <where> is the same as the parameter of Model.Where function,
+// see Model.Where.
+//
+// Note that it returns sql.ErrNoRows if there's no record retrieved with the given conditions
+// from table and <pointer> is not empty.
+//
+// Eg:
+// users := ([]User)(nil)
+// err   := dao.User.Structs(&users)
+//
+// users := ([]*User)(nil)
+// err   := dao.User.Structs(&users)
+func (d *PlatMeetingAttendeeDao) Structs(pointer interface{}, where ...interface{}) error {
+	return d.M.Structs(pointer, where...)
+}
+
+// Scan automatically calls Struct or Structs function according to the type of parameter <pointer>.
+// It calls function Struct if <pointer> is type of *struct/**struct.
+// It calls function Structs if <pointer> is type of *[]struct/*[]*struct.
+//
+// The optional parameter <where> is the same as the parameter of Model.Where function,
+// see Model.Where.
+//
+// Note that it returns sql.ErrNoRows if there's no record retrieved and given pointer is not empty or nil.
+//
+// Eg:
+// user  := new(User)
+// err   := dao.User.Where("id", 1).Scan(user)
+//
+// user  := (*User)(nil)
+// err   := dao.User.Where("id", 1).Scan(&user)
+//
+// users := ([]User)(nil)
+// err   := dao.User.Scan(&users)
+//
+// users := ([]*User)(nil)
+// err   := dao.User.Scan(&users)
+func (d *PlatMeetingAttendeeDao) Scan(pointer interface{}, where ...interface{}) error {
+	return d.M.Scan(pointer, where...)
+}
+
+// Chunk iterates the table with given size and callback function.
+func (d *PlatMeetingAttendeeDao) Chunk(limit int, callback func(entities []*opsdev.PlatMeetingAttendee, err error) bool) {
+	d.M.Chunk(limit, func(result gdb.Result, err error) bool {
+		var entities []*opsdev.PlatMeetingAttendee
+		err = result.Structs(&entities)
+		if err == sql.ErrNoRows {
+			return false
+		}
+		return callback(entities, err)
+	})
+}
+
+// LockUpdate sets the lock for update for current operation.
+func (d *PlatMeetingAttendeeDao) LockUpdate() *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.LockUpdate()}
+}
+
+// LockShared sets the lock in share mode for current operation.
+func (d *PlatMeetingAttendeeDao) LockShared() *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.LockShared()}
+}
+
+// Unscoped enables/disables the soft deleting feature.
+func (d *PlatMeetingAttendeeDao) Unscoped() *PlatMeetingAttendeeDao {
+	return &PlatMeetingAttendeeDao{M: d.M.Unscoped()}
+}

+ 438 - 0
opms_parent/app/dao/opsdev/internal/plat_meeting_work_hour.go

@@ -0,0 +1,438 @@
+// ==========================================================================
+// This is auto-generated by gf cli tool. DO NOT EDIT THIS FILE MANUALLY.
+// ==========================================================================
+
+package internal
+
+import (
+	"context"
+	"database/sql"
+	"time"
+
+	"dashoo.cn/opms_parent/app/model/opsdev"
+	"github.com/gogf/gf/database/gdb"
+	"github.com/gogf/gf/frame/g"
+	"github.com/gogf/gf/frame/gmvc"
+)
+
+// PlatMeetingWorkHourDao is the manager for logic model data accessing
+// and custom defined data operations functions management.
+type PlatMeetingWorkHourDao struct {
+	gmvc.M
+	DB      gdb.DB
+	Table   string
+	Columns platMeetingWorkHourColumns
+}
+
+// PlatMeetingWorkHourColumns defines and stores column names for table plat_meeting_work_hour.
+type platMeetingWorkHourColumns struct {
+	Id          string // 主键
+	MeetingId   string // 关联会议ID
+	AttendeeId  string // 参会记录ID(plat_meeting_attendee.id)
+	UserId      string // 人员ID
+	UserName    string // 人员姓名
+	WorkDate    string // 工时日期
+	WorkHour    string // 工时(小时)
+	Remark      string // 工作内容备注
+	CreatedBy   string // 创建者
+	CreatedName string // 创建人
+	CreatedTime string // 创建时间
+	UpdatedBy   string // 更新者
+	UpdatedName string // 更新人
+	UpdatedTime string // 更新时间
+	DeletedTime string // 删除时间
+}
+
+var (
+	// PlatMeetingWorkHour is globally public accessible object for table plat_meeting_work_hour operations.
+	PlatMeetingWorkHour = PlatMeetingWorkHourDao{
+		M:     g.DB("default").Model("plat_meeting_work_hour").Safe(),
+		DB:    g.DB("default"),
+		Table: "plat_meeting_work_hour",
+		Columns: platMeetingWorkHourColumns{
+			Id:          "id",
+			MeetingId:   "meeting_id",
+			AttendeeId:  "attendee_id",
+			UserId:      "user_id",
+			UserName:    "user_name",
+			WorkDate:    "work_date",
+			WorkHour:    "work_hour",
+			Remark:      "remark",
+			CreatedBy:   "created_by",
+			CreatedName: "created_name",
+			CreatedTime: "created_time",
+			UpdatedBy:   "updated_by",
+			UpdatedName: "updated_name",
+			UpdatedTime: "updated_time",
+			DeletedTime: "deleted_time",
+		},
+	}
+)
+
+func NewPlatMeetingWorkHourDao(tenant string) PlatMeetingWorkHourDao {
+	var dao PlatMeetingWorkHourDao
+	dao = PlatMeetingWorkHourDao{
+		M:     g.DB(tenant).Model("plat_meeting_work_hour").Safe(),
+		DB:    g.DB(tenant),
+		Table: "plat_meeting_work_hour",
+		Columns: platMeetingWorkHourColumns{
+			Id:          "id",
+			MeetingId:   "meeting_id",
+			AttendeeId:  "attendee_id",
+			UserId:      "user_id",
+			UserName:    "user_name",
+			WorkDate:    "work_date",
+			WorkHour:    "work_hour",
+			Remark:      "remark",
+			CreatedBy:   "created_by",
+			CreatedName: "created_name",
+			CreatedTime: "created_time",
+			UpdatedBy:   "updated_by",
+			UpdatedName: "updated_name",
+			UpdatedTime: "updated_time",
+			DeletedTime: "deleted_time",
+		},
+	}
+	return dao
+}
+
+// Ctx is a chaining function, which creates and returns a new DB that is a shallow copy
+// of current DB object and with given context in it.
+// Note that this returned DB object can be used only once, so do not assign it to
+// a global or package variable for long using.
+func (d *PlatMeetingWorkHourDao) Ctx(ctx context.Context) *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.Ctx(ctx)}
+}
+
+// As sets an alias name for current table.
+func (d *PlatMeetingWorkHourDao) As(as string) *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.As(as)}
+}
+
+// TX sets the transaction for current operation.
+func (d *PlatMeetingWorkHourDao) TX(tx *gdb.TX) *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.TX(tx)}
+}
+
+// Master marks the following operation on master node.
+func (d *PlatMeetingWorkHourDao) Master() *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.Master()}
+}
+
+// Slave marks the following operation on slave node.
+// Note that it makes sense only if there's any slave node configured.
+func (d *PlatMeetingWorkHourDao) Slave() *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.Slave()}
+}
+
+// Args sets custom arguments for model operation.
+func (d *PlatMeetingWorkHourDao) Args(args ...interface{}) *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.Args(args...)}
+}
+
+// LeftJoin does "LEFT JOIN ... ON ..." statement on the model.
+// The parameter <table> can be joined table and its joined condition,
+// and also with its alias name, like:
+// Table("user").LeftJoin("user_detail", "user_detail.uid=user.uid")
+// Table("user", "u").LeftJoin("user_detail", "ud", "ud.uid=u.uid")
+func (d *PlatMeetingWorkHourDao) LeftJoin(table ...string) *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.LeftJoin(table...)}
+}
+
+// RightJoin does "RIGHT JOIN ... ON ..." statement on the model.
+// The parameter <table> can be joined table and its joined condition,
+// and also with its alias name, like:
+// Table("user").RightJoin("user_detail", "user_detail.uid=user.uid")
+// Table("user", "u").RightJoin("user_detail", "ud", "ud.uid=u.uid")
+func (d *PlatMeetingWorkHourDao) RightJoin(table ...string) *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.RightJoin(table...)}
+}
+
+// InnerJoin does "INNER JOIN ... ON ..." statement on the model.
+// The parameter <table> can be joined table and its joined condition,
+// and also with its alias name, like:
+// Table("user").InnerJoin("user_detail", "user_detail.uid=user.uid")
+// Table("user", "u").InnerJoin("user_detail", "ud", "ud.uid=u.uid")
+func (d *PlatMeetingWorkHourDao) InnerJoin(table ...string) *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.InnerJoin(table...)}
+}
+
+// Fields sets the operation fields of the model, multiple fields joined using char ','.
+// The parameter <fieldNamesOrMapStruct> can be type of string/map/*map/struct/*struct.
+func (d *PlatMeetingWorkHourDao) Fields(fieldNamesOrMapStruct ...interface{}) *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.Fields(fieldNamesOrMapStruct...)}
+}
+
+// FieldsEx sets the excluded operation fields of the model, multiple fields joined using char ','.
+// The parameter <fieldNamesOrMapStruct> can be type of string/map/*map/struct/*struct.
+func (d *PlatMeetingWorkHourDao) FieldsEx(fieldNamesOrMapStruct ...interface{}) *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.FieldsEx(fieldNamesOrMapStruct...)}
+}
+
+// Option sets the extra operation option for the model.
+func (d *PlatMeetingWorkHourDao) Option(option int) *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.Option(option)}
+}
+
+// OmitEmpty sets OPTION_OMITEMPTY option for the model, which automatically filers
+// the data and where attributes for empty values.
+func (d *PlatMeetingWorkHourDao) OmitEmpty() *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.OmitEmpty()}
+}
+
+// Filter marks filtering the fields which does not exist in the fields of the operated table.
+func (d *PlatMeetingWorkHourDao) Filter() *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.Filter()}
+}
+
+// Where sets the condition statement for the model. The parameter <where> can be type of
+// string/map/gmap/slice/struct/*struct, etc. Note that, if it's called more than one times,
+// multiple conditions will be joined into where statement using "AND".
+// Eg:
+// Where("uid=10000")
+// Where("uid", 10000)
+// Where("money>? AND name like ?", 99999, "vip_%")
+// Where("uid", 1).Where("name", "john")
+// Where("status IN (?)", g.Slice{1,2,3})
+// Where("age IN(?,?)", 18, 50)
+// Where(User{ Id : 1, UserName : "john"})
+func (d *PlatMeetingWorkHourDao) Where(where interface{}, args ...interface{}) *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.Where(where, args...)}
+}
+
+// WherePri does the same logic as M.Where except that if the parameter <where>
+// is a single condition like int/string/float/slice, it treats the condition as the primary
+// key value. That is, if primary key is "id" and given <where> parameter as "123", the
+// WherePri function treats the condition as "id=123", but M.Where treats the condition
+// as string "123".
+func (d *PlatMeetingWorkHourDao) WherePri(where interface{}, args ...interface{}) *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.WherePri(where, args...)}
+}
+
+// And adds "AND" condition to the where statement.
+func (d *PlatMeetingWorkHourDao) And(where interface{}, args ...interface{}) *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.And(where, args...)}
+}
+
+// Or adds "OR" condition to the where statement.
+func (d *PlatMeetingWorkHourDao) Or(where interface{}, args ...interface{}) *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.Or(where, args...)}
+}
+
+// Group sets the "GROUP BY" statement for the model.
+func (d *PlatMeetingWorkHourDao) Group(groupBy string) *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.Group(groupBy)}
+}
+
+// Order sets the "ORDER BY" statement for the model.
+func (d *PlatMeetingWorkHourDao) Order(orderBy ...string) *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.Order(orderBy...)}
+}
+
+// Limit sets the "LIMIT" statement for the model.
+// The parameter <limit> can be either one or two number, if passed two number is passed,
+// it then sets "LIMIT limit[0],limit[1]" statement for the model, or else it sets "LIMIT limit[0]"
+// statement.
+func (d *PlatMeetingWorkHourDao) Limit(limit ...int) *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.Limit(limit...)}
+}
+
+// Offset sets the "OFFSET" statement for the model.
+// It only makes sense for some databases like SQLServer, PostgreSQL, etc.
+func (d *PlatMeetingWorkHourDao) Offset(offset int) *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.Offset(offset)}
+}
+
+// Page sets the paging number for the model.
+// The parameter <page> is started from 1 for paging.
+// Note that, it differs that the Limit function start from 0 for "LIMIT" statement.
+func (d *PlatMeetingWorkHourDao) Page(page, limit int) *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.Page(page, limit)}
+}
+
+// Batch sets the batch operation number for the model.
+func (d *PlatMeetingWorkHourDao) Batch(batch int) *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.Batch(batch)}
+}
+
+// Cache sets the cache feature for the model. It caches the result of the sql, which means
+// if there's another same sql request, it just reads and returns the result from cache, it
+// but not committed and executed into the database.
+//
+// If the parameter <duration> < 0, which means it clear the cache with given <name>.
+// If the parameter <duration> = 0, which means it never expires.
+// If the parameter <duration> > 0, which means it expires after <duration>.
+//
+// The optional parameter <name> is used to bind a name to the cache, which means you can later
+// control the cache like changing the <duration> or clearing the cache with specified <name>.
+//
+// Note that, the cache feature is disabled if the model is operating on a transaction.
+func (d *PlatMeetingWorkHourDao) Cache(duration time.Duration, name ...string) *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.Cache(duration, name...)}
+}
+
+// Data sets the operation data for the model.
+// The parameter <data> can be type of string/map/gmap/slice/struct/*struct, etc.
+// Eg:
+// Data("uid=10000")
+// Data("uid", 10000)
+// Data(g.Map{"uid": 10000, "name":"john"})
+// Data(g.Slice{g.Map{"uid": 10000, "name":"john"}, g.Map{"uid": 20000, "name":"smith"})
+func (d *PlatMeetingWorkHourDao) Data(data ...interface{}) *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.Data(data...)}
+}
+
+// All does "SELECT FROM ..." statement for the model.
+// It retrieves the records from table and returns the result as []*model.PlatMeetingWorkHour.
+// It returns nil if there's no record retrieved with the given conditions from table.
+//
+// The optional parameter <where> is the same as the parameter of M.Where function,
+// see M.Where.
+func (d *PlatMeetingWorkHourDao) All(where ...interface{}) ([]*opsdev.PlatMeetingWorkHour, error) {
+	all, err := d.M.All(where...)
+	if err != nil {
+		return nil, err
+	}
+	var entities []*opsdev.PlatMeetingWorkHour
+	if err = all.Structs(&entities); err != nil && err != sql.ErrNoRows {
+		return nil, err
+	}
+	return entities, nil
+}
+
+// One retrieves one record from table and returns the result as *model.PlatMeetingWorkHour.
+// It returns nil if there's no record retrieved with the given conditions from table.
+//
+// The optional parameter <where> is the same as the parameter of M.Where function,
+// see M.Where.
+func (d *PlatMeetingWorkHourDao) One(where ...interface{}) (*opsdev.PlatMeetingWorkHour, error) {
+	one, err := d.M.One(where...)
+	if err != nil {
+		return nil, err
+	}
+	var entity *opsdev.PlatMeetingWorkHour
+	if err = one.Struct(&entity); err != nil && err != sql.ErrNoRows {
+		return nil, err
+	}
+	return entity, nil
+}
+
+// FindOne retrieves and returns a single Record by M.WherePri and M.One.
+// Also see M.WherePri and M.One.
+func (d *PlatMeetingWorkHourDao) FindOne(where ...interface{}) (*opsdev.PlatMeetingWorkHour, error) {
+	one, err := d.M.FindOne(where...)
+	if err != nil {
+		return nil, err
+	}
+	var entity *opsdev.PlatMeetingWorkHour
+	if err = one.Struct(&entity); err != nil && err != sql.ErrNoRows {
+		return nil, err
+	}
+	return entity, nil
+}
+
+// FindAll retrieves and returns Result by by M.WherePri and M.All.
+// Also see M.WherePri and M.All.
+func (d *PlatMeetingWorkHourDao) FindAll(where ...interface{}) ([]*opsdev.PlatMeetingWorkHour, error) {
+	all, err := d.M.FindAll(where...)
+	if err != nil {
+		return nil, err
+	}
+	var entities []*opsdev.PlatMeetingWorkHour
+	if err = all.Structs(&entities); err != nil && err != sql.ErrNoRows {
+		return nil, err
+	}
+	return entities, nil
+}
+
+// Struct retrieves one record from table and converts it into given struct.
+// The parameter <pointer> should be type of *struct/**struct. If type **struct is given,
+// it can create the struct internally during converting.
+//
+// The optional parameter <where> is the same as the parameter of Model.Where function,
+// see Model.Where.
+//
+// Note that it returns sql.ErrNoRows if there's no record retrieved with the given conditions
+// from table and <pointer> is not nil.
+//
+// Eg:
+// user := new(User)
+// err  := dao.User.Where("id", 1).Struct(user)
+//
+// user := (*User)(nil)
+// err  := dao.User.Where("id", 1).Struct(&user)
+func (d *PlatMeetingWorkHourDao) Struct(pointer interface{}, where ...interface{}) error {
+	return d.M.Struct(pointer, where...)
+}
+
+// Structs retrieves records from table and converts them into given struct slice.
+// The parameter <pointer> should be type of *[]struct/*[]*struct. It can create and fill the struct
+// slice internally during converting.
+//
+// The optional parameter <where> is the same as the parameter of Model.Where function,
+// see Model.Where.
+//
+// Note that it returns sql.ErrNoRows if there's no record retrieved with the given conditions
+// from table and <pointer> is not empty.
+//
+// Eg:
+// users := ([]User)(nil)
+// err   := dao.User.Structs(&users)
+//
+// users := ([]*User)(nil)
+// err   := dao.User.Structs(&users)
+func (d *PlatMeetingWorkHourDao) Structs(pointer interface{}, where ...interface{}) error {
+	return d.M.Structs(pointer, where...)
+}
+
+// Scan automatically calls Struct or Structs function according to the type of parameter <pointer>.
+// It calls function Struct if <pointer> is type of *struct/**struct.
+// It calls function Structs if <pointer> is type of *[]struct/*[]*struct.
+//
+// The optional parameter <where> is the same as the parameter of Model.Where function,
+// see Model.Where.
+//
+// Note that it returns sql.ErrNoRows if there's no record retrieved and given pointer is not empty or nil.
+//
+// Eg:
+// user  := new(User)
+// err   := dao.User.Where("id", 1).Scan(user)
+//
+// user  := (*User)(nil)
+// err   := dao.User.Where("id", 1).Scan(&user)
+//
+// users := ([]User)(nil)
+// err   := dao.User.Scan(&users)
+//
+// users := ([]*User)(nil)
+// err   := dao.User.Scan(&users)
+func (d *PlatMeetingWorkHourDao) Scan(pointer interface{}, where ...interface{}) error {
+	return d.M.Scan(pointer, where...)
+}
+
+// Chunk iterates the table with given size and callback function.
+func (d *PlatMeetingWorkHourDao) Chunk(limit int, callback func(entities []*opsdev.PlatMeetingWorkHour, err error) bool) {
+	d.M.Chunk(limit, func(result gdb.Result, err error) bool {
+		var entities []*opsdev.PlatMeetingWorkHour
+		err = result.Structs(&entities)
+		if err == sql.ErrNoRows {
+			return false
+		}
+		return callback(entities, err)
+	})
+}
+
+// LockUpdate sets the lock for update for current operation.
+func (d *PlatMeetingWorkHourDao) LockUpdate() *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.LockUpdate()}
+}
+
+// LockShared sets the lock in share mode for current operation.
+func (d *PlatMeetingWorkHourDao) LockShared() *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.LockShared()}
+}
+
+// Unscoped enables/disables the soft deleting feature.
+func (d *PlatMeetingWorkHourDao) Unscoped() *PlatMeetingWorkHourDao {
+	return &PlatMeetingWorkHourDao{M: d.M.Unscoped()}
+}

+ 36 - 0
opms_parent/app/dao/opsdev/plat_meeting.go

@@ -0,0 +1,36 @@
+// ============================================================================
+// This is auto-generated by gf cli tool only once. Fill this file as you wish.
+// ============================================================================
+
+package opsdev
+
+import (
+	"dashoo.cn/opms_parent/app/dao/opsdev/internal"
+)
+
+// platMeetingDao is the manager for logic model data accessing
+// and custom defined data operations functions management. You can define
+// methods on it to extend its functionality as you wish.
+type platMeetingDao struct {
+	internal.PlatMeetingDao
+}
+
+var (
+	// PlatMeeting is globally public accessible object for table plat_meeting operations.
+	PlatMeeting = platMeetingDao{
+		internal.PlatMeeting,
+	}
+)
+
+type PlatMeetingDao struct {
+	internal.PlatMeetingDao
+}
+
+func NewPlatMeetingDao(tenant string) *PlatMeetingDao {
+	dao := internal.NewPlatMeetingDao(tenant)
+	return &PlatMeetingDao{
+		dao,
+	}
+}
+
+// Fill with you ideas below.

+ 36 - 0
opms_parent/app/dao/opsdev/plat_meeting_attendee.go

@@ -0,0 +1,36 @@
+// ============================================================================
+// This is auto-generated by gf cli tool only once. Fill this file as you wish.
+// ============================================================================
+
+package opsdev
+
+import (
+	"dashoo.cn/opms_parent/app/dao/opsdev/internal"
+)
+
+// platMeetingAttendeeDao is the manager for logic model data accessing
+// and custom defined data operations functions management. You can define
+// methods on it to extend its functionality as you wish.
+type platMeetingAttendeeDao struct {
+	internal.PlatMeetingAttendeeDao
+}
+
+var (
+	// PlatMeetingAttendee is globally public accessible object for table plat_meeting_attendee operations.
+	PlatMeetingAttendee = platMeetingAttendeeDao{
+		internal.PlatMeetingAttendee,
+	}
+)
+
+type PlatMeetingAttendeeDao struct {
+	internal.PlatMeetingAttendeeDao
+}
+
+func NewPlatMeetingAttendeeDao(tenant string) *PlatMeetingAttendeeDao {
+	dao := internal.NewPlatMeetingAttendeeDao(tenant)
+	return &PlatMeetingAttendeeDao{
+		dao,
+	}
+}
+
+// Fill with you ideas below.

+ 36 - 0
opms_parent/app/dao/opsdev/plat_meeting_work_hour.go

@@ -0,0 +1,36 @@
+// ============================================================================
+// This is auto-generated by gf cli tool only once. Fill this file as you wish.
+// ============================================================================
+
+package opsdev
+
+import (
+	"dashoo.cn/opms_parent/app/dao/opsdev/internal"
+)
+
+// platMeetingWorkHourDao is the manager for logic model data accessing
+// and custom defined data operations functions management. You can define
+// methods on it to extend its functionality as you wish.
+type platMeetingWorkHourDao struct {
+	internal.PlatMeetingWorkHourDao
+}
+
+var (
+	// PlatMeetingWorkHour is globally public accessible object for table plat_meeting_work_hour operations.
+	PlatMeetingWorkHour = platMeetingWorkHourDao{
+		internal.PlatMeetingWorkHour,
+	}
+)
+
+type PlatMeetingWorkHourDao struct {
+	internal.PlatMeetingWorkHourDao
+}
+
+func NewPlatMeetingWorkHourDao(tenant string) *PlatMeetingWorkHourDao {
+	dao := internal.NewPlatMeetingWorkHourDao(tenant)
+	return &PlatMeetingWorkHourDao{
+		dao,
+	}
+}
+
+// Fill with you ideas below.

+ 38 - 0
opms_parent/app/handler/contract/ctr_contract_application.go

@@ -0,0 +1,38 @@
+package contract
+
+import (
+	"context"
+
+	"dashoo.cn/common_definition/comm_def"
+	"dashoo.cn/opms_libary/myerrors"
+	service "dashoo.cn/opms_parent/app/service/contract"
+	model "dashoo.cn/opms_parent/app/model/contract"
+	"github.com/gogf/gf/frame/g"
+)
+
+type CtrContractApplication struct{}
+
+func (c *CtrContractApplication) CreateFromBusiness(ctx context.Context, req *model.CreateFromBusinessReq, rsp *comm_def.CommonMsg) error {
+	g.Log().Infof("CtrContractApplication.CreateFromBusiness request %#v ", *req)
+
+	if req.NboId <= 0 {
+		return myerrors.ValidError("项目ID不能为空")
+	}
+	if req.NboName == "" {
+		return myerrors.ValidError("项目名称不能为空")
+	}
+
+	s, err := service.NewCtrContractApplicationService(ctx)
+	if err != nil {
+		return err
+	}
+
+	id, err := s.CreateFromBusiness(req)
+	if err != nil {
+		return err
+	}
+
+	rsp.Msg = "合同申请已提交"
+	rsp.Data = g.Map{"id": id}
+	return nil
+}

+ 60 - 0
opms_parent/app/handler/opsdev/delivery_project.go

@@ -42,6 +42,20 @@ func (h *DeliveryProjectHandler) GetListAll(ctx context.Context, req *opsdevmode
 	return nil
 }
 
+// GetDelegatedProjectList 获取委托项目列表(自己负责 + 被授权)
+func (h *DeliveryProjectHandler) GetDelegatedProjectList(ctx context.Context, req *opsdevmodel.OpsDeliveryProjectDelegatedSearchReq, rsp *comm_def.CommonMsg) error {
+	srv, err := opsdevSrv.NewDeliveryProjectService(ctx)
+	if err != nil {
+		return err
+	}
+	total, list, err := srv.GetDelegatedProjectList(req)
+	if err != nil {
+		return err
+	}
+	rsp.Data = g.Map{"list": list, "total": total}
+	return nil
+}
+
 // GetEntityById 根据ID获取详情
 func (h *DeliveryProjectHandler) GetEntityById(ctx context.Context, req *comm_def.IdReq, rsp *comm_def.CommonMsg) error {
 	srv, err := opsdevSrv.NewDeliveryProjectService(ctx)
@@ -123,3 +137,49 @@ func (h *DeliveryProjectHandler) AssignDeliveryUser(ctx context.Context, req *op
 	rsp.Data = g.Map{"message": "操作成功"}
 	return nil
 }
+
+// GetDelegatesByProjectId 获取项目授权人列表
+func (h *DeliveryProjectHandler) GetDelegatesByProjectId(ctx context.Context, req *opsdevmodel.OpsDeliveryProjectDelegateListReq, rsp *comm_def.CommonMsg) error {
+	srv, err := opsdevSrv.NewDeliveryProjectService(ctx)
+	if err != nil {
+		return err
+	}
+	list, err := srv.GetDelegatesByProjectId(req)
+	if err != nil {
+		return err
+	}
+	rsp.Data = list
+	return nil
+}
+
+// AddDelegate 添加授权人
+func (h *DeliveryProjectHandler) AddDelegate(ctx context.Context, req *opsdevmodel.OpsDeliveryProjectDelegateAddReq, rsp *comm_def.CommonMsg) error {
+	if err := gvalid.CheckStruct(ctx, req, nil); err != nil {
+		return myerrors.ValidError(err.Error())
+	}
+	srv, err := opsdevSrv.NewDeliveryProjectService(ctx)
+	if err != nil {
+		return err
+	}
+	if err := srv.AddDelegate(req); err != nil {
+		return err
+	}
+	rsp.Data = g.Map{"message": "添加授权人成功"}
+	return nil
+}
+
+// RemoveDelegate 移除授权人
+func (h *DeliveryProjectHandler) RemoveDelegate(ctx context.Context, req *opsdevmodel.OpsDeliveryProjectDelegateDeleteReq, rsp *comm_def.CommonMsg) error {
+	if err := gvalid.CheckStruct(ctx, req, nil); err != nil {
+		return myerrors.ValidError(err.Error())
+	}
+	srv, err := opsdevSrv.NewDeliveryProjectService(ctx)
+	if err != nil {
+		return err
+	}
+	if err := srv.RemoveDelegate(req); err != nil {
+		return err
+	}
+	rsp.Data = g.Map{"message": "移除授权人成功"}
+	return nil
+}

+ 143 - 0
opms_parent/app/handler/opsdev/plat_meeting.go

@@ -0,0 +1,143 @@
+package opsdev
+
+import (
+	"context"
+
+	"dashoo.cn/common_definition/comm_def"
+	"dashoo.cn/opms_libary/myerrors"
+	opsdevmodel "dashoo.cn/opms_parent/app/model/opsdev"
+	services "dashoo.cn/opms_parent/app/service/opsdev"
+	"github.com/gogf/gf/frame/g"
+	"github.com/gogf/gf/util/gvalid"
+)
+
+// PlatMeetingHandler 会议管理接口处理类
+type PlatMeetingHandler struct{}
+
+// GetList 分页查询会议列表
+func (h *PlatMeetingHandler) GetList(ctx context.Context, req *opsdevmodel.PlatMeetingSearchReq, rsp *comm_def.CommonMsg) error {
+	s, err := services.NewPlatMeetingService(ctx)
+	if err != nil {
+		return err
+	}
+	total, list, err := s.GetList(req)
+	if err != nil {
+		return err
+	}
+	rsp.Data = g.Map{"list": list, "total": total}
+	return nil
+}
+
+// Create 新增会议
+func (h *PlatMeetingHandler) Create(ctx context.Context, req *opsdevmodel.PlatMeetingAddReq, rsp *comm_def.CommonMsg) error {
+	if err := gvalid.CheckStruct(ctx, req, nil); err != nil {
+		return myerrors.ValidError(err.Error())
+	}
+	s, err := services.NewPlatMeetingService(ctx)
+	if err != nil {
+		return err
+	}
+	if err := s.Create(req); err != nil {
+		return err
+	}
+	rsp.Data = g.Map{"message": "新增成功"}
+	return nil
+}
+
+// UpdateById 更新会议
+func (h *PlatMeetingHandler) UpdateById(ctx context.Context, req *opsdevmodel.PlatMeetingUpdateReq, rsp *comm_def.CommonMsg) error {
+	if err := gvalid.CheckStruct(ctx, req, nil); err != nil {
+		return myerrors.ValidError(err.Error())
+	}
+	s, err := services.NewPlatMeetingService(ctx)
+	if err != nil {
+		return err
+	}
+	if err := s.UpdateById(req); err != nil {
+		return err
+	}
+	rsp.Data = g.Map{"message": "更新成功"}
+	return nil
+}
+
+// DeleteByIds 删除会议
+func (h *PlatMeetingHandler) DeleteByIds(ctx context.Context, req *opsdevmodel.PlatMeetingDeleteReq, rsp *comm_def.CommonMsg) error {
+	if err := gvalid.CheckStruct(ctx, req, nil); err != nil {
+		return myerrors.ValidError(err.Error())
+	}
+	s, err := services.NewPlatMeetingService(ctx)
+	if err != nil {
+		return err
+	}
+	if err := s.DeleteByIds(req.Ids); err != nil {
+		return err
+	}
+	rsp.Data = g.Map{"message": "删除成功"}
+	return nil
+}
+
+// GetById 根据ID获取会议详情
+func (h *PlatMeetingHandler) GetById(ctx context.Context, req *opsdevmodel.IdReq, rsp *comm_def.CommonMsg) error {
+	if err := gvalid.CheckStruct(ctx, req, nil); err != nil {
+		return myerrors.ValidError(err.Error())
+	}
+	s, err := services.NewPlatMeetingService(ctx)
+	if err != nil {
+		return err
+	}
+	data, err := s.GetById(req.Id)
+	if err != nil {
+		return err
+	}
+	rsp.Data = g.Map{"data": data}
+	return nil
+}
+
+// AddAttendees 追加参会人员
+func (h *PlatMeetingHandler) AddAttendees(ctx context.Context, req *opsdevmodel.PlatMeetingAddAttendeeReq, rsp *comm_def.CommonMsg) error {
+	if err := gvalid.CheckStruct(ctx, req, nil); err != nil {
+		return myerrors.ValidError(err.Error())
+	}
+	s, err := services.NewPlatMeetingService(ctx)
+	if err != nil {
+		return err
+	}
+	if err := s.AddAttendees(req); err != nil {
+		return err
+	}
+	rsp.Data = g.Map{"message": "追加成功"}
+	return nil
+}
+
+// Complete 结束会议(计算时长并生成工时)
+func (h *PlatMeetingHandler) Complete(ctx context.Context, req *opsdevmodel.PlatMeetingCompleteReq, rsp *comm_def.CommonMsg) error {
+	if err := gvalid.CheckStruct(ctx, req, nil); err != nil {
+		return myerrors.ValidError(err.Error())
+	}
+	s, err := services.NewPlatMeetingService(ctx)
+	if err != nil {
+		return err
+	}
+	if err := s.Complete(req); err != nil {
+		return err
+	}
+	rsp.Data = g.Map{"message": "会议已结束,工时已生成"}
+	return nil
+}
+
+// GetWorkHourList 获取会议工时记录
+func (h *PlatMeetingHandler) GetWorkHourList(ctx context.Context, req *opsdevmodel.IdReq, rsp *comm_def.CommonMsg) error {
+	if err := gvalid.CheckStruct(ctx, req, nil); err != nil {
+		return myerrors.ValidError(err.Error())
+	}
+	s, err := services.NewPlatMeetingService(ctx)
+	if err != nil {
+		return err
+	}
+	list, err := s.GetWorkHourList(req.Id)
+	if err != nil {
+		return err
+	}
+	rsp.Data = g.Map{"list": list}
+	return nil
+}

+ 20 - 0
opms_parent/app/model/contract/ctr_contract_application.go

@@ -0,0 +1,20 @@
+// ==========================================================================
+// This is auto-generated by gf cli tool. Fill this file as you wish.
+// ==========================================================================
+
+package contract
+
+import (
+	"dashoo.cn/opms_parent/app/model/contract/internal"
+)
+
+// CtrContractApplication is the golang structure for table ctr_contract_application.
+type CtrContractApplication internal.CtrContractApplication
+
+type CreateFromBusinessReq struct {
+	 NboId           int    `json:"nboId"`
+	 NboName         string `json:"nboName"`
+	 ProductLine     string `json:"productLine"`
+	 CustName        string `json:"custName"`
+	 DistributorName string `json:"distributorName"`
+ }

+ 28 - 0
opms_parent/app/model/contract/internal/ctr_contract_application.go

@@ -0,0 +1,28 @@
+// ==========================================================================
+// This is auto-generated by gf cli tool. DO NOT EDIT THIS FILE MANUALLY.
+// ==========================================================================
+
+package internal
+
+import (
+	"github.com/gogf/gf/os/gtime"
+)
+
+// CtrContractApplication is the golang structure for table ctr_contract_application.
+type CtrContractApplication struct {
+	Id           int         `orm:"id,primary"    json:"id"`           // 主键
+	NboId        int         `orm:"nbo_id"        json:"nboId"`        // 关联项目
+	NboName      string      `orm:"nbo_name"      json:"nboName"`      // 项目名称
+	ContractId   int         `orm:"contract_id"   json:"contractId"`   // 关联合同
+	ContractCode string      `orm:"contract_code" json:"contractCode"` // 合同编号
+	ContractName string      `orm:"contract_name" json:"contractName"` // 合同名称
+	Status       string      `orm:"status"        json:"status"`       // 审核状态 10 待登记 20 已登记
+	Remark       string      `orm:"remark"        json:"remark"`       // 备注
+	CreatedBy    int         `orm:"created_by"    json:"createdBy"`    // 创建者
+	CreatedName  string      `orm:"created_name"  json:"createdName"`  // 创建人
+	CreatedTime  *gtime.Time `orm:"created_time"  json:"createdTime"`  // 创建时间
+	UpdatedBy    int         `orm:"updated_by"    json:"updatedBy"`    // 更新者
+	UpdatedName  string      `orm:"updated_name"  json:"updatedName"`  // 更新人
+	UpdatedTime  *gtime.Time `orm:"updated_time"  json:"updatedTime"`  // 更新时间
+	DeletedTime  *gtime.Time `orm:"deleted_time"  json:"deletedTime"`  // 删除时间
+}

+ 30 - 0
opms_parent/app/model/opsdev/internal/plat_meeting.go

@@ -0,0 +1,30 @@
+// ==========================================================================
+// This is auto-generated by gf cli tool. DO NOT EDIT THIS FILE MANUALLY.
+// ==========================================================================
+
+package internal
+
+import (
+	"github.com/gogf/gf/os/gtime"
+)
+
+// PlatMeeting is the golang structure for table plat_meeting.
+type PlatMeeting struct {
+	Id             uint64      `orm:"id,primary"       json:"id"`             // 主键
+	MeetingTitle   string      `orm:"meeting_title"    json:"meetingTitle"`   // 会议标题
+	MeetingContent string      `orm:"meeting_content"  json:"meetingContent"` // 会议内容/议题
+	MeetingDate    *gtime.Time `orm:"meeting_date"     json:"meetingDate"`    // 会议开始时间
+	MeetingDateEnd *gtime.Time `orm:"meeting_date_end" json:"meetingDateEnd"` // 会议结束时间
+	Duration       float64     `orm:"duration"         json:"duration"`       // 会议时长(小时)
+	OrganizerId    int         `orm:"organizer_id"     json:"organizerId"`    // 会议负责人ID
+	OrganizerName  string      `orm:"organizer_name"   json:"organizerName"`  // 会议负责人姓名
+	DeptId         int         `orm:"dept_id"          json:"deptId"`         // 所属部门ID
+	Remark         string      `orm:"remark"           json:"remark"`         // 备注
+	CreatedBy      int         `orm:"created_by"       json:"createdBy"`      // 创建者
+	CreatedName    string      `orm:"created_name"     json:"createdName"`    // 创建人
+	CreatedTime    *gtime.Time `orm:"created_time"     json:"createdTime"`    // 创建时间
+	UpdatedBy      int         `orm:"updated_by"       json:"updatedBy"`      // 更新者
+	UpdatedName    string      `orm:"updated_name"     json:"updatedName"`    // 更新人
+	UpdatedTime    *gtime.Time `orm:"updated_time"     json:"updatedTime"`    // 更新时间
+	DeletedTime    *gtime.Time `orm:"deleted_time"     json:"deletedTime"`    // 删除时间
+}

+ 25 - 0
opms_parent/app/model/opsdev/internal/plat_meeting_attendee.go

@@ -0,0 +1,25 @@
+// ==========================================================================
+// This is auto-generated by gf cli tool. DO NOT EDIT THIS FILE MANUALLY.
+// ==========================================================================
+
+package internal
+
+import (
+	"github.com/gogf/gf/os/gtime"
+)
+
+// PlatMeetingAttendee is the golang structure for table plat_meeting_attendee.
+type PlatMeetingAttendee struct {
+	Id                uint64      `orm:"id,primary"          json:"id"`                // 主键
+	MeetingId         int         `orm:"meeting_id"          json:"meetingId"`         // 会议ID
+	UserId            int         `orm:"user_id"             json:"userId"`            // 参会人员ID
+	UserName          string      `orm:"user_name"           json:"userName"`          // 参会人员姓名
+	WorkHourGenerated int         `orm:"work_hour_generated" json:"workHourGenerated"` // 是否已生成工时: 0=否, 1=是
+	CreatedBy         int         `orm:"created_by"          json:"createdBy"`         // 创建者
+	CreatedName       string      `orm:"created_name"        json:"createdName"`       // 创建人
+	CreatedTime       *gtime.Time `orm:"created_time"        json:"createdTime"`       // 创建时间
+	UpdatedBy         int         `orm:"updated_by"          json:"updatedBy"`         // 更新者
+	UpdatedName       string      `orm:"updated_name"        json:"updatedName"`       // 更新人
+	UpdatedTime       *gtime.Time `orm:"updated_time"        json:"updatedTime"`       // 更新时间
+	DeletedTime       *gtime.Time `orm:"deleted_time"        json:"deletedTime"`       // 删除时间
+}

+ 28 - 0
opms_parent/app/model/opsdev/internal/plat_meeting_work_hour.go

@@ -0,0 +1,28 @@
+// ==========================================================================
+// This is auto-generated by gf cli tool. DO NOT EDIT THIS FILE MANUALLY.
+// ==========================================================================
+
+package internal
+
+import (
+	"github.com/gogf/gf/os/gtime"
+)
+
+// PlatMeetingWorkHour is the golang structure for table plat_meeting_work_hour.
+type PlatMeetingWorkHour struct {
+	Id          uint64      `orm:"id,primary"   json:"id"`          // 主键
+	MeetingId   int         `orm:"meeting_id"   json:"meetingId"`   // 关联会议ID
+	AttendeeId  int         `orm:"attendee_id"  json:"attendeeId"`  // 参会记录ID(plat_meeting_attendee.id)
+	UserId      int         `orm:"user_id"      json:"userId"`      // 人员ID
+	UserName    string      `orm:"user_name"    json:"userName"`    // 人员姓名
+	WorkDate    *gtime.Time `orm:"work_date"    json:"workDate"`    // 工时日期
+	WorkHour    float64     `orm:"work_hour"    json:"workHour"`    // 工时(小时)
+	Remark      string      `orm:"remark"       json:"remark"`      // 工作内容备注
+	CreatedBy   int         `orm:"created_by"   json:"createdBy"`   // 创建者
+	CreatedName string      `orm:"created_name" json:"createdName"` // 创建人
+	CreatedTime *gtime.Time `orm:"created_time" json:"createdTime"` // 创建时间
+	UpdatedBy   int         `orm:"updated_by"   json:"updatedBy"`   // 更新者
+	UpdatedName string      `orm:"updated_name" json:"updatedName"` // 更新人
+	UpdatedTime *gtime.Time `orm:"updated_time" json:"updatedTime"` // 更新时间
+	DeletedTime *gtime.Time `orm:"deleted_time" json:"deletedTime"` // 删除时间
+}

+ 6 - 0
opms_parent/app/model/opsdev/operation.go

@@ -56,6 +56,12 @@ type OpsOperationEventProcessReq struct {
 	HandleResult    string  `json:"handleResult"`
 	OperateType     string  `json:"operateType"     v:"required#操作类型不能为空"`
 	AdjustWorkHour  float64 `json:"adjustWorkHour"` // 关单时调整累计工时(可选,仅 operateType=80 时生效)
+	// 转研发专用字段
+	OpsUserId       int     `json:"opsUserId"`
+	OpsUserName     string  `json:"opsUserName"`
+	PlanStartTime   string  `json:"planStartTime"`
+	PlanEndTime     string  `json:"planEndTime"`
+	TaskStatus      string  `json:"taskStatus"`
 }
 
 // OpsOperationEventRecordSearchReq 运维事件处理记录搜索请求

+ 15 - 0
opms_parent/app/model/opsdev/ops_delivery_project.go

@@ -76,6 +76,21 @@ type OpsDeliveryProjectUpdateReq struct {
 	Attribute4       int    `json:"attribute4"` // 运维负责人ID
 }
 
+// OpsDeliveryProjectDelegatedSearchReq 委托项目查询请求(自己负责 + 被授权)
+type OpsDeliveryProjectDelegatedSearchReq struct {
+	PageNum          int    `json:"pageNum" form:"pageNum"`                   // 页码
+	PageSize         int    `json:"pageSize" form:"pageSize"`                 // 每页条数
+	KeyWords         string `json:"keyWords" form:"keyWords"`                 // 关键词搜索
+	Status           string `json:"status" form:"status"`                     // 项目状态
+	ProjectStatus    string `json:"projectStatus" form:"projectStatus"`       // 项目状态(支持多选,逗号分隔)
+	ProductLine      string `json:"productLine" form:"productLine"`           // 产品线
+	ProjectName      string `json:"projectName" form:"projectName"`           // 项目名称
+	SalesUserName    string `json:"salesUserName" form:"salesUserName"`       // 销售负责人
+	DeliveryUserName string `json:"deliveryUserName" form:"deliveryUserName"` // 交付负责人
+	SortField        string `json:"sortField" form:"sortField"`               // 排序字段
+	SortOrder        string `json:"sortOrder" form:"sortOrder"`               // 排序方式
+}
+
 // AssignDeliveryUserReq 指派/改派交付负责人请求
 type AssignDeliveryUserReq struct {
 	ProjectId        int    `json:"projectId" v:"required#项目ID不能为空"`      // 项目ID

+ 17 - 1
opms_parent/app/model/opsdev/ops_delivery_project_delegate.go

@@ -11,4 +11,20 @@ import (
 // OpsDeliveryProjectDelegate is the golang structure for table ops_delivery_project_delegate.
 type OpsDeliveryProjectDelegate internal.OpsDeliveryProjectDelegate
 
-// Fill with you ideas below.
+// OpsDeliveryProjectDelegateListReq 查询项目授权人列表请求
+type OpsDeliveryProjectDelegateListReq struct {
+	ProjectId int `json:"projectId" v:"required#项目ID不能为空"`
+}
+
+// OpsDeliveryProjectDelegateAddReq 添加授权人请求
+type OpsDeliveryProjectDelegateAddReq struct {
+	ProjectId   int    `json:"projectId" v:"required#项目ID不能为空"`
+	ProjectName string `json:"projectName"`
+	UserId      int    `json:"userId" v:"required#被授权人ID不能为空"`
+	UserName    string `json:"userName" v:"required#被授权人姓名不能为空"`
+}
+
+// OpsDeliveryProjectDelegateDeleteReq 删除授权人请求
+type OpsDeliveryProjectDelegateDeleteReq struct {
+	Id int `json:"id" v:"required#ID不能为空"`
+}

+ 1 - 0
opms_parent/app/model/opsdev/ops_event_task.go

@@ -294,6 +294,7 @@ type OpsEventTaskUserDayStat struct {
 type OpsEventTaskUserScheduleStat struct {
 	OpsUserId   int                         `json:"opsUserId"`   // 用户ID
 	OpsUserName string                      `json:"opsUserName"` // 用户名称
+	GroupName   string                      `json:"groupName"`   // 所属分组
 	DayStats    []*OpsEventTaskUserDayStat  `json:"dayStats"`    // 每天统计
 	WeekTotal   *OpsEventTaskUserDayStat    `json:"weekTotal"`   // 本周合计
 }

+ 113 - 0
opms_parent/app/model/opsdev/plat_meeting.go

@@ -0,0 +1,113 @@
+// ==========================================================================
+// Code generated and maintained by gf cli tool. DO NOT EDIT.
+// ==========================================================================
+
+package opsdev
+
+import (
+	"dashoo.cn/opms_libary/request"
+	"dashoo.cn/opms_parent/app/model/opsdev/internal"
+	"github.com/gogf/gf/os/gtime"
+)
+
+// PlatMeeting is the golang structure for table plat_meeting.
+type PlatMeeting internal.PlatMeeting
+
+// PlatMeetingSearchReq 会议列表查询请求
+type PlatMeetingSearchReq struct {
+	request.PageReq
+	MeetingTitle     string `json:"meetingTitle"`
+	OrganizerName    string `json:"organizerName"`
+	DeptId           int    `json:"deptId"`
+	MeetingDateStart string `json:"meetingDateStart"`
+	MeetingDateEnd   string `json:"meetingDateEnd"`
+}
+
+// PlatMeetingRsp 会议列表响应
+type PlatMeetingRsp struct {
+	Id             uint64                      `json:"id"`
+	MeetingTitle   string                      `json:"meetingTitle"`
+	MeetingContent string                      `json:"meetingContent"`
+	MeetingDate    *gtime.Time                 `json:"meetingDate"`
+	MeetingDateEnd *gtime.Time                 `json:"meetingDateEnd"`
+	Duration       float64                     `json:"duration"`
+	OrganizerId    int                         `json:"organizerId"`
+	OrganizerName  string                      `json:"organizerName"`
+	DeptId         int                         `json:"deptId"`
+	Remark         string                      `json:"remark"`
+	CreatedBy      int                         `json:"createdBy"`
+	CreatedName    string                      `json:"createdName"`
+	CreatedTime    *gtime.Time                 `json:"createdTime"`
+	UpdatedBy      int                         `json:"updatedBy"`
+	UpdatedName    string                      `json:"updatedName"`
+	UpdatedTime    *gtime.Time                 `json:"updatedTime"`
+	Attendees      []*PlatMeetingAttendeeRsp   `json:"attendees"`
+}
+
+// PlatMeetingAddReq 新增会议请求
+type PlatMeetingAddReq struct {
+	MeetingTitle   string   `json:"meetingTitle" v:"required#会议标题不能为空"`
+	MeetingContent string   `json:"meetingContent"`
+	MeetingDate    string   `json:"meetingDate" v:"required#会议日期不能为空"`
+	Duration       float64  `json:"duration" v:"required#会议时长不能为空"`
+	OrganizerId    int      `json:"organizerId" v:"required#会议负责人不能为空"`
+	OrganizerName  string   `json:"organizerName" v:"required#会议负责人不能为空"`
+	DeptId         int      `json:"deptId"`
+	Remark         string   `json:"remark"`
+	UserIds        []int    `json:"userIds" v:"required#参会人员不能为空"`
+	UserNames      []string `json:"userNames" v:"required#参会人员不能为空"`
+}
+
+// PlatMeetingUpdateReq 更新会议请求
+type PlatMeetingUpdateReq struct {
+	Id             int      `json:"id" v:"required#会议ID不能为空"`
+	MeetingTitle   string   `json:"meetingTitle"`
+	MeetingContent string   `json:"meetingContent"`
+	MeetingDate    string   `json:"meetingDate"`
+	Duration       float64  `json:"duration"`
+	OrganizerId    int      `json:"organizerId"`
+	OrganizerName  string   `json:"organizerName"`
+	DeptId         int      `json:"deptId"`
+	Remark         string   `json:"remark"`
+	UserIds        []int    `json:"userIds"`
+	UserNames      []string `json:"userNames"`
+}
+
+// PlatMeetingDeleteReq 删除会议请求
+type PlatMeetingDeleteReq struct {
+	Ids []int64 `json:"ids" v:"required#请选择需要删除的记录"`
+}
+
+// PlatMeetingCompleteReq 结束会议请求(生成参会人员工时)
+type PlatMeetingCompleteReq struct {
+	Id int `json:"id" v:"required#会议ID不能为空"`
+}
+
+// PlatMeetingAttendeeRsp 参会人员响应
+type PlatMeetingAttendeeRsp struct {
+	Id                uint64      `json:"id"`
+	MeetingId         int         `json:"meetingId"`
+	UserId            int         `json:"userId"`
+	UserName          string      `json:"userName"`
+	WorkHourGenerated int         `json:"workHourGenerated"`
+	CreatedTime       *gtime.Time `json:"createdTime"`
+}
+
+// PlatMeetingAddAttendeeReq 追加参会人员请求
+type PlatMeetingAddAttendeeReq struct {
+	MeetingId int   `json:"meetingId" v:"required#会议ID不能为空"`
+	UserIds   []int    `json:"userIds" v:"required#参会人员不能为空"`
+	UserNames []string `json:"userNames" v:"required#参会人员不能为空"`
+}
+
+// PlatMeetingWorkHourRsp 会议工时响应
+type PlatMeetingWorkHourRsp struct {
+	Id         uint64      `json:"id"`
+	MeetingId  int         `json:"meetingId"`
+	AttendeeId int         `json:"attendeeId"`
+	UserId     int         `json:"userId"`
+	UserName   string      `json:"userName"`
+	WorkDate   *gtime.Time `json:"workDate"`
+	WorkHour   float64     `json:"workHour"`
+	Remark     string      `json:"remark"`
+}

+ 14 - 0
opms_parent/app/model/opsdev/plat_meeting_attendee.go

@@ -0,0 +1,14 @@
+// ==========================================================================
+// This is auto-generated by gf cli tool. Fill this file as you wish.
+// ==========================================================================
+
+package opsdev
+
+import (
+	"dashoo.cn/opms_parent/app/model/opsdev/internal"
+)
+
+// PlatMeetingAttendee is the golang structure for table plat_meeting_attendee.
+type PlatMeetingAttendee internal.PlatMeetingAttendee
+
+// Fill with you ideas below.

+ 14 - 0
opms_parent/app/model/opsdev/plat_meeting_work_hour.go

@@ -0,0 +1,14 @@
+// ==========================================================================
+// This is auto-generated by gf cli tool. Fill this file as you wish.
+// ==========================================================================
+
+package opsdev
+
+import (
+	"dashoo.cn/opms_parent/app/model/opsdev/internal"
+)
+
+// PlatMeetingWorkHour is the golang structure for table plat_meeting_work_hour.
+type PlatMeetingWorkHour internal.PlatMeetingWorkHour
+
+// Fill with you ideas below.

+ 7 - 5
opms_parent/app/model/opsdev/work_hour_stat.go

@@ -22,15 +22,17 @@ type DayHeader struct {
 
 // PersonRow 单人员工时行
 type PersonRow struct {
-	UserId      int                `json:"userId"`
-	UserName    string             `json:"userName"`
-	DailyHours  map[string]DayData `json:"dailyHours"`
-	TotalOpHour float64            `json:"totalOpHour"`
-	TotalRdHour float64            `json:"totalRdHour"`
+	UserId       int                `json:"userId"`
+	UserName     string             `json:"userName"`
+	DailyHours   map[string]DayData `json:"dailyHours"`
+	TotalOpHour  float64            `json:"totalOpHour"`
+	TotalRdHour  float64            `json:"totalRdHour"`
+	TotalMtHour  float64            `json:"totalMtHour"`
 }
 
 // DayData 单日工时明细
 type DayData struct {
 	OpHour float64 `json:"opHour"`
 	RdHour float64 `json:"rdHour"`
+	MtHour float64 `json:"mtHour"`
 }

+ 78 - 0
opms_parent/app/service/contract/ctr_contract_application.go

@@ -0,0 +1,78 @@
+package service
+
+import (
+	"context"
+	"fmt"
+
+	"dashoo.cn/opms_libary/micro_srv"
+	"dashoo.cn/opms_libary/myerrors"
+	"dashoo.cn/opms_libary/request"
+	dao "dashoo.cn/opms_parent/app/dao/contract"
+	model "dashoo.cn/opms_parent/app/model/contract"
+	"github.com/gogf/gf/frame/g"
+)
+
+type CtrContractApplicationService struct {
+	Dao      *dao.CtrContractApplicationDao
+	Tenant   string
+	userInfo request.UserInfo
+}
+
+func NewCtrContractApplicationService(ctx context.Context) (*CtrContractApplicationService, error) {
+	tenant, err := micro_srv.GetTenant(ctx)
+	if err != nil {
+		err = myerrors.TipsError(fmt.Sprintf("获取租户码异常:%s", err.Error()))
+		return nil, err
+	}
+	userInfo, err := micro_srv.GetUserInfo(ctx)
+	if err != nil {
+		err = myerrors.TipsError(fmt.Sprintf("获取用户信息异常:%s", err.Error()))
+		return nil, err
+	}
+	return &CtrContractApplicationService{
+		Dao:      dao.NewCtrContractApplicationDao(tenant),
+		Tenant:   tenant,
+		userInfo: userInfo,
+	}, nil
+}
+
+func (s *CtrContractApplicationService) CreateFromBusiness(req *model.CreateFromBusinessReq) (int, error) {
+	exists, err := s.CheckPendingApplication(req.NboId)
+	if err != nil {
+		return 0, err
+	}
+	if exists {
+		return 0, myerrors.TipsError("该项目已有待处理的合同申请")
+	}
+
+	data := g.Map{
+		"nbo_id":       req.NboId,
+		"nbo_name":     req.NboName,
+		"status":       "10",
+		"created_by":   s.userInfo.Id,
+		"created_name": s.userInfo.UserName,
+	}
+
+	result, err := s.Dao.Insert(data)
+	if err != nil {
+		return 0, myerrors.CreateError(err, "合同申请")
+	}
+
+	lastId, err := result.LastInsertId()
+	if err != nil {
+		return 0, err
+	}
+
+	return int(lastId), nil
+}
+
+func (s *CtrContractApplicationService) CheckPendingApplication(nboId int) (bool, error) {
+	record, err := s.Dao.Where("nbo_id", nboId).
+		Where("status", "10").
+		Where("deleted_time IS NULL").
+		One()
+	if err != nil {
+		return false, err
+	}
+	return record != nil, nil
+}

+ 194 - 0
opms_parent/app/service/opsdev/delivery_project.go

@@ -193,6 +193,114 @@ func (s *DeliveryProjectService) GetListAll(req *opsdevmodel.OpsDeliveryProjectS
 	return int64(total), list, nil
 }
 
+// GetDelegatedProjectList 获取委托项目列表(自己负责 + 被授权)
+func (s *DeliveryProjectService) GetDelegatedProjectList(req *opsdevmodel.OpsDeliveryProjectDelegatedSearchReq) (int64, []*opsdevmodel.OpsDeliveryProject, error) {
+	userId := s.GetCxtUserId()
+	if userId <= 0 {
+		return 0, nil, errors.New("无法获取当前用户信息")
+	}
+
+	m := s.Dao.Ctx(s.Ctx)
+
+	where := s.buildPermissionWhere()
+	if where != "" {
+		// 查询被授权给当前用户的委托项目 ID
+		delegateModel := eventdao.NewOpsDeliveryProjectDelegateDao(s.Tenant).Ctx(s.Ctx)
+		var delegates []*opsdevmodel.OpsDeliveryProjectDelegate
+		if err := delegateModel.Where("user_id", userId).Scan(&delegates); err != nil {
+			return 0, nil, err
+		}
+
+		var delegatedIds []int
+		for _, d := range delegates {
+			delegatedIds = append(delegatedIds, d.ProjectId)
+		}
+
+		if len(delegatedIds) > 0 {
+			m = m.Where(fmt.Sprintf("(delivery_user_id = %d OR id IN (%s))", userId, intSliceToSqlIn(delegatedIds)))
+		} else {
+			m = m.Where("delivery_user_id", userId)
+		}
+	}
+
+	// 项目状态筛选(支持多选,逗号分隔)
+	if req.ProjectStatus != "" {
+		statusList := strings.Split(req.ProjectStatus, ",")
+		if len(statusList) > 0 {
+			m = m.Where("project_status", statusList)
+		}
+	}
+
+	// 项目名称模糊查询
+	if req.ProjectName != "" {
+		m = m.Where("project_name like ?", "%"+req.ProjectName+"%")
+	}
+
+	// 销售负责人模糊查询
+	if req.SalesUserName != "" {
+		m = m.Where("sales_user_name like ?", "%"+req.SalesUserName+"%")
+	}
+
+	// 交付负责人模糊查询
+	if req.DeliveryUserName != "" {
+		m = m.Where("delivery_user_name like ?", "%"+req.DeliveryUserName+"%")
+	}
+
+	// 关键词搜索(项目名称、合同编号)
+	if req.KeyWords != "" {
+		m = m.Where("project_name like ? OR contract_no like ?", "%"+req.KeyWords+"%", "%"+req.KeyWords+"%")
+	}
+
+	// 其他查询条件
+	if req.Status != "" {
+		m = m.Where("project_status", req.Status)
+	}
+	if req.ProductLine != "" {
+		productLineList := strings.Split(req.ProductLine, ",")
+		m = m.Where("product_line", productLineList)
+	}
+
+	// 获取总数
+	total, err := m.Count()
+	if err != nil {
+		return 0, nil, err
+	}
+
+	// 分页查询
+	if req.PageNum <= 0 {
+		req.PageNum = 1
+	}
+	if req.PageSize <= 0 {
+		req.PageSize = 20
+	}
+
+	// 排序
+	orderBy := "created_time desc"
+	if req.SortField != "" && req.SortOrder != "" {
+		orderBy = req.SortField + " " + req.SortOrder
+	}
+
+	var list []*opsdevmodel.OpsDeliveryProject
+	err = m.Page(req.PageNum, req.PageSize).Order(orderBy).Scan(&list)
+	if err != nil {
+		return 0, nil, err
+	}
+
+	return int64(total), list, nil
+}
+
+// intSliceToSqlIn 将整数切片转为 SQL IN 子句字符串
+func intSliceToSqlIn(ids []int) string {
+	if len(ids) == 0 {
+		return ""
+	}
+	strIds := make([]string, len(ids))
+	for i, id := range ids {
+		strIds[i] = strconv.Itoa(id)
+	}
+	return strings.Join(strIds, ",")
+}
+
 // buildPermissionWhere 构建权限条件
 func (s *DeliveryProjectService) buildPermissionWhere() string {
 	// 全部可见角色
@@ -461,3 +569,89 @@ func (s *DeliveryProjectService) AssignDeliveryUser(req *opsdevmodel.AssignDeliv
 	_, err = s.Dao.Where("id", req.ProjectId).Data(data).Update()
 	return err
 }
+
+// GetDelegatesByProjectId 获取项目授权人列表
+func (s *DeliveryProjectService) GetDelegatesByProjectId(req *opsdevmodel.OpsDeliveryProjectDelegateListReq) ([]*opsdevmodel.OpsDeliveryProjectDelegate, error) {
+	delegateModel := eventdao.NewOpsDeliveryProjectDelegateDao(s.Tenant).Ctx(s.Ctx)
+	var list []*opsdevmodel.OpsDeliveryProjectDelegate
+	if err := delegateModel.Where("project_id", req.ProjectId).Scan(&list); err != nil {
+		return nil, err
+	}
+	return list, nil
+}
+
+// AddDelegate 添加授权人
+func (s *DeliveryProjectService) AddDelegate(req *opsdevmodel.OpsDeliveryProjectDelegateAddReq) error {
+	if req.ProjectId <= 0 {
+		return errors.New("项目ID不能为空")
+	}
+	if req.UserId <= 0 {
+		return errors.New("被授权人ID不能为空")
+	}
+
+	// 权限校验
+	hasManageRole := service.StringsContains(s.CxtUser.Roles, "ResearchAndDevelopmentDirector") ||
+		service.StringsContains(s.CxtUser.Roles, "ResearchAndDevelopmentSupervisor")
+	if !hasManageRole {
+		return errors.New("只有研发总监或研发主管才能管理授权人")
+	}
+
+	// 检查项目是否存在
+	project, err := s.Dao.Where("id", req.ProjectId).FindOne()
+	if err != nil {
+		return err
+	}
+	if project == nil {
+		return errors.New("项目不存在")
+	}
+
+	// 检查是否已授权
+	delegateModel := eventdao.NewOpsDeliveryProjectDelegateDao(s.Tenant).Ctx(s.Ctx)
+	existing, err := delegateModel.Where("project_id", req.ProjectId).Where("user_id", req.UserId).FindOne()
+	if err != nil {
+		return err
+	}
+	if existing != nil {
+		return errors.New("该用户已被授权")
+	}
+
+	// 确定项目名称
+	projectName := req.ProjectName
+	if projectName == "" {
+		projectName = project.ProjectName
+	}
+
+	userId := s.GetCxtUserId()
+	userName := s.GetCxtUserName()
+
+	data := g.Map{
+		"project_id":   req.ProjectId,
+		"project_name": projectName,
+		"user_id":      req.UserId,
+		"user_name":    req.UserName,
+		"created_by":   userId,
+		"created_name": userName,
+		"created_time": gtime.Now(),
+		"updated_time": gtime.Now(),
+	}
+	_, err = delegateModel.Data(data).Insert()
+	return err
+}
+
+// RemoveDelegate 移除授权人
+func (s *DeliveryProjectService) RemoveDelegate(req *opsdevmodel.OpsDeliveryProjectDelegateDeleteReq) error {
+	if req.Id <= 0 {
+		return errors.New("ID不能为空")
+	}
+
+	// 权限校验
+	hasManageRole := service.StringsContains(s.CxtUser.Roles, "ResearchAndDevelopmentDirector") ||
+		service.StringsContains(s.CxtUser.Roles, "ResearchAndDevelopmentSupervisor")
+	if !hasManageRole {
+		return errors.New("只有研发总监或研发主管才能管理授权人")
+	}
+
+	delegateModel := eventdao.NewOpsDeliveryProjectDelegateDao(s.Tenant).Ctx(s.Ctx)
+	_, err := delegateModel.Where("id", req.Id).Delete()
+	return err
+}

+ 14 - 12
opms_parent/app/service/opsdev/operation.go

@@ -812,7 +812,7 @@ func (s *OperationService) getDevTaskType(eventType string) string {
 }
 
 // createDevTaskFromEvent 根据运维事件自动创建研发任务(在processNormal事务完成后调用)
-func (s *OperationService) createDevTaskFromEvent(event *opsdevmodel.OpsOperationEvent) {
+func (s *OperationService) createDevTaskFromEvent(event *opsdevmodel.OpsOperationEvent, req *opsdevmodel.OpsOperationEventProcessReq) {
 	var attachments []*opsdevmodel.OpsOperationEventAttachment
 	err := s.AttachmentDao.FieldsEx(s.AttachmentDao.Columns.DeletedTime).
 		Where(s.AttachmentDao.Columns.EventId, event.Id).
@@ -850,16 +850,18 @@ func (s *OperationService) createDevTaskFromEvent(event *opsdevmodel.OpsOperatio
 	}
 
 	taskReq := &opsdevmodel.OpsEventTaskAddReq{
-		ProjectId:   projectId,
-		TaskTitle:   event.EventTitle,
-		TaskDesc:    event.EventDesc,
-		TaskType:    taskType,
-		Priority:    event.PriorityLevel,
-		OpsUserId:   event.OpsUserId,
-		OpsUserName: event.OpsUserName,
-		EventId:     event.Id,
-		EventType:   opsdevmodel.EventTypeOps,
-		Attachments: taskAttachments,
+		ProjectId:     projectId,
+		TaskTitle:     event.EventTitle,
+		TaskDesc:      event.EventDesc,
+		TaskType:      taskType,
+		Priority:      event.PriorityLevel,
+		OpsUserId:     req.OpsUserId,
+		OpsUserName:   req.OpsUserName,
+		PlanStartTime: req.PlanStartTime,
+		PlanEndTime:   req.PlanEndTime,
+		EventId:       event.Id,
+		EventType:     opsdevmodel.EventTypeOps,
+		Attachments:   taskAttachments,
 	}
 
 	taskSvc, err := NewOpsEventTaskService(s.Ctx)
@@ -926,7 +928,7 @@ func (s *OperationService) processNormal(req *opsdevmodel.OpsOperationEventProce
 	}
 
 	if req.OperateType == opsdevmodel.OperateTypeTransfer {
-		s.createDevTaskFromEvent(event)
+		s.createDevTaskFromEvent(event, req)
 	}
 
 	return nil

+ 54 - 1
opms_parent/app/service/opsdev/ops_event_task.go

@@ -92,7 +92,7 @@ func (s *OpsEventTaskService) GetList(req *opsdevmodel.OpsEventTaskSearchReq) (t
 
 	// 发布版本为空筛选(用于发版任务选择未发版任务)
 	if req.ReleaseVersionEmpty {
-		db = db.Where(s.TaskDao.Columns.ReleaseVersion + " is null")
+		db = db.Where(s.TaskDao.Columns.ReleaseVersion + " is null or " + s.TaskDao.Columns.ReleaseVersion + " = ''")
 	}
 
 	// 排期状态筛选(plan_start_time/plan_end_time 为 datetime 类型,需用 IS NULL)
@@ -1456,6 +1456,24 @@ func (s *OpsEventTaskService) GetDashboardData(startDate, endDate string) (*opsd
 		hourMap[hourRows[i].WorkDate] = hourRows[i].TotalHour
 	}
 
+	// 合并会议工时
+	meetingSQL := `
+		SELECT DATE(work_date) AS work_date, SUM(work_hour) AS total_hour
+		FROM plat_meeting_work_hour
+		WHERE user_id = ?
+		  AND work_date >= ? AND work_date <= ?
+		  AND deleted_time IS NULL
+		GROUP BY DATE(work_date)
+	`
+	var meetingHourRows []hourRow
+	if err := db.GetScan(&meetingHourRows, meetingSQL, userId, startDate, endDate+" 23:59:59"); err != nil {
+		g.Log().Error(err)
+	} else {
+		for i := range meetingHourRows {
+			hourMap[meetingHourRows[i].WorkDate] += meetingHourRows[i].TotalHour
+		}
+	}
+
 	taskMap := make(map[string][]*opsdevmodel.DashboardTaskRsp, 7)
 	for i := range taskRows {
 		row := &taskRows[i]
@@ -1524,6 +1542,27 @@ func (s *OpsEventTaskService) canAddWorkHour(status string) bool {
 	return status == opsdevmodel.TaskStatusProcessing
 }
 
+// scheduleGroupMap 排期统计人员分组映射
+var scheduleGroupMap = map[string]string{
+	"徐洲":   "Biobank组",
+	"贾冀川": "Biobank组",
+	"徐凯":   "Biobank组",
+	"耿嘉强": "Biobank组",
+	"范相豪": "Biobank组",
+	"刘旗":   "Biobank组",
+	"周丰林": "LIMS组",
+	"刘振林": "LIMS组",
+	"张旭伟": "LIMS组",
+	"张涵":   "LIMS组",
+	"王晓宇": "CellSop组",
+	"李凯":   "CellSop组",
+	"王硕":   "CellSop组",
+	"卢传敏": "BiobankV4",
+	"韩明儒": "BiobankV4",
+	"石春蕾": "品质部",
+	"刘琦":   "品质部",
+}
+
 // GetScheduleStats 获取人员排期统计(按周、按天、按人汇总任务数和预估工时)
 func (s *OpsEventTaskService) GetScheduleStats(req *opsdevmodel.OpsEventTaskScheduleStatReq) (*opsdevmodel.OpsEventTaskScheduleStatRsp, error) {
 	type dayRow struct {
@@ -1555,6 +1594,19 @@ func (s *OpsEventTaskService) GetScheduleStats(req *opsdevmodel.OpsEventTaskSche
 		args = append(args, req.ProjectId)
 	}
 
+	allNames := make([]string, 0, len(scheduleGroupMap))
+	for name := range scheduleGroupMap {
+		allNames = append(allNames, name)
+	}
+	placeholders := make([]string, len(allNames))
+	for i := range placeholders {
+		placeholders[i] = "?"
+	}
+	sql += " AND ops_user_name IN (" + strings.Join(placeholders, ",") + ")"
+	for _, name := range allNames {
+		args = append(args, name)
+	}
+
 	sql += `
 		GROUP BY ops_user_id, ops_user_name, DATE(plan_start_time)
 		ORDER BY ops_user_name, plan_date
@@ -1575,6 +1627,7 @@ func (s *OpsEventTaskService) GetScheduleStats(req *opsdevmodel.OpsEventTaskSche
 			user = &opsdevmodel.OpsEventTaskUserScheduleStat{
 				OpsUserId:   row.OpsUserId,
 				OpsUserName: row.OpsUserName,
+				GroupName:   scheduleGroupMap[row.OpsUserName],
 				DayStats:    make([]*opsdevmodel.OpsEventTaskUserDayStat, 7),
 			}
 			// 初始化 7 天空数据

+ 465 - 0
opms_parent/app/service/opsdev/plat_meeting.go

@@ -0,0 +1,465 @@
+package opsdev
+
+import (
+	"context"
+
+	"dashoo.cn/opms_libary/myerrors"
+	opsdevdao "dashoo.cn/opms_parent/app/dao/opsdev"
+	opsdevmodel "dashoo.cn/opms_parent/app/model/opsdev"
+	"dashoo.cn/opms_parent/app/service"
+	"github.com/gogf/gf/database/gdb"
+	"github.com/gogf/gf/frame/g"
+	"github.com/gogf/gf/os/gtime"
+	"github.com/gogf/gf/util/gconv"
+)
+
+// PlatMeetingService 会议业务逻辑实现类
+type PlatMeetingService struct {
+	*service.ContextService
+	MeetingDao      *opsdevdao.PlatMeetingDao
+	AttendeeDao     *opsdevdao.PlatMeetingAttendeeDao
+	WorkHourDao     *opsdevdao.PlatMeetingWorkHourDao
+}
+
+// NewPlatMeetingService 初始化service
+func NewPlatMeetingService(ctx context.Context) (svc *PlatMeetingService, err error) {
+	svc = new(PlatMeetingService)
+	if svc.ContextService, err = svc.Init(ctx); err != nil {
+		return nil, err
+	}
+	svc.MeetingDao = opsdevdao.NewPlatMeetingDao(svc.Tenant)
+	svc.AttendeeDao = opsdevdao.NewPlatMeetingAttendeeDao(svc.Tenant)
+	svc.WorkHourDao = opsdevdao.NewPlatMeetingWorkHourDao(svc.Tenant)
+	return svc, nil
+}
+
+// GetList 分页查询会议列表
+func (s *PlatMeetingService) GetList(req *opsdevmodel.PlatMeetingSearchReq) (total int, list []*opsdevmodel.PlatMeetingRsp, err error) {
+	db := s.MeetingDao.FieldsEx(s.MeetingDao.Columns.DeletedTime)
+
+	if req.MeetingTitle != "" {
+		db = db.Where(s.MeetingDao.Columns.MeetingTitle+" like ?", "%"+req.MeetingTitle+"%")
+	}
+	if req.OrganizerName != "" {
+		db = db.Where(s.MeetingDao.Columns.OrganizerName+" like ?", "%"+req.OrganizerName+"%")
+	}
+	if req.DeptId > 0 {
+		db = db.Where(s.MeetingDao.Columns.DeptId, req.DeptId)
+	}
+	if req.MeetingDateStart != "" {
+		db = db.Where(s.MeetingDao.Columns.MeetingDate+" >= ?", req.MeetingDateStart)
+	}
+	if req.MeetingDateEnd != "" {
+		db = db.Where(s.MeetingDao.Columns.MeetingDate+" <= ?", req.MeetingDateEnd)
+	}
+
+	total, err = db.Count()
+	if err != nil {
+		return 0, nil, myerrors.DbError("获取会议总数失败")
+	}
+
+	pageNum, pageSize := req.GetPage()
+	db = db.Order(s.MeetingDao.Columns.CreatedTime + " desc")
+
+	var entities []*opsdevmodel.PlatMeeting
+	err = db.Page(pageNum, pageSize).Scan(&entities)
+	if err != nil {
+		return 0, nil, myerrors.DbError("查询会议列表失败")
+	}
+
+	if err = gconv.Structs(entities, &list); err != nil {
+		return 0, nil, myerrors.DbError("数据转换失败")
+	}
+
+	// 查询每个会议的参会人员
+	for _, item := range list {
+		attendees, _ := s.getAttendeesByMeetingId(int(item.Id))
+		item.Attendees = attendees
+	}
+
+	return
+}
+
+// GetById 根据ID获取会议详情
+func (s *PlatMeetingService) GetById(id int) (*opsdevmodel.PlatMeetingRsp, error) {
+	var entity opsdevmodel.PlatMeeting
+	err := s.MeetingDao.FieldsEx(s.MeetingDao.Columns.DeletedTime).
+		WherePri(s.MeetingDao.Columns.Id, id).Scan(&entity)
+	if err != nil {
+		return nil, myerrors.DbError("查询会议失败")
+	}
+	if entity.Id <= 0 {
+		return nil, myerrors.TipsError("会议不存在")
+	}
+
+	var rsp opsdevmodel.PlatMeetingRsp
+	if err := gconv.Struct(entity, &rsp); err != nil {
+		return nil, myerrors.DbError("数据转换失败")
+	}
+
+	attendees, err := s.getAttendeesByMeetingId(id)
+	if err != nil {
+		return nil, err
+	}
+	rsp.Attendees = attendees
+
+	return &rsp, nil
+}
+
+// Create 新增会议(含参会人员 + 工时生成)
+func (s *PlatMeetingService) Create(req *opsdevmodel.PlatMeetingAddReq) error {
+	data := g.Map{
+		s.MeetingDao.Columns.MeetingTitle:   req.MeetingTitle,
+		s.MeetingDao.Columns.MeetingContent: req.MeetingContent,
+		s.MeetingDao.Columns.OrganizerId:    req.OrganizerId,
+		s.MeetingDao.Columns.OrganizerName:  req.OrganizerName,
+		s.MeetingDao.Columns.DeptId:         req.DeptId,
+		s.MeetingDao.Columns.Duration:       req.Duration,
+		s.MeetingDao.Columns.Remark:         req.Remark,
+	}
+	if req.MeetingDate != "" {
+		data[s.MeetingDao.Columns.MeetingDate] = req.MeetingDate
+	}
+
+	service.SetCreatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
+
+	return s.MeetingDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
+		result, err := s.MeetingDao.TX(tx).Data(data).Insert()
+		if err != nil {
+			return myerrors.DbError("新增会议失败")
+		}
+		meetingId, err := result.LastInsertId()
+		if err != nil {
+			return myerrors.DbError("获取会议ID失败")
+		}
+
+		if err := s.batchInsertAttendees(tx, int(meetingId), req.UserIds, req.UserNames); err != nil {
+			return err
+		}
+
+		// 生成发起人工时
+		workDate := gtime.Now()
+		if req.MeetingDate != "" {
+			workDate = gtime.NewFromStr(req.MeetingDate)
+		}
+		orgWhData := g.Map{
+			s.WorkHourDao.Columns.MeetingId:  int(meetingId),
+			s.WorkHourDao.Columns.AttendeeId: 0,
+			s.WorkHourDao.Columns.UserId:     req.OrganizerId,
+			s.WorkHourDao.Columns.UserName:   req.OrganizerName,
+			s.WorkHourDao.Columns.WorkDate:   workDate,
+			s.WorkHourDao.Columns.WorkHour:   req.Duration,
+			s.WorkHourDao.Columns.Remark:     req.MeetingTitle,
+		}
+		service.SetCreatedInfo(orgWhData, s.GetCxtUserId(), s.GetCxtUserName())
+		if _, err := s.WorkHourDao.TX(tx).Data(orgWhData).Insert(); err != nil {
+			return myerrors.DbError("生成发起人工时失败")
+		}
+
+		// 生成参会人员工时
+		attendees, err := s.AttendeeDao.TX(tx).FieldsEx(s.AttendeeDao.Columns.DeletedTime).
+			Where(s.AttendeeDao.Columns.MeetingId, int(meetingId)).All()
+		if err != nil {
+			return myerrors.DbError("查询参会人员失败")
+		}
+		for _, row := range attendees {
+			whData := g.Map{
+				s.WorkHourDao.Columns.MeetingId:  int(meetingId),
+				s.WorkHourDao.Columns.AttendeeId: row.Id,
+				s.WorkHourDao.Columns.UserId:     row.UserId,
+				s.WorkHourDao.Columns.UserName:   row.UserName,
+				s.WorkHourDao.Columns.WorkDate:   workDate,
+				s.WorkHourDao.Columns.WorkHour:   req.Duration,
+				s.WorkHourDao.Columns.Remark:     req.MeetingTitle,
+			}
+			service.SetCreatedInfo(whData, s.GetCxtUserId(), s.GetCxtUserName())
+			if _, err := s.WorkHourDao.TX(tx).Data(whData).Insert(); err != nil {
+				return myerrors.DbError("生成参会人工时失败")
+			}
+		}
+
+		// 标记参会人工时已生成
+		if _, err := s.AttendeeDao.TX(tx).
+			Data(g.Map{s.AttendeeDao.Columns.WorkHourGenerated: 1}).
+			Where(s.AttendeeDao.Columns.MeetingId, int(meetingId)).Update(); err != nil {
+			return myerrors.DbError("更新参会人标记失败")
+		}
+
+		return nil
+	})
+}
+
+// UpdateById 更新会议
+func (s *PlatMeetingService) UpdateById(req *opsdevmodel.PlatMeetingUpdateReq) error {
+	var entity opsdevmodel.PlatMeeting
+	err := s.MeetingDao.FieldsEx(s.MeetingDao.Columns.DeletedTime).
+		WherePri(s.MeetingDao.Columns.Id, req.Id).Scan(&entity)
+	if err != nil {
+		return myerrors.DbError("查询会议失败")
+	}
+	if entity.Id <= 0 {
+		return myerrors.TipsError("会议不存在")
+	}
+
+	data := g.Map{}
+	if req.MeetingTitle != "" {
+		data[s.MeetingDao.Columns.MeetingTitle] = req.MeetingTitle
+	}
+	if req.MeetingContent != "" {
+		data[s.MeetingDao.Columns.MeetingContent] = req.MeetingContent
+	}
+	if req.MeetingDate != "" {
+		data[s.MeetingDao.Columns.MeetingDate] = req.MeetingDate
+	}
+	if req.Duration > 0 {
+		data[s.MeetingDao.Columns.Duration] = req.Duration
+	}
+	if req.OrganizerId > 0 {
+		data[s.MeetingDao.Columns.OrganizerId] = req.OrganizerId
+	}
+	if req.OrganizerName != "" {
+		data[s.MeetingDao.Columns.OrganizerName] = req.OrganizerName
+	}
+	if req.DeptId > 0 {
+		data[s.MeetingDao.Columns.DeptId] = req.DeptId
+	}
+	if req.Remark != "" {
+		data[s.MeetingDao.Columns.Remark] = req.Remark
+	}
+
+	if len(data) == 0 && len(req.UserIds) == 0 {
+		return nil
+	}
+
+	service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
+
+	return s.MeetingDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
+		if len(data) > 0 {
+			_, err = s.MeetingDao.TX(tx).FieldsEx(service.UpdateFieldEx...).
+				Data(data).WherePri(s.MeetingDao.Columns.Id, req.Id).Update()
+			if err != nil {
+				return myerrors.DbError("更新会议失败")
+			}
+		}
+
+		// 全量替换参会人员
+		if len(req.UserIds) > 0 {
+			if _, err := s.AttendeeDao.TX(tx).
+				Where(s.AttendeeDao.Columns.MeetingId, req.Id).Delete(); err != nil {
+				return myerrors.DbError("清除旧参会人员失败")
+			}
+			if err := s.batchInsertAttendees(tx, req.Id, req.UserIds, req.UserNames); err != nil {
+				return err
+			}
+		}
+		return nil
+	})
+}
+
+// DeleteByIds 删除会议(软删除会议及参会人员)
+func (s *PlatMeetingService) DeleteByIds(ids []int64) error {
+	if len(ids) == 0 {
+		return myerrors.TipsError("请选择需要删除的记录")
+	}
+
+	return s.MeetingDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
+		// 软删除参会人员
+		for _, id := range ids {
+			if _, err := s.AttendeeDao.TX(tx).
+				Where(s.AttendeeDao.Columns.MeetingId, id).Delete(); err != nil {
+				return myerrors.DbError("删除参会人员失败")
+			}
+		}
+		// 软删除会议
+		if _, err := s.MeetingDao.TX(tx).WhereIn(s.MeetingDao.Columns.Id, ids).Delete(); err != nil {
+			return myerrors.DbError("删除会议失败")
+		}
+		return nil
+	})
+}
+
+// AddAttendees 追加参会人员
+func (s *PlatMeetingService) AddAttendees(req *opsdevmodel.PlatMeetingAddAttendeeReq) error {
+	// 校验会议存在
+	var entity opsdevmodel.PlatMeeting
+	err := s.MeetingDao.FieldsEx(s.MeetingDao.Columns.DeletedTime).
+		WherePri(s.MeetingDao.Columns.Id, req.MeetingId).Scan(&entity)
+	if err != nil {
+		return myerrors.DbError("查询会议失败")
+	}
+	if entity.Id <= 0 {
+		return myerrors.TipsError("会议不存在")
+	}
+
+	return s.MeetingDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
+		// 查询已存在参会人,避免重复插入
+		existing, err := s.AttendeeDao.TX(tx).
+			Where(s.AttendeeDao.Columns.MeetingId, req.MeetingId).
+			WhereIn(s.AttendeeDao.Columns.UserId, req.UserIds).
+			Fields(s.AttendeeDao.Columns.UserId).All()
+		if err != nil {
+			return myerrors.DbError("查询已有参会人员失败")
+		}
+
+		existingIds := make(map[int]bool)
+		for _, row := range existing {
+			existingIds[row["user_id"].Int()] = true
+		}
+
+		for i, userId := range req.UserIds {
+			if existingIds[userId] {
+				continue
+			}
+			userName := ""
+			if i < len(req.UserNames) {
+				userName = req.UserNames[i]
+			}
+			attendeeData := g.Map{
+				s.AttendeeDao.Columns.MeetingId: req.MeetingId,
+				s.AttendeeDao.Columns.UserId:    userId,
+				s.AttendeeDao.Columns.UserName:  userName,
+			}
+			service.SetCreatedInfo(attendeeData, s.GetCxtUserId(), s.GetCxtUserName())
+			if _, err := s.AttendeeDao.TX(tx).Data(attendeeData).Insert(); err != nil {
+				return myerrors.DbError("新增参会人员失败")
+			}
+		}
+		return nil
+	})
+}
+
+// Complete 结束会议——为发起人和参会人员补生成工时
+func (s *PlatMeetingService) Complete(req *opsdevmodel.PlatMeetingCompleteReq) error {
+	var entity opsdevmodel.PlatMeeting
+	err := s.MeetingDao.FieldsEx(s.MeetingDao.Columns.DeletedTime).
+		WherePri(s.MeetingDao.Columns.Id, req.Id).Scan(&entity)
+	if err != nil {
+		return myerrors.DbError("查询会议失败")
+	}
+	if entity.Id <= 0 {
+		return myerrors.TipsError("会议不存在")
+	}
+	if entity.Duration <= 0 {
+		return myerrors.TipsError("会议时长未设置,无法生成工时")
+	}
+
+	return s.MeetingDao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
+		workDate := gtime.Now()
+		if entity.MeetingDate != nil {
+			workDate = entity.MeetingDate
+		}
+
+		// 检查发起人工时是否已存在
+		orgCount, err := s.WorkHourDao.TX(tx).FieldsEx(s.WorkHourDao.Columns.DeletedTime).
+			Where(s.WorkHourDao.Columns.MeetingId, req.Id).
+			Where(s.WorkHourDao.Columns.UserId, entity.OrganizerId).
+			Where(s.WorkHourDao.Columns.AttendeeId, 0).Count()
+		if err != nil {
+			return myerrors.DbError("查询发起人工时失败")
+		}
+		if orgCount == 0 {
+			orgWhData := g.Map{
+				s.WorkHourDao.Columns.MeetingId:  req.Id,
+				s.WorkHourDao.Columns.AttendeeId: 0,
+				s.WorkHourDao.Columns.UserId:     entity.OrganizerId,
+				s.WorkHourDao.Columns.UserName:   entity.OrganizerName,
+				s.WorkHourDao.Columns.WorkDate:   workDate,
+				s.WorkHourDao.Columns.WorkHour:   entity.Duration,
+				s.WorkHourDao.Columns.Remark:     entity.MeetingTitle,
+			}
+			service.SetCreatedInfo(orgWhData, s.GetCxtUserId(), s.GetCxtUserName())
+			if _, err := s.WorkHourDao.TX(tx).Data(orgWhData).Insert(); err != nil {
+				return myerrors.DbError("生成发起人工时失败")
+			}
+		}
+
+		// 查询未生成工时的参会人员
+		attendees, err := s.AttendeeDao.TX(tx).FieldsEx(s.AttendeeDao.Columns.DeletedTime).
+			Where(s.AttendeeDao.Columns.MeetingId, req.Id).
+			Where(s.AttendeeDao.Columns.WorkHourGenerated, 0).All()
+		if err != nil {
+			return myerrors.DbError("查询参会人员失败")
+		}
+
+		for _, row := range attendees {
+			whData := g.Map{
+				s.WorkHourDao.Columns.MeetingId:  req.Id,
+				s.WorkHourDao.Columns.AttendeeId: row.Id,
+				s.WorkHourDao.Columns.UserId:     row.UserId,
+				s.WorkHourDao.Columns.UserName:   row.UserName,
+				s.WorkHourDao.Columns.WorkDate:   workDate,
+				s.WorkHourDao.Columns.WorkHour:   entity.Duration,
+				s.WorkHourDao.Columns.Remark:     entity.MeetingTitle,
+			}
+			service.SetCreatedInfo(whData, s.GetCxtUserId(), s.GetCxtUserName())
+			if _, err := s.WorkHourDao.TX(tx).Data(whData).Insert(); err != nil {
+				return myerrors.DbError("生成参会人工时失败")
+			}
+		}
+
+		// 标记参会人工时已生成
+		if _, err := s.AttendeeDao.TX(tx).
+			Data(g.Map{s.AttendeeDao.Columns.WorkHourGenerated: 1}).
+			Where(s.AttendeeDao.Columns.MeetingId, req.Id).Update(); err != nil {
+			return myerrors.DbError("更新参会人标记失败")
+		}
+
+		return nil
+	})
+}
+
+// GetWorkHourList 获取会议工时记录列表
+func (s *PlatMeetingService) GetWorkHourList(meetingId int) ([]*opsdevmodel.PlatMeetingWorkHourRsp, error) {
+	var entities []*opsdevmodel.PlatMeetingWorkHour
+	err := s.WorkHourDao.FieldsEx(s.WorkHourDao.Columns.DeletedTime).
+		Where(s.WorkHourDao.Columns.MeetingId, meetingId).
+		Order(s.WorkHourDao.Columns.Id + " asc").Scan(&entities)
+	if err != nil {
+		return nil, myerrors.DbError("查询工时记录失败")
+	}
+
+	var list []*opsdevmodel.PlatMeetingWorkHourRsp
+	if err := gconv.Structs(entities, &list); err != nil {
+		return nil, myerrors.DbError("数据转换失败")
+	}
+	return list, nil
+}
+
+// getAttendeesByMeetingId 根据会议ID查询参会人员
+func (s *PlatMeetingService) getAttendeesByMeetingId(meetingId int) ([]*opsdevmodel.PlatMeetingAttendeeRsp, error) {
+	var entities []*opsdevmodel.PlatMeetingAttendee
+	err := s.AttendeeDao.FieldsEx(s.AttendeeDao.Columns.DeletedTime).
+		Where(s.AttendeeDao.Columns.MeetingId, meetingId).
+		Order(s.AttendeeDao.Columns.Id + " asc").Scan(&entities)
+	if err != nil {
+		return nil, myerrors.DbError("查询参会人员失败")
+	}
+
+	var list []*opsdevmodel.PlatMeetingAttendeeRsp
+	if err := gconv.Structs(entities, &list); err != nil {
+		return nil, myerrors.DbError("数据转换失败")
+	}
+	return list, nil
+}
+
+// batchInsertAttendees 批量新增参会人员(需在事务内调用)
+func (s *PlatMeetingService) batchInsertAttendees(
+	tx *gdb.TX, meetingId int, userIds []int, userNames []string,
+) error {
+	for i, userId := range userIds {
+		userName := ""
+		if i < len(userNames) {
+			userName = userNames[i]
+		}
+		data := g.Map{
+			s.AttendeeDao.Columns.MeetingId: meetingId,
+			s.AttendeeDao.Columns.UserId:    userId,
+			s.AttendeeDao.Columns.UserName:  userName,
+		}
+		service.SetCreatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
+		if _, err := s.AttendeeDao.TX(tx).Data(data).Insert(); err != nil {
+			return myerrors.DbError("新增参会人员失败")
+		}
+	}
+	return nil
+}

+ 19 - 9
opms_parent/app/service/opsdev/work_hour_stat.go

@@ -19,6 +19,7 @@ type rawHourRow struct {
 	WorkDate    string
 	OpHour      float64
 	RdHour      float64
+	MtHour      float64
 }
 
 // WorkHourStatService 工时统计服务
@@ -66,23 +67,29 @@ func (s *WorkHourStatService) GetStat(req *opsdevmodel.WorkHourStatReq) (*opsdev
 
 	unionSQL := fmt.Sprintf(`
 		SELECT ops_user_id, ops_user_name, DATE(work_date) AS work_date,
-		       SUM(work_hour) AS op_hour, 0.0 AS rd_hour
+		       SUM(work_hour) AS op_hour, 0.0 AS rd_hour, 0.0 AS mt_hour
 		FROM ops_operation_work_hour
 		WHERE work_date >= ? AND work_date <= ? AND deleted_time IS NULL
 		GROUP BY ops_user_id, ops_user_name, DATE(work_date)
 		UNION ALL
 		SELECT ops_user_id, ops_user_name, DATE(actual_work_date) AS work_date,
-		       0.0 AS op_hour, SUM(actual_work_hour) AS rd_hour
+		       0.0 AS op_hour, SUM(actual_work_hour) AS rd_hour, 0.0 AS mt_hour
 		FROM ops_event_task_work_hour
 		WHERE actual_work_date >= ? AND actual_work_date <= ? AND deleted_time IS NULL
 		GROUP BY ops_user_id, ops_user_name, DATE(actual_work_date)
+		UNION ALL
+		SELECT user_id AS ops_user_id, user_name AS ops_user_name, DATE(work_date) AS work_date,
+		       0.0 AS op_hour, 0.0 AS rd_hour, SUM(work_hour) AS mt_hour
+		FROM plat_meeting_work_hour
+		WHERE work_date >= ? AND work_date <= ? AND deleted_time IS NULL
+		GROUP BY user_id, user_name, DATE(work_date)
 	`)
 
 	startDT := req.StartDate + " 00:00:00"
 	endDT := req.EndDate + " 23:59:59"
 
 	var rawRows []rawHourRow
-	if err := g.DB(s.Tenant).GetScan(&rawRows, unionSQL, startDT, endDT, startDT, endDT); err != nil {
+	if err := g.DB(s.Tenant).GetScan(&rawRows, unionSQL, startDT, endDT, startDT, endDT, startDT, endDT); err != nil {
 		return nil, myerrors.TipsError("查询工时数据失败: " + err.Error())
 	}
 
@@ -109,6 +116,7 @@ func (s *WorkHourStatService) GetStat(req *opsdevmodel.WorkHourStatReq) (*opsdev
 		dd := p.DailyHours[row.WorkDate]
 		dd.OpHour += row.OpHour
 		dd.RdHour += row.RdHour
+		dd.MtHour += row.MtHour
 		p.DailyHours[row.WorkDate] = dd
 	}
 
@@ -129,21 +137,23 @@ func (s *WorkHourStatService) GetStat(req *opsdevmodel.WorkHourStatReq) (*opsdev
 		}
 
 		dailyHours := make(map[string]opsdevmodel.DayData)
-		var totalOp, totalRd float64
+		var totalOp, totalRd, totalMt float64
 
 		for _, dateStr := range allDates {
 			dd := p.DailyHours[dateStr]
 			dailyHours[dateStr] = dd
 			totalOp += dd.OpHour
 			totalRd += dd.RdHour
+			totalMt += dd.MtHour
 		}
 
 		persons = append(persons, opsdevmodel.PersonRow{
-			UserId:      p.OpsUserId,
-			UserName:    p.OpsUserName,
-			DailyHours:  dailyHours,
-			TotalOpHour: totalOp,
-			TotalRdHour: totalRd,
+			UserId:       p.OpsUserId,
+			UserName:     p.OpsUserName,
+			DailyHours:   dailyHours,
+			TotalOpHour:  totalOp,
+			TotalRdHour:  totalRd,
+			TotalMtHour:  totalMt,
 		})
 	}
 

+ 2 - 2
opms_parent/config/config.toml

@@ -1,8 +1,8 @@
 # 应用系统设置
 [setting]
     logpath = "/tmp/log/admin"
-    bind-addr = "192.168.0.188:8002"
-    bind-mutipart-addr = "192.168.0.188:9999"
+    bind-addr = "192.168.0.123:8002"
+    bind-mutipart-addr = "192.168.0.123:9999"
     need-advertise-addr = false
     srv-name = "dashoo.opms.parent-0.0.2-cj"
     env = "dev"

+ 2 - 0
opms_parent/main.go

@@ -92,6 +92,8 @@ func main() {
 	s.RegisterName("OpsEventTask", new(opsdev.OpsEventTaskHandler), "")
 	// 注册工时统计处理器
 	s.RegisterName("WorkHourStat", new(opsdev.WorkHourStatHandler), "")
+	// 注册会议处理器
+	s.RegisterName("PlatMeeting", new(opsdev.PlatMeetingHandler), "")
 
 	// 首页
 	s.RegisterName("Home", new(home.HomeHandler), "")

BIN
opms_parent/oms_parent_v2


+ 76 - 0
opms_parent/schema/migration_plat_meeting.sql

@@ -0,0 +1,76 @@
+-- ============================================================================
+-- Migration: 部门会议管理功能 - 数据库变更
+-- Description: 部门会议登记、参会人员管理、会议工时生成
+-- 1. 新建 plat_meeting 会议主表
+-- 2. 新建 plat_meeting_attendee 参会人员表
+-- 3. 新建 plat_meeting_work_hour 会议工时记录表
+-- ============================================================================
+
+-- 1. 会议主表
+CREATE TABLE `plat_meeting` (
+    `id`                BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
+    `meeting_title`     VARCHAR(256)    NOT NULL COMMENT '会议标题',
+    `meeting_content`   TEXT            NULL     COMMENT '会议内容/议题',
+    `meeting_date`      DATETIME        NOT NULL COMMENT '会议开始时间',
+    `meeting_date_end`  DATETIME        NULL     COMMENT '会议结束时间',
+    `duration`          DECIMAL(8,2)    NULL     COMMENT '会议时长(小时)',
+    `organizer_id`      INT             NOT NULL COMMENT '会议负责人ID',
+    `organizer_name`    VARCHAR(64)     NOT NULL COMMENT '会议负责人姓名',
+    `status`            VARCHAR(10)     NOT NULL DEFAULT '10' COMMENT '状态: 10=待开始, 20=进行中, 30=已结束',
+    `dept_id`           INT             NULL     COMMENT '所属部门ID',
+    `remark`            TEXT            NULL     COMMENT '备注',
+    `created_by`        INT             NOT NULL COMMENT '创建者',
+    `created_name`      VARCHAR(64)     NOT NULL COMMENT '创建人',
+    `created_time`      DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `updated_by`        INT             NULL     COMMENT '更新者',
+    `updated_name`      VARCHAR(64)     NULL     COMMENT '更新人',
+    `updated_time`      DATETIME        NULL     COMMENT '更新时间',
+    `deleted_time`      DATETIME        NULL     COMMENT '删除时间',
+    INDEX `idx_organizer_id` (`organizer_id`),
+    INDEX `idx_dept_id` (`dept_id`),
+    INDEX `idx_status` (`status`),
+    INDEX `idx_meeting_date` (`meeting_date`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='部门会议记录';
+
+-- 2. 参会人员表
+CREATE TABLE `plat_meeting_attendee` (
+    `id`                  BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
+    `meeting_id`          INT             NOT NULL COMMENT '会议ID',
+    `user_id`             INT             NOT NULL COMMENT '参会人员ID',
+    `user_name`           VARCHAR(64)     NOT NULL COMMENT '参会人员姓名',
+    `source_type`         VARCHAR(10)     NOT NULL DEFAULT '10' COMMENT '添加来源: 10=手动选择, 20=按部门, 30=按角色',
+    `source_id`           INT             NULL     COMMENT '来源ID(部门ID/角色ID)',
+    `work_hour_generated` TINYINT         NOT NULL DEFAULT 0 COMMENT '是否已生成工时: 0=否, 1=是',
+    `created_by`          INT             NOT NULL COMMENT '创建者',
+    `created_name`        VARCHAR(64)     NOT NULL COMMENT '创建人',
+    `created_time`        DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `updated_by`          INT             NULL     COMMENT '更新者',
+    `updated_name`        VARCHAR(64)     NULL     COMMENT '更新人',
+    `updated_time`        DATETIME        NULL     COMMENT '更新时间',
+    `deleted_time`        DATETIME        NULL     COMMENT '删除时间',
+    INDEX `idx_meeting_id` (`meeting_id`),
+    INDEX `idx_user_id` (`user_id`),
+    UNIQUE KEY `uk_meeting_user` (`meeting_id`, `user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='会议参会人员';
+
+-- 3. 会议工时记录表
+CREATE TABLE `plat_meeting_work_hour` (
+    `id`             BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
+    `meeting_id`     INT             NOT NULL COMMENT '关联会议ID',
+    `attendee_id`    INT             NOT NULL COMMENT '参会记录ID(plat_meeting_attendee.id)',
+    `user_id`        INT             NOT NULL COMMENT '人员ID',
+    `user_name`      VARCHAR(64)     NOT NULL COMMENT '人员姓名',
+    `work_date`      DATETIME        NOT NULL COMMENT '工时日期',
+    `work_hour`      DECIMAL(8,2)    NOT NULL COMMENT '工时(小时)',
+    `remark`         TEXT            NULL     COMMENT '工作内容备注',
+    `created_by`     INT             NOT NULL COMMENT '创建者',
+    `created_name`   VARCHAR(64)     NOT NULL COMMENT '创建人',
+    `created_time`   DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `updated_by`     INT             NULL     COMMENT '更新者',
+    `updated_name`   VARCHAR(64)     NULL     COMMENT '更新人',
+    `updated_time`   DATETIME        NULL     COMMENT '更新时间',
+    `deleted_time`   DATETIME        NULL     COMMENT '删除时间',
+    INDEX `idx_meeting_id` (`meeting_id`),
+    INDEX `idx_user_id` (`user_id`),
+    INDEX `idx_work_date` (`work_date`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='会议工时记录';