Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
AUG26-008。
- 迁移 000214–000217:tb_shop 全局唯一且不可修改的随机分销码(含存量回填)、
tb_agent_distribution_registration 待审批注册记录、tb_withdrawal_qualification 资料版本、
tb_commission_withdrawal_request_attempt 审批尝试记录,以及提现申请的 latest_*/异常标记列;
不修改既有迁移,down 在存在本 Change 业务事实或新类型场景行时拒绝破坏性回滚。
- 公开接口 POST /api/c/v1/agent-distribution-registrations:无认证,复用既有短信验证码校验、
消费与限流;无效分销码、停用上级、验证码无效或已消费统一返回「分销码不可用」且不落库,
审批通过前不创建店铺、账号或钱包。
- 审批通过才在同一事务内建启用店铺、代理主账号、钱包、上级层级与业务员快照,驳回不建实体,
重复回调不重复建实体,提交后清理上级下级缓存。
- 提现资料资格按不可变版本保存,替换合同或法人身份证即新增版本并同事务失效旧有效版本;
超管作废原因必填;代理停用与店铺删除联动失效。
- 提现每次提交或重提新增不可变审批尝试记录并冻结金额;企业微信通过仅一次从冻结扣减、
保持状态 2 并写 paid_at(不使用状态 4),驳回/cancelled/deleted 仅一次释放,
通过后撤销不回滚、不重新冻结、只写正交异常标记;加锁顺序统一为申请→尝试→钱包。
- 本地人工终审对已关联审批实例的申请返回状态冲突,approval_instance_id 为空的存量申请保持既有行为,
不新增任何配置开关。
- 补齐审批业务类型注册点全集:业务类型与场景字段常量、场景 DTO 两处枚举与中文描述、
场景字段白名单/合法类型/中文名、数据库 CHECK、Worker 决策消费者与装配、审批审计资源映射,
以及三个新审计资源与 13 个审计动作;失败/拒绝审计改为必达。
- 新增后台路由与 OpenAPI:资格提交/查询/作废、提现申请/重提/详情、店铺详情返回只读分销码。
- 归档本 Change:主 Spec 新增 agent-distribution-withdrawal 能力(5 个 Requirement)。
验证(junhong_cmp_test + Redis DB 6,显式 DB_*,未重置整库):
- 迁移 up → version 217 且 dirty=false → down 3 → up 回 217,fixture 复核残留为 0。
- 受控状态机脚手架 227 项通过 / 0 项失败,覆盖 18 组场景(幂等与乱序回调、资金冻结/释放/重提、
退款回扣 × 在途提现并发、负向场景拒绝审计与 14 个动作码审计真实落库)。
- gofmt 空、go build/go vet 通过、gendocs 与工作区逐字节一致、context-health 通过、
openspec validate --strict 通过、doctor healthy;自动化测试按项目决策为 N/A。
运行期前置(未完成,非代码交付物):由超管经 PUT /api/admin/wecom/scenes/{business_type} 为
agent_distribution_approval、withdrawal_qualification_approval、commission_withdrawal_approval
配置启用场景与模板控件映射;未配置时相应提交失败关闭。
330 lines
13 KiB
Go
330 lines
13 KiB
Go
package routes
|
||
|
||
import (
|
||
"github.com/gofiber/fiber/v2"
|
||
|
||
"github.com/break/junhong_cmp_fiber/internal/bootstrap"
|
||
apphandler "github.com/break/junhong_cmp_fiber/internal/handler/app"
|
||
"github.com/break/junhong_cmp_fiber/internal/middleware"
|
||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||
"github.com/break/junhong_cmp_fiber/pkg/config"
|
||
"github.com/break/junhong_cmp_fiber/pkg/openapi"
|
||
)
|
||
|
||
// RegisterPersonalCustomerRoutes 注册个人客户路由
|
||
// 路由挂载在 /api/c/v1 下
|
||
//
|
||
// 重要:Fiber 的 Group.Use() 会在路由表中注册全局 USE 处理器,
|
||
// 匹配该前缀下的所有请求(不区分 Group 对象)。
|
||
// 因此公开路由必须在任何 Use() 调用之前注册,利用 Fiber 按注册顺序匹配的机制,
|
||
// 确保公开路由优先命中并直接返回,不会被后续的认证中间件拦截。
|
||
func RegisterPersonalCustomerRoutes(router fiber.Router, doc *openapi.Generator, basePath string, handlers *bootstrap.Handlers, personalAuthMiddleware *middleware.PersonalAuthMiddleware) {
|
||
authBasePath := "/auth"
|
||
|
||
// === 公开路由(无需认证)===
|
||
// 代理扫码注册必须在任何 Use() 调用之前注册,否则会被后续认证中间件拦截。
|
||
registerAgentDistributionPublicRoutes(router, handlers.AgentDistribution, doc, basePath)
|
||
|
||
Register(router, doc, basePath, "GET", "/wechat/appid", handlers.ClientWechat.GetAppID, RouteSpec{
|
||
Summary: "获取当前生效的公众号 AppID",
|
||
Description: "用于 C 端免登录场景获取当前生效微信配置中的公众号 AppID,响应仅返回 app_id 字段",
|
||
Tags: []string{"个人客户 - 微信"},
|
||
Auth: false,
|
||
Input: nil,
|
||
Output: &dto.WechatAppIDResponse{},
|
||
})
|
||
|
||
Register(router, doc, basePath, "GET", "/wechat/jssdk-config", handlers.ClientWechat.GetJSSDKConfig, RouteSpec{
|
||
Summary: "获取微信 JSSDK 签名配置",
|
||
Description: "前端调用 wx.config() 初始化微信 JS-SDK 时所需的签名参数,需传入当前页面完整 URL",
|
||
Tags: []string{"个人客户 - 微信"},
|
||
Auth: false,
|
||
Input: &dto.JSSDKConfigRequest{},
|
||
Output: &dto.JSSDKConfigResponse{},
|
||
})
|
||
|
||
Register(router, doc, basePath, "POST", authBasePath+"/verify-asset", handlers.ClientAuth.VerifyAsset, RouteSpec{
|
||
Summary: "资产验证",
|
||
Tags: []string{"个人客户 - 认证"},
|
||
Auth: false,
|
||
Input: &dto.VerifyAssetRequest{},
|
||
Output: &dto.VerifyAssetResponse{},
|
||
})
|
||
|
||
Register(router, doc, basePath, "POST", authBasePath+"/wechat-login", handlers.ClientAuth.WechatLogin, RouteSpec{
|
||
Summary: "公众号登录",
|
||
Tags: []string{"个人客户 - 认证"},
|
||
Auth: false,
|
||
Input: &dto.WechatLoginRequest{},
|
||
Output: &dto.WechatLoginResponse{},
|
||
})
|
||
|
||
Register(router, doc, basePath, "POST", authBasePath+"/miniapp-login", handlers.ClientAuth.MiniappLogin, RouteSpec{
|
||
Summary: "小程序登录",
|
||
Tags: []string{"个人客户 - 认证"},
|
||
Auth: false,
|
||
Input: &dto.MiniappLoginRequest{},
|
||
Output: &dto.WechatLoginResponse{},
|
||
})
|
||
|
||
Register(router, doc, basePath, "POST", authBasePath+"/send-code", handlers.ClientAuth.SendCode, RouteSpec{
|
||
Summary: "发送验证码",
|
||
Tags: []string{"个人客户 - 认证"},
|
||
Auth: false,
|
||
Input: &dto.ClientSendCodeRequest{},
|
||
Output: &dto.ClientSendCodeResponse{},
|
||
})
|
||
|
||
// 开发模式测试登录接口(仅 logging.development=true 时注册)
|
||
if cfg := config.Get(); cfg != nil && cfg.Logging.Development {
|
||
Register(router, doc, basePath, "POST", authBasePath+"/dev-login", handlers.ClientAuth.DevLogin, RouteSpec{
|
||
Summary: "开发环境测试登录",
|
||
Description: "⚠️ 仅开发模式可用。传入资产标识符(ICCID等),自动绑定测试客户并签发 JWT,用于接口测试。",
|
||
Tags: []string{"个人客户 - 认证"},
|
||
Auth: false,
|
||
Input: &dto.DevLoginRequest{},
|
||
Output: &dto.DevLoginResponse{},
|
||
})
|
||
}
|
||
|
||
// === 需要认证的 auth 路由 ===
|
||
authProtectedGroup := router.Group(authBasePath)
|
||
authProtectedGroup.Use(personalAuthMiddleware.Authenticate())
|
||
|
||
Register(authProtectedGroup, doc, basePath+authBasePath, "POST", "/bind-phone", handlers.ClientAuth.BindPhone, RouteSpec{
|
||
Summary: "绑定手机号",
|
||
Tags: []string{"个人客户 - 认证"},
|
||
Auth: true,
|
||
Input: &dto.BindPhoneRequest{},
|
||
Output: &dto.BindPhoneResponse{},
|
||
})
|
||
|
||
Register(authProtectedGroup, doc, basePath+authBasePath, "POST", "/change-phone", handlers.ClientAuth.ChangePhone, RouteSpec{
|
||
Summary: "更换手机号",
|
||
Tags: []string{"个人客户 - 认证"},
|
||
Auth: true,
|
||
Input: &dto.ChangePhoneRequest{},
|
||
Output: &dto.ChangePhoneResponse{},
|
||
})
|
||
|
||
Register(authProtectedGroup, doc, basePath+authBasePath, "POST", "/logout", handlers.ClientAuth.Logout, RouteSpec{
|
||
Summary: "退出登录",
|
||
Tags: []string{"个人客户 - 认证"},
|
||
Auth: true,
|
||
Input: nil,
|
||
Output: &dto.LogoutResponse{},
|
||
})
|
||
|
||
// 需要认证的路由
|
||
authGroup := router.Group("")
|
||
authGroup.Use(personalAuthMiddleware.Authenticate())
|
||
if handlers.ClientNotification != nil {
|
||
registerPersonalNotificationRoutes(authGroup, handlers.ClientNotification, doc, basePath)
|
||
}
|
||
|
||
// 获取个人资料
|
||
Register(authGroup, doc, basePath, "GET", "/profile", handlers.PersonalCustomer.GetProfile, RouteSpec{
|
||
Summary: "获取个人资料",
|
||
Description: "获取当前登录客户的个人资料",
|
||
Tags: []string{"个人客户 - 账户"},
|
||
Auth: true,
|
||
Input: nil,
|
||
Output: &apphandler.PersonalCustomerDTO{},
|
||
})
|
||
|
||
// 更新个人资料
|
||
Register(authGroup, doc, basePath, "PUT", "/profile", handlers.PersonalCustomer.UpdateProfile, RouteSpec{
|
||
Summary: "更新个人资料",
|
||
Description: "更新当前登录客户的昵称和头像",
|
||
Tags: []string{"个人客户 - 账户"},
|
||
Auth: true,
|
||
Input: &apphandler.UpdateProfileRequest{},
|
||
Output: nil,
|
||
})
|
||
|
||
Register(authGroup, doc, basePath, "GET", "/asset/info", handlers.ClientAsset.GetAssetInfo, RouteSpec{
|
||
Summary: "资产信息",
|
||
Tags: []string{"个人客户 - 资产"},
|
||
Auth: true,
|
||
Input: &dto.AssetInfoRequest{},
|
||
Output: &dto.AssetInfoResponse{},
|
||
})
|
||
|
||
Register(authGroup, doc, basePath, "GET", "/asset/packages", handlers.ClientAsset.GetAvailablePackages, RouteSpec{
|
||
Summary: "资产可购套餐列表",
|
||
Tags: []string{"个人客户 - 资产"},
|
||
Auth: true,
|
||
Input: &dto.AssetPackageListRequest{},
|
||
Output: &dto.AssetPackageListResponse{},
|
||
})
|
||
|
||
Register(authGroup, doc, basePath, "GET", "/asset/package-history", handlers.ClientAsset.GetPackageHistory, RouteSpec{
|
||
Summary: "资产套餐历史",
|
||
Description: "查询客户已绑定资产当前世代的套餐历史层级。每个主套餐及其关联加油包占一个分页名额,total 为筛选后的顶层项数量;status 与 package_type 必须由同一使用记录联合命中,命中任一成员即返回完整关系组。关联主套餐物理缺失返回异常独立项,存在但不可展示的关联主套餐返回统一读取错误。",
|
||
Tags: []string{"个人客户 - 资产"},
|
||
Auth: true,
|
||
Input: &dto.AssetPackageHistoryRequest{},
|
||
Output: &dto.AssetPackageHistoryResponse{},
|
||
})
|
||
|
||
Register(authGroup, doc, basePath, "POST", "/asset/refresh", handlers.ClientAsset.RefreshAsset, RouteSpec{
|
||
Summary: "资产刷新",
|
||
Tags: []string{"个人客户 - 资产"},
|
||
Auth: true,
|
||
Input: &dto.AssetRefreshRequest{},
|
||
Output: &dto.AssetRefreshResponse{},
|
||
})
|
||
|
||
Register(authGroup, doc, basePath, "GET", "/wallet/detail", handlers.ClientWallet.GetWalletDetail, RouteSpec{
|
||
Summary: "钱包详情",
|
||
Tags: []string{"个人客户 - 钱包"},
|
||
Auth: true,
|
||
Input: &dto.WalletDetailRequest{},
|
||
Output: &dto.WalletDetailResponse{},
|
||
})
|
||
|
||
Register(authGroup, doc, basePath, "GET", "/wallet/transactions", handlers.ClientWallet.GetWalletTransactions, RouteSpec{
|
||
Summary: "钱包流水列表",
|
||
Tags: []string{"个人客户 - 钱包"},
|
||
Auth: true,
|
||
Input: &dto.WalletTransactionListRequest{},
|
||
Output: &dto.WalletTransactionListResponse{},
|
||
})
|
||
|
||
Register(authGroup, doc, basePath, "GET", "/wallet/recharge-check", handlers.ClientWallet.GetRechargeCheck, RouteSpec{
|
||
Summary: "充值前校验",
|
||
Tags: []string{"个人客户 - 钱包"},
|
||
Auth: true,
|
||
Input: &dto.ClientRechargeCheckRequest{},
|
||
Output: &dto.ClientRechargeCheckResponse{},
|
||
})
|
||
|
||
Register(authGroup, doc, basePath, "POST", "/wallet/recharge", handlers.ClientWallet.CreateRecharge, RouteSpec{
|
||
Summary: "创建充值订单",
|
||
Tags: []string{"个人客户 - 钱包"},
|
||
Auth: true,
|
||
Input: &dto.ClientCreateRechargeRequest{},
|
||
Output: &dto.ClientRechargeResponse{},
|
||
})
|
||
|
||
Register(authGroup, doc, basePath, "GET", "/wallet/recharges", handlers.ClientWallet.GetRechargeList, RouteSpec{
|
||
Summary: "充值记录列表",
|
||
Tags: []string{"个人客户 - 钱包"},
|
||
Auth: true,
|
||
Input: &dto.ClientRechargeListRequest{},
|
||
Output: &dto.ClientRechargeListResponse{},
|
||
})
|
||
|
||
Register(authGroup, doc, basePath, "GET", "/recharge-orders", handlers.ClientRechargeOrder.ListRechargeOrders, RouteSpec{
|
||
Summary: "充值订单列表",
|
||
Tags: []string{"个人客户 - 充值订单"},
|
||
Auth: true,
|
||
Input: &dto.ClientRechargeOrderListRequest{},
|
||
Output: nil,
|
||
})
|
||
|
||
Register(authGroup, doc, basePath, "GET", "/recharge-orders/:id", handlers.ClientRechargeOrder.GetRechargeOrderDetail, RouteSpec{
|
||
Summary: "充值订单详情",
|
||
Tags: []string{"个人客户 - 充值订单"},
|
||
Auth: true,
|
||
Input: &dto.IDReq{},
|
||
Output: nil,
|
||
})
|
||
|
||
Register(authGroup, doc, basePath, "POST", "/orders/create", handlers.ClientOrder.CreateOrder, RouteSpec{
|
||
Summary: "创建订单",
|
||
Tags: []string{"个人客户 - 订单"},
|
||
Auth: true,
|
||
Input: &dto.ClientCreateOrderRequest{},
|
||
Output: &dto.ClientCreateOrderResponse{},
|
||
})
|
||
|
||
Register(authGroup, doc, basePath, "GET", "/orders", handlers.ClientOrder.ListOrders, RouteSpec{
|
||
Summary: "订单列表",
|
||
Tags: []string{"个人客户 - 订单"},
|
||
Auth: true,
|
||
Input: &dto.ClientOrderListRequest{},
|
||
Output: &dto.ClientOrderListResponse{},
|
||
})
|
||
|
||
Register(authGroup, doc, basePath, "GET", "/orders/:id", handlers.ClientOrder.GetOrderDetail, RouteSpec{
|
||
Summary: "订单详情",
|
||
Tags: []string{"个人客户 - 订单"},
|
||
Auth: true,
|
||
Input: &dto.IDReq{},
|
||
Output: &dto.ClientOrderDetailResponse{},
|
||
})
|
||
|
||
Register(authGroup, doc, basePath, "POST", "/orders/:id/pay", handlers.ClientOrder.PayOrder, RouteSpec{
|
||
Summary: "订单支付",
|
||
Tags: []string{"个人客户 - 订单"},
|
||
Auth: true,
|
||
Input: &dto.ClientPayOrderParams{},
|
||
Output: &dto.ClientPayOrderResponse{},
|
||
})
|
||
|
||
Register(authGroup, doc, basePath, "GET", "/exchange/pending", handlers.ClientExchange.GetPending, RouteSpec{
|
||
Summary: "查询待处理换货单",
|
||
Tags: []string{"个人客户 - 换货"},
|
||
Auth: true,
|
||
Input: &dto.ClientExchangePendingRequest{},
|
||
Output: &dto.ClientExchangePendingResponse{},
|
||
})
|
||
|
||
Register(authGroup, doc, basePath, "POST", "/exchange/:id/shipping-info", handlers.ClientExchange.SubmitShippingInfo, RouteSpec{
|
||
Summary: "提交收货信息",
|
||
Tags: []string{"个人客户 - 换货"},
|
||
Auth: true,
|
||
Input: &dto.ClientShippingInfoParams{},
|
||
Output: nil,
|
||
})
|
||
|
||
Register(authGroup, doc, basePath, "GET", "/realname/link", handlers.ClientRealname.GetRealnameLink, RouteSpec{
|
||
Summary: "获取实名认证链接",
|
||
Tags: []string{"个人客户 - 实名"},
|
||
Auth: true,
|
||
Input: &dto.RealnimeLinkRequest{},
|
||
Output: &dto.RealnimeLinkResponse{},
|
||
})
|
||
|
||
Register(authGroup, doc, basePath, "GET", "/device/cards", handlers.ClientDevice.GetDeviceCards, RouteSpec{
|
||
Summary: "获取设备卡列表",
|
||
Tags: []string{"个人客户 - 设备"},
|
||
Auth: true,
|
||
Input: &dto.DeviceCardListRequest{},
|
||
Output: &dto.DeviceCardListResponse{},
|
||
})
|
||
|
||
Register(authGroup, doc, basePath, "POST", "/device/reboot", handlers.ClientDevice.RebootDevice, RouteSpec{
|
||
Summary: "设备重启",
|
||
Tags: []string{"个人客户 - 设备"},
|
||
Auth: true,
|
||
Input: &dto.DeviceRebootRequest{},
|
||
Output: &dto.DeviceOperationResponse{},
|
||
})
|
||
|
||
Register(authGroup, doc, basePath, "POST", "/device/factory-reset", handlers.ClientDevice.FactoryResetDevice, RouteSpec{
|
||
Summary: "恢复出厂设置",
|
||
Tags: []string{"个人客户 - 设备"},
|
||
Auth: true,
|
||
Input: &dto.DeviceFactoryResetRequest{},
|
||
Output: &dto.DeviceOperationResponse{},
|
||
})
|
||
|
||
Register(authGroup, doc, basePath, "POST", "/device/wifi", handlers.ClientDevice.SetWiFi, RouteSpec{
|
||
Summary: "设备WiFi配置",
|
||
Tags: []string{"个人客户 - 设备"},
|
||
Auth: true,
|
||
Input: &dto.DeviceWifiRequest{},
|
||
Output: &dto.DeviceOperationResponse{},
|
||
})
|
||
|
||
Register(authGroup, doc, basePath, "POST", "/device/switch-card", handlers.ClientDevice.SwitchCard, RouteSpec{
|
||
Summary: "设备切卡",
|
||
Tags: []string{"个人客户 - 设备"},
|
||
Auth: true,
|
||
Input: &dto.DeviceSwitchCardRequest{},
|
||
Output: &dto.DeviceSwitchCardResponse{},
|
||
})
|
||
}
|