feat(业务用户组): AUG26-003 业务用户组与店铺负责人分组导入
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Failing after 1h43m42s

- 迁移 000221:新增 tb_business_user_group、tb_business_user_group_member、tb_shop_business_owner_import_task,成员一账号一行由部分唯一索引保证,店铺所属组按当前负责人实时推导,不回填历史分组。
- 用户组 CRUD、成员改组/清空归属、店铺批量交接(原子失败不部分写入)。
- 店铺负责人 CSV 导入任务:逐行独立事务、逐行明细、任务级与行级失败分离。
- 读侧推导与筛选:未分组、业务线、停用组可筛出并带停用标记。
- 补齐操作审计动作与资源、openapi 清单、发布门禁巡检表清单。
- 归档 add-shop-salesperson-groups 变更并同步 openspec/specs/business-user-group,补齐 AUG26-003 验证证据链。
This commit is contained in:
2026-09-14 16:51:44 +08:00
parent 957a235585
commit c7f9e005af
56 changed files with 4166 additions and 344 deletions

View File

@@ -482,6 +482,20 @@ const (
AuditActionIntegrationAttemptStarted = "integration.attempt_started"
// AuditActionIntegrationInboundReceived 表示入站回调处理前已记录的审计事实。
AuditActionIntegrationInboundReceived = "integration.inbound_received"
// AuditActionBusinessUserGroupCreated 表示创建业务用户组。
AuditActionBusinessUserGroupCreated = "business_user_group.create"
// AuditActionBusinessUserGroupUpdated 表示更新业务用户组名称、业务线、排序或备注。
AuditActionBusinessUserGroupUpdated = "business_user_group.update"
// AuditActionBusinessUserGroupEnabled 表示启用业务用户组。
AuditActionBusinessUserGroupEnabled = "business_user_group.enable"
// AuditActionBusinessUserGroupDisabled 表示停用业务用户组。
AuditActionBusinessUserGroupDisabled = "business_user_group.disable"
// AuditActionBusinessUserGroupDeleted 表示删除无成员业务用户组。
AuditActionBusinessUserGroupDeleted = "business_user_group.delete"
// AuditActionBusinessUserGroupMembersUpdated 表示批量设置或清空平台用户的业务用户组归属。
AuditActionBusinessUserGroupMembersUpdated = "business_user_group.update_members"
// AuditActionShopBusinessOwnerBatchUpdated 表示勾选店铺批量设置或清空平台业务员归属。
AuditActionShopBusinessOwnerBatchUpdated = "shop.batch_update_business_owner"
// AuditActionIotCardImportTaskCreated 表示创建 IoT 卡导入任务。
AuditActionIotCardImportTaskCreated = "iot_card_import_task.create"
// AuditActionIotCardImportTaskCompleted 表示 IoT 卡导入任务完成。
@@ -494,6 +508,12 @@ const (
AuditActionAssetPackageBatchOrderTaskCreated = "asset_package_batch_order_task.create"
// AuditActionAssetPackageBatchOrderTaskCompleted 表示资产套餐批量订购任务完成。
AuditActionAssetPackageBatchOrderTaskCompleted = "asset_package_batch_order_task.complete"
// AuditActionShopBusinessOwnerImportTaskCreated 表示创建店铺负责人 CSV 导入任务。
AuditActionShopBusinessOwnerImportTaskCreated = "shop_business_owner_import_task.create"
// AuditActionShopBusinessOwnerImportTaskCompleted 表示店铺负责人 CSV 导入任务完成。
AuditActionShopBusinessOwnerImportTaskCompleted = "shop_business_owner_import_task.complete"
// AuditActionShopBusinessOwnerImported 表示导入单行实际变更了店铺负责人归属。
AuditActionShopBusinessOwnerImported = "shop.import_business_owner"
// AuditActionOrderPackageInvalidateTaskCreated 表示创建订单套餐批量失效任务。
AuditActionOrderPackageInvalidateTaskCreated = "order_package_invalidate_task.create"
// AuditActionOrderPackageInvalidateTaskCompleted 表示订单套餐批量失效任务完成。
@@ -637,6 +657,12 @@ const (
AuditResourceDeviceImportTask = "device_import_task"
// AuditResourceAssetPackageBatchOrderTask 表示资产套餐批量订购任务资源。
AuditResourceAssetPackageBatchOrderTask = "asset_package_batch_order_task"
// AuditResourceShopBusinessOwnerImportTask 表示店铺负责人 CSV 导入任务资源。
AuditResourceShopBusinessOwnerImportTask = "shop_business_owner_import_task"
// AuditResourceBusinessUserGroup 表示业务用户组资源。
AuditResourceBusinessUserGroup = "business_user_group"
// AuditResourceShopBusinessOwnerBatch 表示店铺负责人批量交接批次根资源。
AuditResourceShopBusinessOwnerBatch = "shop_business_owner_batch"
// AuditResourceOrderPackageInvalidateTask 表示订单套餐批量失效任务资源。
AuditResourceOrderPackageInvalidateTask = "order_package_invalidate_task"
// AuditResourceExportTask 表示业务导出任务资源。
@@ -779,6 +805,10 @@ const (
AuditResourceRoleApprovalOutbox = "approval_outbox"
// AuditResourceRoleBatchTask 表示批量根事件的任务资源。
AuditResourceRoleBatchTask = "batch_task"
// AuditResourceRoleBusinessUserGroupTarget 表示业务用户组维护的主要目标。
AuditResourceRoleBusinessUserGroupTarget = "business_user_group_target"
// AuditResourceRoleShopBusinessOwnerBatch 表示店铺负责人批量交接的批次根资源。
AuditResourceRoleShopBusinessOwnerBatch = "shop_business_owner_batch"
// AuditResourceRoleRetentionMonth 表示留存清理目标自然月。
AuditResourceRoleRetentionMonth = "retention_month"
// AuditResourceRoleNotificationTarget 表示本次写操作的通知资源。

View File

@@ -0,0 +1,51 @@
package constants
// 业务用户组是平台用户的业务分类,只用于店铺负责人推导、筛选与批量维护,
// 不进入登录、角色权限或数据范围判定。
// 业务用户组所属业务线取值,来源于 AUG26-003 的三枚举,允许为空表示未分类。
const (
// BusinessLineStandard 表示标品业务线。
BusinessLineStandard = "standard"
// BusinessLineSmart 表示智能产品业务线。
BusinessLineSmart = "smart"
// BusinessLineOther 表示其他业务线。
BusinessLineOther = "other"
)
// GetBusinessLineName 返回业务线单值的中文名称,空值表示未设置业务线。
func GetBusinessLineName(businessLine string) string {
switch businessLine {
case BusinessLineStandard:
return "标品"
case BusinessLineSmart:
return "智能产品"
case BusinessLineOther:
return "其他"
default:
return ""
}
}
// IsValidBusinessLine 判断业务线单值是否合法;允许为空表示未设置业务线。
func IsValidBusinessLine(businessLine string) bool {
switch businessLine {
case "", BusinessLineStandard, BusinessLineSmart, BusinessLineOther:
return true
default:
return false
}
}
// 业务用户组维护入口的角色约束与统一失败文案。
const (
// PlatformManagementForbiddenMessage 表示非超级管理员/平台账号访问平台维护入口的统一失败文案。
PlatformManagementForbiddenMessage = "无权限操作该资源或资源不存在"
)
// IsAvailablePlatformBusinessOwner 判定账号是否为当前有效的平台业务员。
// 语义为「平台用户 + 启用 + 未软删」,供本 Change 的成员维护、批量交接与导入共用;
// 既有内联判定按 As-Is 保留,不回改。需要数据库条件的调用方自行在查询中表达同一条件。
func IsAvailablePlatformBusinessOwner(userType, status int, deleted bool) bool {
return userType == UserTypePlatform && status == StatusEnabled && !deleted
}

View File

@@ -75,6 +75,8 @@ const (
// 订单套餐失效任务类型
TaskTypeOrderPackageInvalidate = "order:package:invalidate" // 批量失效订单套餐
TaskTypeAssetPackageBatchOrder = "asset:package:batch_order" // 资产套餐批量订购
// 店铺负责人 CSV 导入任务类型,独立成队列以隔离逐行执行的长时任务。
TaskTypeShopBusinessOwnerImport = "shop_business_owner:import"
// 订单超时任务类型
TaskTypeOrderExpire = "order:expire" // 订单超时自动取消
@@ -209,34 +211,35 @@ const (
QueueDefault = "default" // 旧版默认队列(兼容历史任务)
QueueLow = "low" // 旧版低优先级队列(兼容历史任务)
QueueEmailSend = TaskTypeEmailSend // 发送邮件任务队列
QueueCommission = TaskTypeCommission // 分佣计算任务队列
QueueIotCardImport = TaskTypeIotCardImport // IoT 卡导入任务队列
QueueDeviceImport = TaskTypeDeviceImport // 设备导入任务队列
QueueExportDispatch = TaskTypeExportDispatch // 导出分发任务队列
QueueExportShard = TaskTypeExportShard // 导出分片任务队列
QueueExportFinalize = TaskTypeExportFinalize // 导出收尾任务队列
QueueCommissionStatsUpdate = TaskTypeCommissionStatsUpdate // 佣金统计更新任务队列
QueueCommissionStatsSync = TaskTypeCommissionStatsSync // 佣金统计同步任务队列
QueueCommissionStatsArchive = TaskTypeCommissionStatsArchive // 佣金统计归档任务队列
QueuePollingRealname = TaskTypePollingRealname // 实名轮询任务队列
QueuePollingCarddata = TaskTypePollingCarddata // 流量轮询任务队列
QueuePollingPackage = TaskTypePollingPackage // 套餐轮询任务队列
QueuePollingProtect = TaskTypePollingProtect // 保护期轮询任务队列
QueuePollingCardStatus = TaskTypePollingCardStatus // 卡状态轮询任务队列
QueuePackageFirstActivation = TaskTypePackageFirstActivation // 首次实名激活任务队列
QueuePackageQueueActivation = TaskTypePackageQueueActivation // 主套餐排队激活任务队列
QueuePackageDataReset = TaskTypePackageDataReset // 套餐流量重置任务队列
QueueOrderPackageInvalidate = TaskTypeOrderPackageInvalidate // 批量失效订单套餐任务队列
QueueAssetPackageBatchOrder = TaskTypeAssetPackageBatchOrder // 资产套餐批量订购任务队列
QueueOrderExpire = TaskTypeOrderExpire // 订单超时取消任务队列
QueueAutoPurchase = TaskTypeAutoPurchaseAfterRecharge // 充值后自动购包任务队列
QueueAlertCheck = TaskTypeAlertCheck // 告警检查任务队列
QueueDataCleanup = TaskTypeDataCleanup // 数据清理任务队列
QueueDailyTrafficFlush = TaskTypeDailyTrafficFlush // 每日流量落盘任务队列
QueueOutboxDeliver = TaskTypeOutboxDeliver // 公共 Outbox 投递队列
QueueCardObservationSeries = TaskTypeCardObservationSeries // 卡观测事件序列队列
QueueWeComApproval = "wecom:approval" // 企业微信审批同步与恢复队列
QueueEmailSend = TaskTypeEmailSend // 发送邮件任务队列
QueueCommission = TaskTypeCommission // 分佣计算任务队列
QueueIotCardImport = TaskTypeIotCardImport // IoT 卡导入任务队列
QueueDeviceImport = TaskTypeDeviceImport // 设备导入任务队列
QueueExportDispatch = TaskTypeExportDispatch // 导出分发任务队列
QueueExportShard = TaskTypeExportShard // 导出分片任务队列
QueueExportFinalize = TaskTypeExportFinalize // 导出收尾任务队列
QueueCommissionStatsUpdate = TaskTypeCommissionStatsUpdate // 佣金统计更新任务队列
QueueCommissionStatsSync = TaskTypeCommissionStatsSync // 佣金统计同步任务队列
QueueCommissionStatsArchive = TaskTypeCommissionStatsArchive // 佣金统计归档任务队列
QueuePollingRealname = TaskTypePollingRealname // 实名轮询任务队列
QueuePollingCarddata = TaskTypePollingCarddata // 流量轮询任务队列
QueuePollingPackage = TaskTypePollingPackage // 套餐轮询任务队列
QueuePollingProtect = TaskTypePollingProtect // 保护期轮询任务队列
QueuePollingCardStatus = TaskTypePollingCardStatus // 卡状态轮询任务队列
QueuePackageFirstActivation = TaskTypePackageFirstActivation // 首次实名激活任务队列
QueuePackageQueueActivation = TaskTypePackageQueueActivation // 主套餐排队激活任务队列
QueuePackageDataReset = TaskTypePackageDataReset // 套餐流量重置任务队列
QueueOrderPackageInvalidate = TaskTypeOrderPackageInvalidate // 批量失效订单套餐任务队列
QueueAssetPackageBatchOrder = TaskTypeAssetPackageBatchOrder // 资产套餐批量订购任务队列
QueueShopBusinessOwnerImport = TaskTypeShopBusinessOwnerImport // 店铺负责人 CSV 导入任务队列
QueueOrderExpire = TaskTypeOrderExpire // 订单超时取消任务队列
QueueAutoPurchase = TaskTypeAutoPurchaseAfterRecharge // 充值后自动购包任务队列
QueueAlertCheck = TaskTypeAlertCheck // 告警检查任务队列
QueueDataCleanup = TaskTypeDataCleanup // 数据清理任务队列
QueueDailyTrafficFlush = TaskTypeDailyTrafficFlush // 每日流量落盘任务队列
QueueOutboxDeliver = TaskTypeOutboxDeliver // 公共 Outbox 投递队列
QueueCardObservationSeries = TaskTypeCardObservationSeries // 卡观测事件序列队列
QueueWeComApproval = "wecom:approval" // 企业微信审批同步与恢复队列
DefaultRetryMax = 5 // 默认任务最大重试次数
DefaultTimeout = 10 * time.Minute // 默认任务超时时间
@@ -286,6 +289,8 @@ func QueueForTaskType(taskType string) string {
return QueueOrderPackageInvalidate
case TaskTypeAssetPackageBatchOrder:
return QueueAssetPackageBatchOrder
case TaskTypeShopBusinessOwnerImport:
return QueueShopBusinessOwnerImport
case TaskTypeOrderExpire:
return QueueOrderExpire
case TaskTypeAutoPurchaseAfterRecharge:
@@ -318,37 +323,38 @@ func QueueForTaskType(taskType string) string {
// DefaultTaskQueueWeights 返回 Worker 默认监听的全部任务队列及其权重。
func DefaultTaskQueueWeights() map[string]int {
return map[string]int{
QueueCritical: 6,
QueueIotCardImport: 6,
QueueDeviceImport: 6,
QueueAutoPurchase: 5,
QueuePackageFirstActivation: 5,
QueuePackageQueueActivation: 5,
QueueOrderPackageInvalidate: 5,
QueueAssetPackageBatchOrder: 5,
QueueOrderExpire: 4,
QueueEmailSend: 4,
QueueExportDispatch: 4,
QueueExportFinalize: 4,
QueueCommission: 3,
QueueAlertCheck: 3,
QueueExportShard: 2,
QueueCommissionStatsUpdate: 2,
QueueCommissionStatsSync: 2,
QueueCommissionStatsArchive: 2,
QueuePollingRealname: 1,
QueuePollingCarddata: 1,
QueuePollingPackage: 1,
QueuePollingProtect: 1,
QueuePollingCardStatus: 1,
QueuePackageDataReset: 1,
QueueDataCleanup: 1,
QueueDailyTrafficFlush: 1,
QueueOutboxDeliver: 4,
QueueCardObservationSeries: 2,
QueueWeComApproval: 2,
QueueDefault: 1,
QueueLow: 1,
QueueCritical: 6,
QueueIotCardImport: 6,
QueueDeviceImport: 6,
QueueAutoPurchase: 5,
QueuePackageFirstActivation: 5,
QueuePackageQueueActivation: 5,
QueueOrderPackageInvalidate: 5,
QueueAssetPackageBatchOrder: 5,
QueueShopBusinessOwnerImport: 4,
QueueOrderExpire: 4,
QueueEmailSend: 4,
QueueExportDispatch: 4,
QueueExportFinalize: 4,
QueueCommission: 3,
QueueAlertCheck: 3,
QueueExportShard: 2,
QueueCommissionStatsUpdate: 2,
QueueCommissionStatsSync: 2,
QueueCommissionStatsArchive: 2,
QueuePollingRealname: 1,
QueuePollingCarddata: 1,
QueuePollingPackage: 1,
QueuePollingProtect: 1,
QueuePollingCardStatus: 1,
QueuePackageDataReset: 1,
QueueDataCleanup: 1,
QueueDailyTrafficFlush: 1,
QueueOutboxDeliver: 4,
QueueCardObservationSeries: 2,
QueueWeComApproval: 2,
QueueDefault: 1,
QueueLow: 1,
}
}

View File

@@ -0,0 +1,73 @@
package constants
import "time"
// 店铺负责人 CSV 导入的场景常量。
const (
// StoragePurposeShopBusinessOwnerImport 表示店铺负责人 CSV 导入上传用途。
StoragePurposeShopBusinessOwnerImport = "shop_import"
// ShopBusinessOwnerImportStoragePrefix 表示店铺负责人导入文件的对象存储目录。
ShopBusinessOwnerImportStoragePrefix = "shop-imports"
// ShopBusinessOwnerImportTaskTimeout 表示单个店铺负责人导入任务的最长执行时间。
ShopBusinessOwnerImportTaskTimeout = 2 * time.Hour
// ShopBusinessOwnerImportProgressBatchSize 表示导入按批更新进度计数的批大小。
ShopBusinessOwnerImportProgressBatchSize = 100
// ShopBusinessOwnerImportItemStatusSuccess 表示导入单行已成功变更店铺负责人。
ShopBusinessOwnerImportItemStatusSuccess = 3
// ShopBusinessOwnerImportItemStatusFailed 表示导入单行校验或执行失败并保留原值。
ShopBusinessOwnerImportItemStatusFailed = 4
)
// 店铺负责人 CSV 导入的操作类型取值,行内互相排斥:换绑必须填业务员登录账号,清空不得填写。
const (
// ShopBusinessOwnerImportOperationRebind 表示把店铺负责人换绑到指定平台业务员。
ShopBusinessOwnerImportOperationRebind = "换绑"
// ShopBusinessOwnerImportOperationClear 表示清空店铺负责人。
ShopBusinessOwnerImportOperationClear = "清空"
)
// ShopBusinessOwnerImportColumns 表示导入文件首行必须完全一致的固定列序。
var ShopBusinessOwnerImportColumns = []string{"店铺编码", "操作类型", "业务员登录账号", "备注"}
// 店铺负责人 CSV 导入的任务级失败原因,与行级失败原因分开记录。
const (
// ShopBusinessOwnerImportErrorFileFormat 表示文件格式或表头与固定列序不符。
ShopBusinessOwnerImportErrorFileFormat = "文件格式或表头与固定列序不符"
// ShopBusinessOwnerImportErrorEncoding 表示文件既非合法 UTF-8 也无法按 GBK 解码。
ShopBusinessOwnerImportErrorEncoding = "文件编码无法按 UTF-8 或 GBK 解码"
// ShopBusinessOwnerImportErrorNoDataRow 表示文件没有数据行。
ShopBusinessOwnerImportErrorNoDataRow = "文件没有数据行"
)
// 店铺负责人 CSV 导入的行级失败原因,取值集合固定,不包含数据范围无权。
const (
// ShopBusinessOwnerImportRowErrorShopMissing 表示店铺编码不存在或已删除。
ShopBusinessOwnerImportRowErrorShopMissing = "店铺编码不存在或已删除"
// ShopBusinessOwnerImportRowErrorOperation 表示操作类型不是换绑或清空。
ShopBusinessOwnerImportRowErrorOperation = "操作类型非法"
// ShopBusinessOwnerImportRowErrorOwnerRequired 表示换绑未填写业务员登录账号。
ShopBusinessOwnerImportRowErrorOwnerRequired = "换绑必须填写业务员登录账号"
// ShopBusinessOwnerImportRowErrorOwnerForbidden 表示清空却填写了业务员登录账号。
ShopBusinessOwnerImportRowErrorOwnerForbidden = "清空不得填写业务员登录账号"
// ShopBusinessOwnerImportRowErrorOwnerInvalid 表示业务员登录账号不存在或非启用平台用户。
ShopBusinessOwnerImportRowErrorOwnerInvalid = "业务员登录账号不存在或非启用平台用户"
// ShopBusinessOwnerImportRowErrorFormat 表示该行列数与固定列序不一致。
ShopBusinessOwnerImportRowErrorFormat = "行格式错误"
)
const (
// ShopOwnerImportAccessDescription 表示店铺负责人导入入口的角色说明。
ShopOwnerImportAccessDescription = "仅超级管理员和平台账号可访问,代理与企业账号返回 403。"
)
// GetShopBusinessOwnerImportItemStatusName 返回导入行状态中文名称。
func GetShopBusinessOwnerImportItemStatusName(status int) string {
switch status {
case ShopBusinessOwnerImportItemStatusSuccess:
return "成功"
case ShopBusinessOwnerImportItemStatusFailed:
return "失败"
default:
return "未知"
}
}

View File

@@ -76,6 +76,8 @@ func BuildDocHandlers() *bootstrap.Handlers {
Refund: admin.NewRefundHandler(nil),
OrderPackageInvalidate: admin.NewOrderPackageInvalidateHandler(nil),
AssetPackageBatchOrder: admin.NewAssetPackageBatchOrderHandler(nil, nil),
BusinessUserGroup: admin.NewBusinessUserGroupHandler(nil, nil),
ShopBusinessOwnerImport: admin.NewShopBusinessOwnerImportHandler(nil),
ClientWechat: app.NewClientWechatHandler(nil, nil, nil),
SuperAdmin: admin.NewSuperAdminHandler(nil),
SystemConfig: admin.NewSystemConfigHandler(nil, nil),

View File

@@ -74,6 +74,7 @@ func (h *Handler) RegisterHandlers() *asynq.ServeMux {
h.registerDeviceImportHandler()
h.registerOrderPackageInvalidateHandler()
h.registerAssetPackageBatchOrderHandler()
h.registerShopBusinessOwnerImportHandler()
h.registerExportHandlers()
h.registerCommissionStatsHandlers()
h.registerCommissionCalculationHandler()
@@ -141,6 +142,18 @@ func (h *Handler) registerAssetPackageBatchOrderHandler() {
h.logger.Info("注册资产套餐批量订购任务处理器", zap.String("task_type", constants.TaskTypeAssetPackageBatchOrder))
}
func (h *Handler) registerShopBusinessOwnerImportHandler() {
handler := task.NewShopBusinessOwnerImportHandler(
h.db,
h.workerResult.Stores.ShopBusinessOwnerImportTask,
h.storage,
h.logger,
audit.NewWriter(audit.NewRegistry(), nil),
)
h.mux.HandleFunc(constants.TaskTypeShopBusinessOwnerImport, handler.Handle)
h.logger.Info("注册店铺负责人导入任务处理器", zap.String("task_type", constants.TaskTypeShopBusinessOwnerImport))
}
func (h *Handler) registerDeviceImportHandler() {
deviceImportHandler := task.NewDeviceImportHandler(
h.db,

View File

@@ -22,37 +22,38 @@ type OrderExpirer interface {
// WorkerStores Worker 侧所有 Store 的集合
type WorkerStores struct {
AssetAllocationRecord *postgres.AssetAllocationRecordStore
IotCardImportTask *postgres.IotCardImportTaskStore
IotCard *postgres.IotCardStore
DeviceImportTask *postgres.DeviceImportTaskStore
ExportTask *postgres.ExportTaskStore
ExportShardTask *postgres.ExportShardTaskStore
Device *postgres.DeviceStore
DeviceSimBinding *postgres.DeviceSimBindingStore
ShopSeriesCommissionStats *postgres.ShopSeriesCommissionStatsStore
ShopPackageAllocation *postgres.ShopPackageAllocationStore
CommissionRecord *postgres.CommissionRecordStore
Shop *postgres.ShopStore
ShopSeriesAllocation *postgres.ShopSeriesAllocationStore
PackageSeries *postgres.PackageSeriesStore
Order *postgres.OrderStore
OrderItem *postgres.OrderItemStore
Package *postgres.PackageStore
PackageUsage *postgres.PackageUsageStore
PackageUsageDailyRecord *postgres.PackageUsageDailyRecordStore
PollingAlertRule *postgres.PollingAlertRuleStore
PollingAlertHistory *postgres.PollingAlertHistoryStore
DataCleanupConfig *postgres.DataCleanupConfigStore
DataCleanupLog *postgres.DataCleanupLogStore
AgentWallet *postgres.AgentWalletStore
AgentWalletTransaction *postgres.AgentWalletTransactionStore
AssetWallet *postgres.AssetWalletStore
AssetIdentifier *postgres.AssetIdentifierStore
PersonalCustomer *postgres.PersonalCustomerStore
PersonalCustomerPhone *postgres.PersonalCustomerPhoneStore
OrderPackageInvalidateTask *postgres.OrderPackageInvalidateTaskStore
AssetPackageBatchOrderTask *postgres.AssetPackageBatchOrderTaskStore
AssetAllocationRecord *postgres.AssetAllocationRecordStore
IotCardImportTask *postgres.IotCardImportTaskStore
IotCard *postgres.IotCardStore
DeviceImportTask *postgres.DeviceImportTaskStore
ExportTask *postgres.ExportTaskStore
ExportShardTask *postgres.ExportShardTaskStore
Device *postgres.DeviceStore
DeviceSimBinding *postgres.DeviceSimBindingStore
ShopSeriesCommissionStats *postgres.ShopSeriesCommissionStatsStore
ShopPackageAllocation *postgres.ShopPackageAllocationStore
CommissionRecord *postgres.CommissionRecordStore
Shop *postgres.ShopStore
ShopSeriesAllocation *postgres.ShopSeriesAllocationStore
PackageSeries *postgres.PackageSeriesStore
Order *postgres.OrderStore
OrderItem *postgres.OrderItemStore
Package *postgres.PackageStore
PackageUsage *postgres.PackageUsageStore
PackageUsageDailyRecord *postgres.PackageUsageDailyRecordStore
PollingAlertRule *postgres.PollingAlertRuleStore
PollingAlertHistory *postgres.PollingAlertHistoryStore
DataCleanupConfig *postgres.DataCleanupConfigStore
DataCleanupLog *postgres.DataCleanupLogStore
AgentWallet *postgres.AgentWalletStore
AgentWalletTransaction *postgres.AgentWalletTransactionStore
AssetWallet *postgres.AssetWalletStore
AssetIdentifier *postgres.AssetIdentifierStore
PersonalCustomer *postgres.PersonalCustomerStore
PersonalCustomerPhone *postgres.PersonalCustomerPhoneStore
OrderPackageInvalidateTask *postgres.OrderPackageInvalidateTaskStore
AssetPackageBatchOrderTask *postgres.AssetPackageBatchOrderTaskStore
ShopBusinessOwnerImportTask *postgres.ShopBusinessOwnerImportTaskStore
}
// WorkerServices Worker 侧所有 Service 的集合

View File

@@ -36,7 +36,7 @@ func (s *Service) GenerateFileKey(purpose, fileName string) (string, error) {
if ext == "" {
ext = ".bin"
}
if (purpose == constants.StoragePurposeAssetPackageBatchOrder || purpose == constants.StoragePurposeDeviceBatchAllocation) && !strings.EqualFold(ext, ".csv") {
if (purpose == constants.StoragePurposeAssetPackageBatchOrder || purpose == constants.StoragePurposeDeviceBatchAllocation || purpose == constants.StoragePurposeShopBusinessOwnerImport) && !strings.EqualFold(ext, ".csv") {
return "", errors.New(errors.CodeInvalidParam, "批量业务文件必须为CSV格式")
}

View File

@@ -19,4 +19,7 @@ var PurposeMappings = map[string]PurposeMapping{
"attachment": {Prefix: "attachments", ContentType: ""},
constants.StoragePurposeAssetPackageBatchOrder: {Prefix: constants.AssetPackageBatchOrderStoragePrefix, ContentType: "text/csv"},
constants.StoragePurposeDeviceBatchAllocation: {Prefix: constants.DeviceBatchAllocationStoragePrefix, ContentType: "text/csv"},
constants.StoragePurposeShopBusinessOwnerImport: {
Prefix: constants.ShopBusinessOwnerImportStoragePrefix, ContentType: "text/csv",
},
}

40
pkg/utils/encoding.go Normal file
View File

@@ -0,0 +1,40 @@
package utils
import (
"fmt"
"strings"
"unicode/utf8"
"golang.org/x/text/encoding/simplifiedchinese"
)
// utf8BOM 是 UTF-8 字节顺序标记,上传的 CSV 常带该前缀。
var utf8BOM = []byte{0xEF, 0xBB, 0xBF}
// replacementChar 是解码器无法映射字节时产生的替换字符。
const replacementChar = "\uFFFD"
// DecodeTextToUTF8 把上传文本解码为 UTF-8 字节。
// 先剥离 UTF-8 BOM剥离后已是合法 UTF-8 时原样返回,否则按 GBK 回退解码。
// 仅在 UTF-8 校验失败时才尝试 GBK避免对合法 UTF-8 内容做启发式改写;
// GBK 解码失败或仍残留无法映射的字节时返回错误,由调用方按任务级失败处理。
func DecodeTextToUTF8(data []byte) ([]byte, error) {
trimmed := data
if len(trimmed) >= len(utf8BOM) && string(trimmed[:len(utf8BOM)]) == string(utf8BOM) {
trimmed = trimmed[len(utf8BOM):]
}
if utf8.Valid(trimmed) {
return trimmed, nil
}
decoded, err := simplifiedchinese.GBK.NewDecoder().Bytes(trimmed)
if err != nil {
return nil, fmt.Errorf("GBK 解码失败: %w", err)
}
if !utf8.Valid(decoded) {
return nil, fmt.Errorf("GBK 解码结果不是合法 UTF-8")
}
if strings.Contains(string(decoded), replacementChar) {
return nil, fmt.Errorf("GBK 解码存在无法映射的字节")
}
return decoded, nil
}