feat(手机号资产关联): AUG26-009 手机号—资产关联、十项上限与后台解绑
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m2s

- 新增成对迁移 000223(tb_phone_asset_association,含有效关系部分唯一索引与 down 守卫)与 000224(解绑导入任务表),不回填历史
- H5:need_bind_phone 三支判定(开关关闭完全短路);已有主号幂等建联;十项上限按手机号 advisory 串行化(含换绑到全新号的并发场景);换绑原子迁移与冲突整单回滚;不写遗留列
- 后台:关联列表、单项/批量解绑、CSV 导入解绑(B1–B16),超管/平台 gate + 资产数据范围复核,三态统一文案
- 读侧:卡/设备列表与详情按页一次 IN 聚合;两类导出补「关联手机号」列并保留历史表头反解兼容
- 脱敏:关联审计走独立动作/资源只写脱敏手机号;访问日志手机号类字段脱敏
- 同步主 Spec openspec/specs/phone-asset-association 并归档 AUG26-009,补齐 requirement-evidence 与入口矩阵,context-health 通过
This commit is contained in:
2026-09-15 11:54:56 +08:00
parent 93e072e1e2
commit 70e680eb0a
67 changed files with 3951 additions and 147 deletions

View File

@@ -0,0 +1,157 @@
package admin
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
associationSvc "github.com/break/junhong_cmp_fiber/internal/service/phone_asset_association"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/response"
)
// PhoneAssetAssociationHandler 手机号—资产关联后台处理器。
// 仅提供查看投影与解除入口;关联只能由 H5 短信验证建立,后台不提供创建或补录入口。
type PhoneAssetAssociationHandler struct {
service *associationSvc.Service
validator *validator.Validate
}
// NewPhoneAssetAssociationHandler 创建手机号—资产关联处理器。
func NewPhoneAssetAssociationHandler(service *associationSvc.Service, validator *validator.Validate) *PhoneAssetAssociationHandler {
return &PhoneAssetAssociationHandler{service: service, validator: validator}
}
// List 查询手机号—资产关联列表。
// GET /api/admin/phone-asset-associations
func (h *PhoneAssetAssociationHandler) List(c *fiber.Ctx) error {
if err := requirePlatformManagement(c); err != nil {
return err
}
var request dto.ListPhoneAssetAssociationRequest
if err := c.QueryParser(&request); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
if err := h.validate("手机号资产关联查询参数不合法", &request); err != nil {
return err
}
result, err := h.service.List(c.UserContext(), &request)
if err != nil {
return err
}
return response.SuccessWithPagination(c, result.Items, result.Total, result.Page, result.Size)
}
// Unbind 解除指定的一条手机号—资产关联。
// DELETE /api/admin/phone-asset-associations/:id
func (h *PhoneAssetAssociationHandler) Unbind(c *fiber.Ctx) error {
if err := requirePlatformManagement(c); err != nil {
return err
}
id, err := pathID(c)
if err != nil {
return err
}
var request dto.UnbindPhoneAssetAssociationRequest
if err := c.BodyParser(&request); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
// 路径字段由 Handler 解析后回填再执行结构校验ENG-ERR-002
request.ID = id
if err := h.validate("手机号资产解绑参数不合法", &request); err != nil {
return err
}
result, err := h.service.Unbind(c.UserContext(), &request)
if err != nil {
return err
}
return response.Success(c, result)
}
// BatchUnbind 按资产集合解除全部当前有效关联。
// POST /api/admin/phone-asset-associations/batch-unbind
func (h *PhoneAssetAssociationHandler) BatchUnbind(c *fiber.Ctx) error {
if err := requirePlatformManagement(c); err != nil {
return err
}
var request dto.BatchUnbindPhoneAssetAssociationRequest
if err := c.BodyParser(&request); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
if err := h.validate("手机号资产批量解绑参数不合法", &request); err != nil {
return err
}
result, err := h.service.BatchUnbind(c.UserContext(), &request)
if err != nil {
return err
}
return response.Success(c, result)
}
// CreateImportTask 创建手机号资产解绑 CSV 导入任务。
// POST /api/admin/phone-asset-associations/unbind-imports
func (h *PhoneAssetAssociationHandler) CreateImportTask(c *fiber.Ctx) error {
if err := requirePlatformManagement(c); err != nil {
return err
}
var request dto.CreatePhoneAssetUnbindImportRequest
if err := c.BodyParser(&request); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
if err := h.validate("手机号资产解绑导入参数不合法", &request); err != nil {
return err
}
result, err := h.service.CreateImportTask(c.UserContext(), &request)
if err != nil {
return err
}
return response.Success(c, result)
}
// ListImportTasks 查询手机号资产解绑导入任务列表。
// GET /api/admin/phone-asset-associations/unbind-imports
func (h *PhoneAssetAssociationHandler) ListImportTasks(c *fiber.Ctx) error {
if err := requirePlatformManagement(c); err != nil {
return err
}
var request dto.ListPhoneAssetUnbindImportRequest
if err := c.QueryParser(&request); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
if err := h.validate("手机号资产解绑导入查询参数不合法", &request); err != nil {
return err
}
result, err := h.service.ListImportTasks(c.UserContext(), &request)
if err != nil {
return err
}
return response.SuccessWithPagination(c, result.Items, result.Total, result.Page, result.Size)
}
// ImportTaskDetail 查询手机号资产解绑导入任务详情与逐行结果。
// GET /api/admin/phone-asset-associations/unbind-imports/:id
func (h *PhoneAssetAssociationHandler) ImportTaskDetail(c *fiber.Ctx) error {
if err := requirePlatformManagement(c); err != nil {
return err
}
id, err := pathID(c)
if err != nil {
return err
}
result, err := h.service.GetImportTask(c.UserContext(), id)
if err != nil {
return err
}
return response.Success(c, result)
}
// validate 执行结构校验失败时返回字段级提示ENG-ERR-002
func (h *PhoneAssetAssociationHandler) validate(prefix string, request any) error {
if h.validator == nil {
return errors.New(errors.CodeInternalError)
}
if err := h.validator.Struct(request); err != nil {
return errors.New(errors.CodeInvalidParam, validationMessage(prefix, request, err))
}
return nil
}