Files
junhong_cmp_fiber/internal/handler/admin/package_silence.go
邹进 909f4d0ec2
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Failing after 8m39s
feat: 实名即生效套餐沉默期、延期办理与资产出库时间
- 迁移 000259~000265:套餐记录沉默期字段与扫描索引、套餐默认沉默期、延期政策/流水/订单/批次/明细 5 张表、默认政策与系统配置(总开关默认关闭)、企微审批白名单、卡与设备出库时间及出库记录
- 沉默期设置(订单发放、自动购包、钱包续费)、后台购买时调整(超默认值按政策免审或企微审批)、统一判定与激活闸门、排队与孤儿扫描跳过过期套餐、过期套餐禁止退款、每 10 分钟扫描
- 总开关专用接口与打开前 4 项检查;延期政策;延期资格与计费;H5 自助与后台二维码在线支付(商户池路由)、支付回调与查单恢复、线下登记、特殊/批量延期、异常重试与原路退款;沉默期审批场景与终态处理
- 换货沉默期流水与出库时间;卡/设备列表、详情、导出与出库记录接口;延期订单与即将过期套餐列表及导出;财务流水纳入延期支付
- 沉默期与出库时间回填工具 cmd/silence-backfill;前端接口变更说明

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:32:58 +09:00

360 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package admin
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
packagesilenceapp "github.com/break/junhong_cmp_fiber/internal/application/packagesilence"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
packagesilencequery "github.com/break/junhong_cmp_fiber/internal/query/packagesilence"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/response"
)
// PackageSilenceHandler 后台沉默期:总开关、延期政策、延期资格、延期订单、线下登记、特殊延期、批次、异常、流水与出库记录。
// Handler 只做绑定与参数校验,权限点、数据范围、金额与状态由用例判断。
type PackageSilenceHandler struct {
switches *packagesilenceapp.SwitchService
policies *packagesilenceapp.PolicyService
extensions *packagesilenceapp.ExtensionService
query *packagesilencequery.Query
validator *validator.Validate
}
// NewPackageSilenceHandler 创建后台沉默期 Handler。
func NewPackageSilenceHandler(
switches *packagesilenceapp.SwitchService,
policies *packagesilenceapp.PolicyService,
extensions *packagesilenceapp.ExtensionService,
query *packagesilencequery.Query,
validator *validator.Validate,
) *PackageSilenceHandler {
return &PackageSilenceHandler{switches: switches, policies: policies, extensions: extensions, query: query, validator: validator}
}
func (h *PackageSilenceHandler) bindQuery(c *fiber.Ctx, prefix string, req any) error {
if err := c.QueryParser(req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
return h.validate(prefix, req)
}
func (h *PackageSilenceHandler) bindBody(c *fiber.Ctx, prefix string, req any) error {
if err := c.BodyParser(req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
return h.validate(prefix, req)
}
func (h *PackageSilenceHandler) validate(prefix string, req any) error {
if h.validator == nil {
return nil
}
if err := h.validator.Struct(req); err != nil {
return errors.New(errors.CodeInvalidParam, validationMessage(prefix, req, err))
}
return nil
}
func silencePathID(c *fiber.Ctx) (uint, error) {
id, err := c.ParamsInt("id")
if err != nil || id <= 0 {
return 0, errors.New(errors.CodeInvalidParam, "ID 无效")
}
return uint(id), nil
}
// GetSwitch 查询沉默期总开关与打开前检查。GET /api/admin/package-silence/switch
func (h *PackageSilenceHandler) GetSwitch(c *fiber.Ctx) error {
result, err := h.switches.Status(c.UserContext())
if err != nil {
return err
}
return response.Success(c, result)
}
// UpdateSwitch 切换沉默期总开关(仅超级管理员)。PUT /api/admin/package-silence/switch
func (h *PackageSilenceHandler) UpdateSwitch(c *fiber.Ctx) error {
var req dto.SilenceSwitchRequest
if err := h.bindBody(c, "沉默期总开关参数不合法", &req); err != nil {
return err
}
result, err := h.switches.Toggle(c.UserContext(), *req.Enabled)
if err != nil {
return err
}
return response.Success(c, result)
}
// ListPolicies 延期政策列表。GET /api/admin/package-silence/policies
func (h *PackageSilenceHandler) ListPolicies(c *fiber.Ctx) error {
var req dto.SilencePolicyListRequest
if err := h.bindQuery(c, "延期政策查询参数不合法", &req); err != nil {
return err
}
items, total, err := h.policies.List(c.UserContext(), &req)
if err != nil {
return err
}
page, pageSize := req.Page, req.PageSize
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 20
}
return response.SuccessWithPagination(c, items, total, page, pageSize)
}
// GetPolicy 延期政策详情。GET /api/admin/package-silence/policies/:id
func (h *PackageSilenceHandler) GetPolicy(c *fiber.Ctx) error {
id, err := silencePathID(c)
if err != nil {
return err
}
result, err := h.policies.Get(c.UserContext(), id)
if err != nil {
return err
}
return response.Success(c, result)
}
// CreatePolicy 新增延期政策。POST /api/admin/package-silence/policies
func (h *PackageSilenceHandler) CreatePolicy(c *fiber.Ctx) error {
var req dto.SilencePolicyRequest
if err := h.bindBody(c, "延期政策参数不合法", &req); err != nil {
return err
}
result, err := h.policies.Create(c.UserContext(), &req)
if err != nil {
return err
}
return response.Success(c, result)
}
// UpdatePolicy 编辑延期政策。PUT /api/admin/package-silence/policies/:id
func (h *PackageSilenceHandler) UpdatePolicy(c *fiber.Ctx) error {
id, err := silencePathID(c)
if err != nil {
return err
}
var req dto.SilencePolicyRequest
if err := h.bindBody(c, "延期政策参数不合法", &req); err != nil {
return err
}
result, err := h.policies.Update(c.UserContext(), id, &req)
if err != nil {
return err
}
return response.Success(c, result)
}
// UpdatePolicyStatus 启停延期政策。PUT /api/admin/package-silence/policies/:id/status
func (h *PackageSilenceHandler) UpdatePolicyStatus(c *fiber.Ctx) error {
id, err := silencePathID(c)
if err != nil {
return err
}
var req dto.SilencePolicyStatusRequest
if err := h.bindBody(c, "延期政策状态参数不合法", &req); err != nil {
return err
}
result, err := h.policies.SetStatus(c.UserContext(), id, *req.Status)
if err != nil {
return err
}
return response.Success(c, result)
}
// MatchPolicy 查询套餐当前匹配的延期政策。GET /api/admin/package-silence/policies/match
func (h *PackageSilenceHandler) MatchPolicy(c *fiber.Ctx) error {
var req dto.SilencePolicyMatchRequest
if err := h.bindQuery(c, "政策匹配查询参数不合法", &req); err != nil {
return err
}
result, err := h.policies.MatchForPackage(c.UserContext(), req.PackageID)
if err != nil {
return err
}
return response.Success(c, result)
}
// Eligibility 延期资格。GET /api/admin/package-silence/eligibility
func (h *PackageSilenceHandler) Eligibility(c *fiber.Ctx) error {
var req dto.SilenceEligibilityRequest
if err := h.bindQuery(c, "延期资格查询参数不合法", &req); err != nil {
return err
}
result, err := h.extensions.Eligibility(c.UserContext(), req.PackageUsageID, packagesilenceapp.Scope{})
if err != nil {
return err
}
return response.Success(c, result)
}
// Quote 按天数试算付费延期。GET /api/admin/package-silence/quote
func (h *PackageSilenceHandler) Quote(c *fiber.Ctx) error {
var req dto.SilenceQuoteRequest
if err := h.bindQuery(c, "延期试算参数不合法", &req); err != nil {
return err
}
result, err := h.extensions.Quote(c.UserContext(), req.PackageUsageID, req.Days, packagesilenceapp.Scope{})
if err != nil {
return err
}
return response.Success(c, result)
}
// CreateQRCodeOrder 运营创建在线延期订单并生成支付二维码。POST /api/admin/package-silence/orders/qrcode
func (h *PackageSilenceHandler) CreateQRCodeOrder(c *fiber.Ctx) error {
var req dto.SilenceOnlineOrderRequest
if err := h.bindBody(c, "延期下单参数不合法", &req); err != nil {
return err
}
result, err := h.extensions.CreateOnlineOrder(c.UserContext(), packagesilenceapp.OnlineOrderCommand{
Request: req, Scope: packagesilenceapp.Scope{}, PayerClientIP: c.IP(),
})
if err != nil {
return err
}
return response.Success(c, result)
}
// RegisterOffline 登记线下付费延期。POST /api/admin/package-silence/orders/offline
func (h *PackageSilenceHandler) RegisterOffline(c *fiber.Ctx) error {
var req dto.SilenceOfflineOrderRequest
if err := h.bindBody(c, "线下付费延期参数不合法", &req); err != nil {
return err
}
result, err := h.extensions.RegisterOffline(c.UserContext(), req)
if err != nil {
return err
}
return response.Success(c, result)
}
// ListOrders 延期订单列表。GET /api/admin/package-silence/orders
func (h *PackageSilenceHandler) ListOrders(c *fiber.Ctx) error {
var req dto.SilenceOrderListRequest
if err := h.bindQuery(c, "延期订单查询参数不合法", &req); err != nil {
return err
}
result, err := h.query.ListOrders(c.UserContext(), packagesilencequery.OrderFilter{Request: req})
if err != nil {
return err
}
return response.Success(c, result)
}
// GetOrder 延期订单详情。GET /api/admin/package-silence/orders/:id
func (h *PackageSilenceHandler) GetOrder(c *fiber.Ctx) error {
id, err := silencePathID(c)
if err != nil {
return err
}
result, err := h.query.GetOrder(c.UserContext(), id, 0)
if err != nil {
return err
}
return response.Success(c, result)
}
// SubmitSpecial 提交特殊延期(单个 / 批量)。POST /api/admin/package-silence/special
func (h *PackageSilenceHandler) SubmitSpecial(c *fiber.Ctx) error {
var req dto.SilenceSpecialRequest
if err := h.bindBody(c, "特殊延期参数不合法", &req); err != nil {
return err
}
result, err := h.extensions.SubmitSpecial(c.UserContext(), req)
if err != nil {
return err
}
return response.Success(c, result)
}
// ListBatches 延期批次列表。GET /api/admin/package-silence/batches
func (h *PackageSilenceHandler) ListBatches(c *fiber.Ctx) error {
var req dto.SilenceBatchListRequest
if err := h.bindQuery(c, "延期批次查询参数不合法", &req); err != nil {
return err
}
result, err := h.query.ListBatches(c.UserContext(), req)
if err != nil {
return err
}
return response.Success(c, result)
}
// GetBatch 延期批次详情(含明细)。GET /api/admin/package-silence/batches/:id
func (h *PackageSilenceHandler) GetBatch(c *fiber.Ctx) error {
id, err := silencePathID(c)
if err != nil {
return err
}
result, err := h.extensions.BatchDetail(c.UserContext(), id)
if err != nil {
return err
}
return response.Success(c, result)
}
// HandleException 异常明细重试 / 关闭。POST /api/admin/package-silence/extensions/:id/handle
func (h *PackageSilenceHandler) HandleException(c *fiber.Ctx) error {
id, err := silencePathID(c)
if err != nil {
return err
}
var req dto.SilenceExceptionHandleRequest
if err := h.bindBody(c, "异常处理参数不合法", &req); err != nil {
return err
}
result, err := h.extensions.HandleException(c.UserContext(), id, req)
if err != nil {
return err
}
return response.Success(c, result)
}
// ListRecords 套餐沉默期流水。GET /api/admin/package-silence/records
func (h *PackageSilenceHandler) ListRecords(c *fiber.Ctx) error {
var req dto.SilenceRecordListRequest
if err := h.bindQuery(c, "沉默期流水查询参数不合法", &req); err != nil {
return err
}
result, err := h.extensions.Records(c.UserContext(), req.PackageUsageID, packagesilenceapp.Scope{})
if err != nil {
return err
}
return response.Success(c, result)
}
// ListUsages 即将过期 / 已过期套餐。GET /api/admin/package-silence/usages
func (h *PackageSilenceHandler) ListUsages(c *fiber.Ctx) error {
var req dto.SilenceUsageListRequest
if err := h.bindQuery(c, "沉默期套餐查询参数不合法", &req); err != nil {
return err
}
result, err := h.query.ListUsages(c.UserContext(), req)
if err != nil {
return err
}
return response.Success(c, result)
}
// ListOutboundRecords 资产各世代出库记录。GET /api/admin/assets/:asset_type/:id/outbound-records
func (h *PackageSilenceHandler) ListOutboundRecords(c *fiber.Ctx) error {
id, err := silencePathID(c)
if err != nil {
return err
}
assetType := c.Params("asset_type")
if assetType != "card" && assetType != "iot_card" && assetType != "device" {
return errors.New(errors.CodeInvalidParam, "资产类型仅支持 card 或 device")
}
result, err := h.query.ListOutboundRecords(c.UserContext(), assetType, id)
if err != nil {
return err
}
return response.Success(c, result)
}