feat(csmanualentry): 手动录单支持收号出号分组

- 录单记录新增组别(收号/出号),唯一约束放宽为编号+组别,同一编号两组各可登记一次
- 操作人选项支持组别标记,录单选中后自动带出组别
- 列表新增组别列与筛选,今日统计按收号/出号分列
This commit is contained in:
yml2213
2026-11-06 14:02:28 +08:00
parent 9d55b4510c
commit e027f88fbb
11 changed files with 431 additions and 74 deletions
@@ -34,9 +34,10 @@ func (r *Repository) FindAdminSnapshot(ctx context.Context, adminID uint64) (nam
}
// Create 写入一条录单记录。
func (r *Repository) Create(ctx context.Context, adminID uint64, adminName, adminUsername, entryNo, platform, operator, remark string) (*EntryDTO, error) {
func (r *Repository) Create(ctx context.Context, adminID uint64, adminName, adminUsername, entryNo, entryGroup, platform, operator, remark string) (*EntryDTO, error) {
row := model.CSManualEntry{
EntryNo: entryNo,
EntryGroup: entryGroup,
Platform: platform,
Operator: operator,
AdminID: adminID,
@@ -67,15 +68,16 @@ func (r *Repository) FindByID(ctx context.Context, id uint64) (*EntryDTO, error)
return &dto, nil
}
// Update 修正录单的编号、平台、操作人与备注。编号唯一冲突返回 ErrEntryNoExists。
func (r *Repository) Update(ctx context.Context, id uint64, entryNo, platform, operator, remark string) (*EntryDTO, error) {
// Update 修正录单的编号、组别、平台、操作人与备注。同组内编号唯一冲突返回 ErrEntryNoExists。
func (r *Repository) Update(ctx context.Context, id uint64, entryNo, entryGroup, platform, operator, remark string) (*EntryDTO, error) {
result := r.db.WithContext(ctx).Model(&model.CSManualEntry{}).
Where("id = ?", id).
Updates(map[string]any{
"entry_no": entryNo,
"platform": platform,
"operator": operator,
"remark": remark,
"entry_no": entryNo,
"entry_group": entryGroup,
"platform": platform,
"operator": operator,
"remark": remark,
})
if result.Error != nil {
if isDuplicateEntry(result.Error) {
@@ -116,6 +118,10 @@ func (r *Repository) List(ctx context.Context, query ListQuery) (*PaginatedResul
db = db.Where("entry_no = ?", query.EntryNo)
countDB = countDB.Where("entry_no = ?", query.EntryNo)
}
if query.EntryGroup != "" {
db = db.Where("entry_group = ?", query.EntryGroup)
countDB = countDB.Where("entry_group = ?", query.EntryGroup)
}
if query.AdminID > 0 {
db = db.Where("admin_id = ?", query.AdminID)
countDB = countDB.Where("admin_id = ?", query.AdminID)
@@ -168,13 +174,17 @@ func (r *Repository) TodayAgentStats(ctx context.Context, day time.Time) ([]Agen
end := start.AddDate(0, 0, 1)
type row struct {
AdminID uint64
AdminName string
Count int64
AdminID uint64
AdminName string
Count int64
CollectCount int64
SellCount int64
}
rows := make([]row, 0)
if err := r.db.WithContext(ctx).Table("cs_manual_entries").
Select("admin_id, MAX(admin_name) AS admin_name, COUNT(*) AS count").
Select(`admin_id, MAX(admin_name) AS admin_name, COUNT(*) AS count,
SUM(CASE WHEN entry_group = 'collect' THEN 1 ELSE 0 END) AS collect_count,
SUM(CASE WHEN entry_group = 'sell' THEN 1 ELSE 0 END) AS sell_count`).
Where("created_at >= ? AND created_at < ?", start, end).
Group("admin_id").
Scan(&rows).Error; err != nil {
@@ -190,10 +200,12 @@ func (r *Repository) TodayAgentStats(ctx context.Context, day time.Time) ([]Agen
items := make([]AgentDayStatsDTO, 0, len(rows))
for _, item := range rows {
items = append(items, AgentDayStatsDTO{
Date: date,
AdminID: item.AdminID,
AdminName: item.AdminName,
Count: item.Count,
Date: date,
AdminID: item.AdminID,
AdminName: item.AdminName,
Count: item.Count,
CollectCount: item.CollectCount,
SellCount: item.SellCount,
})
}
return items, nil
@@ -232,11 +244,12 @@ func (r *Repository) ListAllOptions(ctx context.Context) (*OptionListDTO, error)
}
// CreateOption 新增一个平台 / 操作人选项,重名返回 ErrOptionExists。
func (r *Repository) CreateOption(ctx context.Context, optionType, name string) (*OptionDTO, error) {
// 操作人选项可通过 groupTag 标记所属组别(collect/sell,空表示未分组)。
func (r *Repository) CreateOption(ctx context.Context, optionType, name, groupTag string) (*OptionDTO, error) {
if !IsValidOptionType(optionType) {
return nil, ErrOptionTypeInvalid
}
row := model.CSManualEntryOption{Type: optionType, Name: name}
row := model.CSManualEntryOption{Type: optionType, Name: name, GroupTag: groupTag}
if err := r.db.WithContext(ctx).Create(&row).Error; err != nil {
if isDuplicateEntry(err) {
return nil, ErrOptionExists
@@ -247,8 +260,8 @@ func (r *Repository) CreateOption(ctx context.Context, optionType, name string)
return &dto, nil
}
// UpdateOption 重命名一个选项,重名返回 ErrOptionExists。
func (r *Repository) UpdateOption(ctx context.Context, id uint64, name string) (*OptionDTO, error) {
// UpdateOption 重命名一个选项并可更新组别标记,重名返回 ErrOptionExists。
func (r *Repository) UpdateOption(ctx context.Context, id uint64, name, groupTag string) (*OptionDTO, error) {
var row model.CSManualEntryOption
if err := r.db.WithContext(ctx).First(&row, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -258,7 +271,10 @@ func (r *Repository) UpdateOption(ctx context.Context, id uint64, name string) (
}
result := r.db.WithContext(ctx).Model(&model.CSManualEntryOption{}).
Where("id = ?", id).
Update("name", name)
Updates(map[string]any{
"name": name,
"group_tag": groupTag,
})
if result.Error != nil {
if isDuplicateEntry(result.Error) {
return nil, ErrOptionExists
@@ -266,6 +282,7 @@ func (r *Repository) UpdateOption(ctx context.Context, id uint64, name string) (
return nil, result.Error
}
row.Name = name
row.GroupTag = groupTag
dto := toOptionDTO(row)
return &dto, nil
}
@@ -306,6 +323,7 @@ func toDTO(row model.CSManualEntry) EntryDTO {
return EntryDTO{
ID: row.ID,
EntryNo: row.EntryNo,
EntryGroup: row.EntryGroup,
Platform: row.Platform,
Operator: row.Operator,
AdminID: row.AdminID,
@@ -321,6 +339,7 @@ func toOptionDTO(row model.CSManualEntryOption) OptionDTO {
ID: row.ID,
Type: row.Type,
Name: row.Name,
GroupTag: row.GroupTag,
SortOrder: row.SortOrder,
CreatedAt: row.CreatedAt,
}