Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
- 新增六对成对迁移 000232–000237:H5 弹窗类型、退款结算标识与申请人备注、优先轮询事实字段与两个新终态、通道阈值命中留痕、手机号最近解绑人、提现资格校验留痕 - 退款:原因必填与申请人备注、来源支付与渠道流水冻结、线下处理流水号补录审计、按订单查询可选退款方式、企微审批材料补齐且新增字段缺失映射即明确失败 - 优先轮询:人工关闭、有效期到期独立周期任务、失败与过期人工重触发、事实字段与异常重试查询、资产解析端点只读投影 - 通道阈值:命中事实同事务留痕与命中记录查询;员工账单:列表筛选与详情投影;商户池:列表投影与统计周期语义;H5:弹窗类型与类别排序 - 手机号:有效关联数量与最近解绑人、短信验证码失败次数限制;导出:佣金明细十五列与报表序号列 - 时间筛选:三处新增筛选纳入统一严格解析契约,员工账单产生时间参数改名 - 同步 12 份主 Spec 需求、两端点与异步任务证据链,门禁 context-health 与 OpenSpec 校验通过
296 lines
11 KiB
Go
296 lines
11 KiB
Go
package admin
|
||
|
||
import (
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/gofiber/fiber/v2"
|
||
|
||
employeecollectionapp "github.com/break/junhong_cmp_fiber/internal/application/employeecollection"
|
||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||
employeecollectionquery "github.com/break/junhong_cmp_fiber/internal/query/employeecollection"
|
||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||
)
|
||
|
||
// EmployeeCollectionHandler 处理员工代收款账单、核销申请与线下收款方式字典请求。
|
||
// 边界只做绑定、路径参数校验与统一响应,状态、金额与权限不变量由应用用例判断。
|
||
type EmployeeCollectionHandler struct {
|
||
paymentMethod *employeecollectionapp.PaymentMethodService
|
||
paymentMethodQuery *employeecollectionquery.PaymentMethodQuery
|
||
billClose *employeecollectionapp.BillCloseService
|
||
billQuery *employeecollectionquery.BillQuery
|
||
application *employeecollectionapp.ApplicationService
|
||
applicationQuery *employeecollectionquery.ApplicationQuery
|
||
}
|
||
|
||
// NewEmployeeCollectionHandler 创建员工代收款处理器。
|
||
func NewEmployeeCollectionHandler(
|
||
service *employeecollectionapp.PaymentMethodService,
|
||
billClose *employeecollectionapp.BillCloseService,
|
||
application *employeecollectionapp.ApplicationService,
|
||
) *EmployeeCollectionHandler {
|
||
return &EmployeeCollectionHandler{paymentMethod: service, billClose: billClose, application: application}
|
||
}
|
||
|
||
// SetApplicationQuery 注入核销申请只读投影。
|
||
func (h *EmployeeCollectionHandler) SetApplicationQuery(query *employeecollectionquery.ApplicationQuery) {
|
||
h.applicationQuery = query
|
||
}
|
||
|
||
// SetPaymentMethodQuery 注入线下收款方式字典只读投影。
|
||
func (h *EmployeeCollectionHandler) SetPaymentMethodQuery(query *employeecollectionquery.PaymentMethodQuery) {
|
||
h.paymentMethodQuery = query
|
||
}
|
||
|
||
// SetBillQuery 注入员工代收款账单只读投影。
|
||
func (h *EmployeeCollectionHandler) SetBillQuery(query *employeecollectionquery.BillQuery) {
|
||
h.billQuery = query
|
||
}
|
||
|
||
// CreatePaymentMethod 创建线下收款方式。
|
||
// POST /api/admin/employee-collection-payment-methods
|
||
func (h *EmployeeCollectionHandler) CreatePaymentMethod(c *fiber.Ctx) error {
|
||
var request dto.CreateEmployeeCollectionPaymentMethodRequest
|
||
if err := c.BodyParser(&request); err != nil {
|
||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||
}
|
||
result, err := h.paymentMethod.Create(c.UserContext(), request)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return response.Success(c, result)
|
||
}
|
||
|
||
// UpdatePaymentMethod 更新线下收款方式,支持修改名称、排序、启停、备注与未被引用时的稳定编码。
|
||
// PUT /api/admin/employee-collection-payment-methods/:id
|
||
func (h *EmployeeCollectionHandler) UpdatePaymentMethod(c *fiber.Ctx) error {
|
||
id, err := pathID(c)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
var request dto.UpdateEmployeeCollectionPaymentMethodRequest
|
||
if err := c.BodyParser(&request); err != nil {
|
||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||
}
|
||
result, err := h.paymentMethod.Update(c.UserContext(), id, request)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return response.Success(c, result)
|
||
}
|
||
|
||
// DeletePaymentMethod 删除未被核销申请引用的线下收款方式。
|
||
// DELETE /api/admin/employee-collection-payment-methods/:id
|
||
func (h *EmployeeCollectionHandler) DeletePaymentMethod(c *fiber.Ctx) error {
|
||
id, err := pathID(c)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err := h.paymentMethod.Delete(c.UserContext(), id); err != nil {
|
||
return err
|
||
}
|
||
return response.Success(c, nil)
|
||
}
|
||
|
||
// ListPaymentMethods 分页查询线下收款方式。
|
||
// GET /api/admin/employee-collection-payment-methods
|
||
func (h *EmployeeCollectionHandler) ListPaymentMethods(c *fiber.Ctx) error {
|
||
var request dto.EmployeeCollectionPaymentMethodListRequest
|
||
if err := c.QueryParser(&request); err != nil {
|
||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||
}
|
||
result, err := h.paymentMethodQuery.List(c.UserContext(), request)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return response.SuccessWithPagination(c, result.List, result.Total, result.Page, result.PageSize)
|
||
}
|
||
|
||
// ListBills 分页查询员工代收款账单。
|
||
// GET /api/admin/employee-collection-bills
|
||
func (h *EmployeeCollectionHandler) ListBills(c *fiber.Ctx) error {
|
||
if err := rejectLegacyBillTimeParams(c); err != nil {
|
||
return err
|
||
}
|
||
var request dto.EmployeeCollectionBillListRequest
|
||
if err := c.QueryParser(&request); err != nil {
|
||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||
}
|
||
result, err := h.billQuery.List(c.UserContext(), request)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return response.SuccessWithPagination(c, result.List, result.Total, result.Page, result.PageSize)
|
||
}
|
||
|
||
// StatisticsBills 汇总员工代收款账单金额与待处理数量。
|
||
// GET /api/admin/employee-collection-bills/statistics
|
||
func (h *EmployeeCollectionHandler) StatisticsBills(c *fiber.Ctx) error {
|
||
if err := rejectLegacyBillTimeParams(c); err != nil {
|
||
return err
|
||
}
|
||
var request dto.EmployeeCollectionBillStatisticsRequest
|
||
if err := c.QueryParser(&request); err != nil {
|
||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||
}
|
||
result, err := h.billQuery.Statistics(c.UserContext(), request)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return response.Success(c, result)
|
||
}
|
||
|
||
// billLegacyTimeParams 是本次被统一时间筛选替换的旧参数名。
|
||
var billLegacyTimeParams = []string{"created_from", "created_to"}
|
||
|
||
// rejectLegacyBillTimeParams 拒绝携带旧时间参数名的账单列表与统计请求。
|
||
//
|
||
// 时间筛选已统一为 start_time/end_time(严格 RFC3339 带时区秒级闭区间),
|
||
// 旧参数名 MUST NOT 被静默忽略或保留别名:静默忽略会让调用方以为筛选生效而拿到全量数据。
|
||
func rejectLegacyBillTimeParams(c *fiber.Ctx) error {
|
||
for _, name := range billLegacyTimeParams {
|
||
if c.Context().QueryArgs().Has(name) {
|
||
return errors.New(errors.CodeInvalidParam,
|
||
"账单时间筛选参数已统一为 start_time 与 end_time,不再接受 "+name)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// GetBill 查询员工代收款账单详情。
|
||
// GET /api/admin/employee-collection-bills/:id
|
||
func (h *EmployeeCollectionHandler) GetBill(c *fiber.Ctx) error {
|
||
id, err := pathID(c)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
result, err := h.billQuery.Detail(c.UserContext(), id)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return response.Success(c, result)
|
||
}
|
||
|
||
// CloseBill 关闭员工代收款账单,仅超级管理员可操作。
|
||
// POST /api/admin/employee-collection-bills/:id/close
|
||
func (h *EmployeeCollectionHandler) CloseBill(c *fiber.Ctx) error {
|
||
id, err := pathID(c)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
var request dto.CloseEmployeeCollectionBillRequest
|
||
if err := c.BodyParser(&request); err != nil {
|
||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||
}
|
||
bill, err := h.billClose.Close(c.UserContext(), id, request.Reason)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
result, err := employeecollectionquery.ProjectBill(bill)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return response.Success(c, result)
|
||
}
|
||
|
||
// CreateApplication 为本人可见账单创建核销申请。
|
||
// POST /api/admin/employee-collection-applications
|
||
func (h *EmployeeCollectionHandler) CreateApplication(c *fiber.Ctx) error {
|
||
request, err := bindApplicationRequest(c)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
result, err := h.application.Create(c.UserContext(), request)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
projected, err := employeecollectionquery.ProjectApplicationSubmit(employeecollectionquery.ApplicationSubmitProjection{
|
||
Application: result.Application, Attempt: result.Attempt, Allocations: result.Allocations,
|
||
Bills: result.Bills, InstanceID: result.InstanceID, InstanceStatus: result.InstanceStatus,
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return response.Success(c, projected)
|
||
}
|
||
|
||
// ResubmitApplication 修改并重提已驳回的核销申请。
|
||
// PUT /api/admin/employee-collection-applications/:id
|
||
func (h *EmployeeCollectionHandler) ResubmitApplication(c *fiber.Ctx) error {
|
||
id, err := pathID(c)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
request, err := bindApplicationRequest(c)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
result, err := h.application.Resubmit(c.UserContext(), id, request)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
projected, err := employeecollectionquery.ProjectApplicationSubmit(employeecollectionquery.ApplicationSubmitProjection{
|
||
Application: result.Application, Attempt: result.Attempt, Allocations: result.Allocations,
|
||
Bills: result.Bills, InstanceID: result.InstanceID, InstanceStatus: result.InstanceStatus,
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return response.Success(c, projected)
|
||
}
|
||
|
||
// ListApplications 分页查询核销申请。
|
||
// GET /api/admin/employee-collection-applications
|
||
func (h *EmployeeCollectionHandler) ListApplications(c *fiber.Ctx) error {
|
||
var request dto.EmployeeCollectionApplicationListRequest
|
||
if err := c.QueryParser(&request); err != nil {
|
||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||
}
|
||
result, err := h.applicationQuery.List(c.UserContext(), request)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return response.SuccessWithPagination(c, result.List, result.Total, result.Page, result.PageSize)
|
||
}
|
||
|
||
// GetApplication 查询核销申请详情,含分摊与全部审批尝试历史。
|
||
// GET /api/admin/employee-collection-applications/:id
|
||
func (h *EmployeeCollectionHandler) GetApplication(c *fiber.Ctx) error {
|
||
id, err := pathID(c)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
result, err := h.applicationQuery.Detail(c.UserContext(), id)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return response.Success(c, result)
|
||
}
|
||
|
||
// bindApplicationRequest 绑定并转换创建或重提核销申请请求。
|
||
// 付款时间按带时区的 RFC3339 解析;其余边界与业务校验由应用用例统一判断。
|
||
func bindApplicationRequest(c *fiber.Ctx) (employeecollectionapp.SubmitApplicationCommand, error) {
|
||
var request dto.SubmitEmployeeCollectionApplicationRequest
|
||
if err := c.BodyParser(&request); err != nil {
|
||
return employeecollectionapp.SubmitApplicationCommand{}, errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||
}
|
||
paidAt, err := time.Parse(time.RFC3339, strings.TrimSpace(request.PaidAt))
|
||
if err != nil {
|
||
return employeecollectionapp.SubmitApplicationCommand{}, errors.New(errors.CodeInvalidParam, "付款时间必须为带时区的 RFC3339 格式")
|
||
}
|
||
allocations := make([]employeecollectionapp.ApplicationAllocationCommand, 0, len(request.Allocations))
|
||
for _, item := range request.Allocations {
|
||
allocations = append(allocations, employeecollectionapp.ApplicationAllocationCommand{
|
||
BillID: item.BillID, Amount: item.Amount,
|
||
})
|
||
}
|
||
return employeecollectionapp.SubmitApplicationCommand{
|
||
PaymentMethodID: request.PaymentMethodID, PaidAmount: request.PaidAmount,
|
||
PayerName: request.PayerName, PaidAt: paidAt,
|
||
ExternalTransactionNo: request.ExternalTransactionNo,
|
||
PaymentVoucherKeys: request.PaymentVoucherKeys, Remark: request.Remark,
|
||
ActingReason: request.ActingReason, Allocations: allocations,
|
||
}, nil
|
||
}
|