七月迭代短暂完结,还有很多后端的关键东西没有弄,这是一版赶时间做的东西
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m26s

This commit is contained in:
2026-07-25 17:06:58 +08:00
parent ad9f613dd6
commit 73f5125d3d
249 changed files with 17137 additions and 877 deletions

View File

@@ -99,6 +99,13 @@ func (s *AccountStore) Update(ctx context.Context, account *model.Account) error
return s.db.WithContext(ctx).Save(account).Error
}
// BindWeCom 更新账号的企业微信成员绑定快照。
func (s *AccountStore) BindWeCom(ctx context.Context, accountID uint, corpID, userID, name string, updater uint) error {
return s.db.WithContext(ctx).Model(&model.Account{}).Where("id = ?", accountID).Updates(map[string]any{
"wecom_corp_id": corpID, "wecom_userid": userID, "wecom_name": name, "updater": updater,
}).Error
}
// Delete 软删除账号
func (s *AccountStore) Delete(ctx context.Context, id uint) error {
return s.db.WithContext(ctx).Delete(&model.Account{}, id).Error
@@ -254,6 +261,22 @@ func (s *AccountStore) GetByIDs(ctx context.Context, ids []uint) ([]*model.Accou
return accounts, nil
}
// GetDisplayAccountsByIDs 批量读取历史业务记录的账号展示信息。
// 该查询只返回 ID 和用户名,包含已软删除账号,不用于授权判定。
func (s *AccountStore) GetDisplayAccountsByIDs(ctx context.Context, ids []uint) ([]*model.Account, error) {
if len(ids) == 0 {
return []*model.Account{}, nil
}
var accounts []*model.Account
if err := s.db.WithContext(ctx).Unscoped().
Select("id", "username").
Where("id IN ?", ids).
Find(&accounts).Error; err != nil {
return nil, err
}
return accounts, nil
}
func (s *AccountStore) GetPrimaryAccountsByShopIDs(ctx context.Context, shopIDs []uint) ([]*model.Account, error) {
if len(shopIDs) == 0 {
return []*model.Account{}, nil

View File

@@ -0,0 +1,106 @@
package postgres
import (
"context"
"fmt"
"time"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/store"
"github.com/break/junhong_cmp_fiber/pkg/asynctask"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
)
// AssetPackageBatchOrderTaskStore 批量订购任务数据访问层。
type AssetPackageBatchOrderTaskStore struct {
db *gorm.DB
}
// NewAssetPackageBatchOrderTaskStore 创建批量订购任务 Store。
func NewAssetPackageBatchOrderTaskStore(db *gorm.DB) *AssetPackageBatchOrderTaskStore {
return &AssetPackageBatchOrderTaskStore{db: db}
}
// Create 创建批量订购任务。
func (s *AssetPackageBatchOrderTaskStore) Create(ctx context.Context, task *model.AssetPackageBatchOrderTask) error {
return s.db.WithContext(ctx).Create(task).Error
}
// GetByID 按 ID 查询批量订购任务。
func (s *AssetPackageBatchOrderTaskStore) GetByID(ctx context.Context, id uint) (*model.AssetPackageBatchOrderTask, error) {
var task model.AssetPackageBatchOrderTask
query := s.applyVisibleScope(ctx, s.db.WithContext(ctx).Where("id = ?", id))
if err := query.First(&task).Error; err != nil {
return nil, err
}
return &task, nil
}
// List 分页查询批量订购任务。
func (s *AssetPackageBatchOrderTaskStore) List(ctx context.Context, opts *store.QueryOptions, status *int) ([]*model.AssetPackageBatchOrderTask, int64, error) {
query := s.applyVisibleScope(ctx, s.db.WithContext(ctx).Model(&model.AssetPackageBatchOrderTask{}))
if status != nil {
query = query.Where("status = ?", *status)
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
if opts == nil {
opts = &store.QueryOptions{Page: 1, PageSize: constants.DefaultPageSize}
}
var tasks []*model.AssetPackageBatchOrderTask
if err := query.Order("created_at DESC").Offset((opts.Page - 1) * opts.PageSize).Limit(opts.PageSize).Find(&tasks).Error; err != nil {
return nil, 0, err
}
return tasks, total, nil
}
// Claim 领取待处理或已超时的处理中任务,避免正常执行期间被重复消费。
func (s *AssetPackageBatchOrderTaskStore) Claim(ctx context.Context, id uint) (bool, error) {
now := time.Now()
staleBefore := now.Add(-constants.AssetPackageBatchOrderTaskTimeout)
result := s.db.WithContext(ctx).Model(&model.AssetPackageBatchOrderTask{}).
Where("id = ? AND (status = ? OR (status = ? AND started_at < ?))", id, asynctask.StatusPending, asynctask.StatusProcessing, staleBefore).
Updates(map[string]any{"status": asynctask.StatusProcessing, "started_at": now, "updated_at": now})
return result.RowsAffected == 1, result.Error
}
func (s *AssetPackageBatchOrderTaskStore) applyVisibleScope(ctx context.Context, query *gorm.DB) *gorm.DB {
if middleware.GetUserTypeFromContext(ctx) != constants.UserTypeAgent {
return query
}
shopIDs := middleware.GetSubordinateShopIDs(ctx)
if len(shopIDs) == 0 {
return query.Where("1 = 0")
}
return query.Where("creator_shop_id IN ?", shopIDs)
}
// MarkFailed 将任务标记为整体失败。
func (s *AssetPackageBatchOrderTaskStore) MarkFailed(ctx context.Context, id uint, message string) error {
now := time.Now()
return s.db.WithContext(ctx).Model(&model.AssetPackageBatchOrderTask{}).Where("id = ?", id).
Updates(map[string]any{"status": asynctask.StatusFailed, "error_message": message, "completed_at": now, "updated_at": now}).Error
}
// Complete 保存逐行结果并完成任务。
func (s *AssetPackageBatchOrderTaskStore) Complete(ctx context.Context, id uint, items model.AssetPackageBatchOrderResultItems, successCount, failCount int) error {
now := time.Now()
return s.db.WithContext(ctx).Model(&model.AssetPackageBatchOrderTask{}).
Where("id = ? AND status = ?", id, asynctask.StatusProcessing).
Updates(map[string]any{
"status": asynctask.StatusCompleted, "total_count": len(items),
"success_count": successCount, "fail_count": failCount,
"result_items": items, "completed_at": now, "updated_at": now,
}).Error
}
// GenerateTaskNo 生成批量订购任务编号。
func (s *AssetPackageBatchOrderTaskStore) GenerateTaskNo() string {
now := time.Now()
return fmt.Sprintf("BPO-%s-%06d", now.Format("20060102"), now.UnixNano()%1000000)
}

View File

@@ -101,6 +101,9 @@ func (s *DeviceImportTaskStore) List(ctx context.Context, opts *store.QueryOptio
if status, ok := filters["status"].(int); ok && status > 0 {
query = query.Where("status = ?", status)
}
if operationType, ok := filters["operation_type"].(string); ok && operationType != "" {
query = query.Where("operation_type = ?", operationType)
}
if batchNo, ok := filters["batch_no"].(string); ok && batchNo != "" {
query = query.Where("batch_no LIKE ?", "%"+batchNo+"%")
}

View File

@@ -70,6 +70,21 @@ func (s *DeviceStore) GetByIdentifier(ctx context.Context, identifier string) (*
return &device, nil
}
// GetByIdentifiers 批量按 VirtualNo、IMEI 或 SN 查询设备,并应用当前数据权限。
func (s *DeviceStore) GetByIdentifiers(ctx context.Context, identifiers []string) ([]*model.Device, error) {
devices := make([]*model.Device, 0)
if len(identifiers) == 0 {
return devices, nil
}
query := s.db.WithContext(ctx).
Where("virtual_no IN ? OR imei IN ? OR sn IN ?", identifiers, identifiers, identifiers)
query = middleware.ApplyShopFilter(ctx, query)
if err := query.Find(&devices).Error; err != nil {
return nil, err
}
return devices, nil
}
func (s *DeviceStore) GetByIDs(ctx context.Context, ids []uint) ([]*model.Device, error) {
var devices []*model.Device
if len(ids) == 0 {
@@ -152,6 +167,9 @@ func (s *DeviceStore) applyDeviceFilters(ctx context.Context, query *gorm.DB, fi
if activationStatus, ok := filters["activation_status"].(int); ok {
query = s.applyActivationStatusFilter(query, activationStatus)
}
if realNameStatus, ok := filters["real_name_status"].(int); ok {
query = s.applyRealNameStatusFilter(query, realNameStatus)
}
if shopIDs, ok := filters["shop_ids"].([]uint); ok {
if len(shopIDs) == 0 {
query = query.Where("1 = 0")
@@ -206,6 +224,27 @@ func (s *DeviceStore) applyDeviceFilters(ctx context.Context, query *gorm.DB, fi
return query
}
// applyRealNameStatusFilter 按设备当前有效绑定卡的实名状态过滤。
// 任意有效绑定卡已实名时设备视为已实名,否则视为未实名。
func (s *DeviceStore) applyRealNameStatusFilter(query *gorm.DB, realNameStatus int) *gorm.DB {
verifiedBindingCondition := `EXISTS (
SELECT 1
FROM tb_device_sim_binding AS b
JOIN tb_iot_card AS c
ON c.id = b.iot_card_id
AND c.real_name_status = ?
AND c.deleted_at IS NULL
WHERE b.device_id = tb_device.id
AND b.bind_status = ?
AND b.deleted_at IS NULL
)`
args := []any{constants.RealNameStatusVerified, constants.BindStatusBound}
if realNameStatus == constants.RealNameStatusVerified {
return query.Where(verifiedBindingCondition, args...)
}
return query.Where("NOT "+verifiedBindingCondition, args...)
}
// applyHasActivePackageFilter 按"是否有生效中主套餐"过滤设备。
// true: 设备在 tb_package_usage 中存在生效中(status=PackageUsageStatusActive)的主套餐记录;
// false: 不存在上述记录。

View File

@@ -902,6 +902,9 @@ func (s *IotCardStore) applyStandaloneFilters(ctx context.Context, query *gorm.D
if networkStatus, ok := filters["network_status"].(int); ok {
query = query.Where("network_status = ?", networkStatus)
}
if realNameStatus, ok := filters["real_name_status"].(int); ok {
query = query.Where("real_name_status = ?", realNameStatus)
}
if enterpriseID, ok := filters["authorized_enterprise_id"].(uint); ok && enterpriseID > 0 {
// 只返回有效授权给该企业的卡
query = query.Where("id IN (?)",

View File

@@ -144,3 +144,29 @@ func (s *RefundStore) FindActiveByOrderID(ctx context.Context, orderID uint) (*m
}
return &req, nil
}
// HasUnfinishedByAsset 检查资产是否存在仍会影响资产状态的退款申请。
// 待审批、已退回,以及已通过但退款后资产处理尚未完成的申请均视为未终结。
func (s *RefundStore) HasUnfinishedByAsset(ctx context.Context, assetType string, assetID uint) (bool, error) {
var assetColumn string
switch assetType {
case constants.ExchangeAssetTypeIotCard:
assetColumn = "iot_card_id"
case constants.ExchangeAssetTypeDevice:
assetColumn = "device_id"
default:
return false, gorm.ErrInvalidData
}
var count int64
err := s.db.WithContext(ctx).
Model(&model.RefundRequest{}).
Where(assetColumn+" = ?", assetID).
Where("status IN ? OR (status = ? AND asset_reset = ?)",
[]int{model.RefundStatusPending, model.RefundStatusReturned},
model.RefundStatusApproved,
false,
).
Count(&count).Error
return count > 0, err
}