323 lines
12 KiB
Go
323 lines
12 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/constants"
|
||
"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)
|
||
}
|
||
|
||
// RecoverApplication 恢复核销申请的原审批提交事实。
|
||
// POST /api/admin/employee-collection-applications/:id/recover-approval
|
||
func (h *EmployeeCollectionHandler) RecoverApplication(c *fiber.Ctx) error {
|
||
id, err := pathID(c)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
result, err := h.application.RecoverApproval(c.UserContext(), id)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return response.Success(c, &dto.EmployeeCollectionApplicationRecoveryResponse{
|
||
ApplicationID: result.ApplicationID, ApprovalInstanceID: result.ApprovalInstanceID,
|
||
Action: result.Action, Result: result.Result, SubmissionStatus: result.SubmissionStatus,
|
||
SubmissionStatusName: constants.GetWeComSubmissionStatusName(result.SubmissionStatus), Recoverable: result.Recoverable,
|
||
LastRecoveryAt: formatRecoveryTime(result.LastRecoveryAt),
|
||
})
|
||
}
|
||
|
||
func formatRecoveryTime(value *time.Time) string {
|
||
if value == nil {
|
||
return ""
|
||
}
|
||
return value.UTC().Format(time.RFC3339)
|
||
}
|
||
|
||
// 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
|
||
}
|