feat: 客户端接口数据模型基础准备
- 新增资产状态、订单来源、操作人类型、实名链接类型常量 - 8个模型新增字段(asset_status/generation/source/retail_price等) - 数据库迁移000082:7张表15+字段,含存量retail_price回填 - BUG-1修复:代理零售价渠道隔离,cost_price分配锁定 - BUG-2修复:一次性佣金仅客户端订单触发 - BUG-4修复:充值回调Store操作纳入事务 - 新增资产手动停用接口(PATCH /iot-cards/:id/deactivate、/devices/:id/deactivate) - Carrier管理新增实名链接配置 - 后台订单generation写时快照 - BatchUpdatePricing支持retail_price调价目标 - 清理全部H5旧接口和个人客户旧登录方法
This commit is contained in:
@@ -5,7 +5,6 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/app"
|
||||
authHandler "github.com/break/junhong_cmp_fiber/internal/handler/auth"
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/callback"
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/h5"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
@@ -21,14 +20,12 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
|
||||
Shop: admin.NewShopHandler(svc.Shop),
|
||||
ShopRole: admin.NewShopRoleHandler(svc.Shop),
|
||||
AdminAuth: admin.NewAuthHandler(svc.Auth, validate),
|
||||
H5Auth: h5.NewAuthHandler(svc.Auth, validate),
|
||||
ShopCommission: admin.NewShopCommissionHandler(svc.ShopCommission),
|
||||
CommissionWithdrawal: admin.NewCommissionWithdrawalHandler(svc.CommissionWithdrawal),
|
||||
CommissionWithdrawalSetting: admin.NewCommissionWithdrawalSettingHandler(svc.CommissionWithdrawalSetting),
|
||||
Enterprise: admin.NewEnterpriseHandler(svc.Enterprise),
|
||||
EnterpriseCard: admin.NewEnterpriseCardHandler(svc.EnterpriseCard),
|
||||
EnterpriseDevice: admin.NewEnterpriseDeviceHandler(svc.EnterpriseDevice),
|
||||
EnterpriseDeviceH5: h5.NewEnterpriseDeviceHandler(svc.EnterpriseDevice),
|
||||
Authorization: admin.NewAuthorizationHandler(svc.Authorization),
|
||||
MyCommission: admin.NewMyCommissionHandler(svc.MyCommission),
|
||||
IotCard: admin.NewIotCardHandler(svc.IotCard),
|
||||
@@ -41,13 +38,10 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
|
||||
PackageSeries: admin.NewPackageSeriesHandler(svc.PackageSeries),
|
||||
Package: admin.NewPackageHandler(svc.Package),
|
||||
PackageUsage: admin.NewPackageUsageHandler(svc.PackageDailyRecord),
|
||||
H5PackageUsage: h5.NewPackageUsageHandler(deps.DB, svc.PackageCustomerView),
|
||||
ShopPackageBatchAllocation: admin.NewShopPackageBatchAllocationHandler(svc.ShopPackageBatchAllocation),
|
||||
ShopPackageBatchPricing: admin.NewShopPackageBatchPricingHandler(svc.ShopPackageBatchPricing),
|
||||
ShopSeriesGrant: admin.NewShopSeriesGrantHandler(svc.ShopSeriesGrant),
|
||||
AdminOrder: admin.NewOrderHandler(svc.Order, validate),
|
||||
H5Order: h5.NewOrderHandler(svc.Order),
|
||||
H5Recharge: h5.NewRechargeHandler(svc.Recharge),
|
||||
PaymentCallback: callback.NewPaymentHandler(svc.Order, svc.Recharge, svc.AgentRecharge, deps.WechatPayment),
|
||||
PollingConfig: admin.NewPollingConfigHandler(svc.PollingConfig),
|
||||
PollingConcurrency: admin.NewPollingConcurrencyHandler(svc.PollingConcurrency),
|
||||
@@ -56,6 +50,7 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
|
||||
PollingCleanup: admin.NewPollingCleanupHandler(svc.PollingCleanup),
|
||||
PollingManualTrigger: admin.NewPollingManualTriggerHandler(svc.PollingManualTrigger),
|
||||
Asset: admin.NewAssetHandler(svc.Asset, svc.Device, svc.StopResumeService),
|
||||
AssetLifecycle: admin.NewAssetLifecycleHandler(svc.AssetLifecycle),
|
||||
AssetWallet: admin.NewAssetWalletHandler(svc.AssetWallet),
|
||||
WechatConfig: admin.NewWechatConfigHandler(svc.WechatConfig),
|
||||
AgentRecharge: admin.NewAgentRechargeHandler(svc.AgentRecharge),
|
||||
|
||||
@@ -32,13 +32,9 @@ func initMiddlewares(deps *Dependencies, stores *stores) *Middlewares {
|
||||
// 创建后台认证中间件(传入 ShopStore 以支持预计算下级店铺 ID)
|
||||
adminAuthMiddleware := createAdminAuthMiddleware(tokenManager, stores.Shop)
|
||||
|
||||
// 创建H5认证中间件(传入 ShopStore 以支持预计算下级店铺 ID)
|
||||
h5AuthMiddleware := createH5AuthMiddleware(tokenManager, stores.Shop)
|
||||
|
||||
return &Middlewares{
|
||||
PersonalAuth: personalAuthMiddleware,
|
||||
AdminAuth: adminAuthMiddleware,
|
||||
H5Auth: h5AuthMiddleware,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,29 +64,3 @@ func createAdminAuthMiddleware(tokenManager *pkgauth.TokenManager, shopStore pkg
|
||||
ShopStore: shopStore,
|
||||
})
|
||||
}
|
||||
|
||||
func createH5AuthMiddleware(tokenManager *pkgauth.TokenManager, shopStore pkgmiddleware.AuthShopStoreInterface) fiber.Handler {
|
||||
return pkgmiddleware.Auth(pkgmiddleware.AuthConfig{
|
||||
TokenValidator: func(token string) (*pkgmiddleware.UserContextInfo, error) {
|
||||
tokenInfo, err := tokenManager.ValidateAccessToken(context.Background(), token)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.CodeInvalidToken, "认证令牌无效或已过期")
|
||||
}
|
||||
|
||||
// 检查用户类型:H5 允许 Agent(3), Enterprise(4)
|
||||
if tokenInfo.UserType != constants.UserTypeAgent &&
|
||||
tokenInfo.UserType != constants.UserTypeEnterprise {
|
||||
return nil, errors.New(errors.CodeForbidden, "权限不足")
|
||||
}
|
||||
|
||||
return &pkgmiddleware.UserContextInfo{
|
||||
UserID: tokenInfo.UserID,
|
||||
UserType: tokenInfo.UserType,
|
||||
ShopID: tokenInfo.ShopID,
|
||||
EnterpriseID: tokenInfo.EnterpriseID,
|
||||
}, nil
|
||||
},
|
||||
SkipPaths: []string{"/api/h5/login", "/api/h5/refresh-token"},
|
||||
ShopStore: shopStore,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ type services struct {
|
||||
PollingCleanup *pollingSvc.CleanupService
|
||||
PollingManualTrigger *pollingSvc.ManualTriggerService
|
||||
Asset *assetSvc.Service
|
||||
AssetLifecycle *assetSvc.LifecycleService
|
||||
AssetWallet *assetWalletSvc.Service
|
||||
StopResumeService *iotCardSvc.StopResumeService
|
||||
WechatConfig *wechatConfigSvc.Service
|
||||
@@ -158,6 +159,7 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
PollingCleanup: pollingSvc.NewCleanupService(s.DataCleanupConfig, s.DataCleanupLog, deps.Logger),
|
||||
PollingManualTrigger: pollingSvc.NewManualTriggerService(s.PollingManualTriggerLog, s.IotCard, deps.Redis, deps.Logger),
|
||||
Asset: assetSvc.New(deps.DB, s.Device, s.IotCard, s.PackageUsage, s.Package, s.PackageSeries, s.DeviceSimBinding, s.Shop, deps.Redis, iotCard),
|
||||
AssetLifecycle: assetSvc.NewLifecycleService(deps.DB, s.IotCard, s.Device),
|
||||
AssetWallet: assetWalletSvc.New(s.AssetWallet, s.AssetWalletTransaction),
|
||||
StopResumeService: iotCardSvc.NewStopResumeService(deps.DB, deps.Redis, s.IotCard, s.DeviceSimBinding, deps.GatewayClient, deps.Logger),
|
||||
WechatConfig: wechatConfig,
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/app"
|
||||
authHandler "github.com/break/junhong_cmp_fiber/internal/handler/auth"
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/callback"
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/h5"
|
||||
"github.com/break/junhong_cmp_fiber/internal/middleware"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
@@ -19,14 +18,12 @@ type Handlers struct {
|
||||
Shop *admin.ShopHandler
|
||||
ShopRole *admin.ShopRoleHandler
|
||||
AdminAuth *admin.AuthHandler
|
||||
H5Auth *h5.AuthHandler
|
||||
ShopCommission *admin.ShopCommissionHandler
|
||||
CommissionWithdrawal *admin.CommissionWithdrawalHandler
|
||||
CommissionWithdrawalSetting *admin.CommissionWithdrawalSettingHandler
|
||||
Enterprise *admin.EnterpriseHandler
|
||||
EnterpriseCard *admin.EnterpriseCardHandler
|
||||
EnterpriseDevice *admin.EnterpriseDeviceHandler
|
||||
EnterpriseDeviceH5 *h5.EnterpriseDeviceHandler
|
||||
Authorization *admin.AuthorizationHandler
|
||||
MyCommission *admin.MyCommissionHandler
|
||||
IotCard *admin.IotCardHandler
|
||||
@@ -39,13 +36,10 @@ type Handlers struct {
|
||||
PackageSeries *admin.PackageSeriesHandler
|
||||
Package *admin.PackageHandler
|
||||
PackageUsage *admin.PackageUsageHandler
|
||||
H5PackageUsage *h5.PackageUsageHandler
|
||||
ShopPackageBatchAllocation *admin.ShopPackageBatchAllocationHandler
|
||||
ShopPackageBatchPricing *admin.ShopPackageBatchPricingHandler
|
||||
ShopSeriesGrant *admin.ShopSeriesGrantHandler
|
||||
AdminOrder *admin.OrderHandler
|
||||
H5Order *h5.OrderHandler
|
||||
H5Recharge *h5.RechargeHandler
|
||||
PaymentCallback *callback.PaymentHandler
|
||||
PollingConfig *admin.PollingConfigHandler
|
||||
PollingConcurrency *admin.PollingConcurrencyHandler
|
||||
@@ -54,6 +48,7 @@ type Handlers struct {
|
||||
PollingCleanup *admin.PollingCleanupHandler
|
||||
PollingManualTrigger *admin.PollingManualTriggerHandler
|
||||
Asset *admin.AssetHandler
|
||||
AssetLifecycle *admin.AssetLifecycleHandler
|
||||
AssetWallet *admin.AssetWalletHandler
|
||||
WechatConfig *admin.WechatConfigHandler
|
||||
AgentRecharge *admin.AgentRechargeHandler
|
||||
@@ -64,6 +59,5 @@ type Handlers struct {
|
||||
type Middlewares struct {
|
||||
PersonalAuth *middleware.PersonalAuthMiddleware
|
||||
AdminAuth func(*fiber.Ctx) error
|
||||
H5Auth func(*fiber.Ctx) error
|
||||
// TODO: 新增 Middleware 在此添加字段
|
||||
}
|
||||
|
||||
59
internal/handler/admin/asset_lifecycle.go
Normal file
59
internal/handler/admin/asset_lifecycle.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
)
|
||||
|
||||
// AssetLifecycleService 资产生命周期服务接口
|
||||
type AssetLifecycleService interface {
|
||||
// DeactivateIotCard 停用 IoT 卡
|
||||
DeactivateIotCard(ctx context.Context, id uint) error
|
||||
// DeactivateDevice 停用设备
|
||||
DeactivateDevice(ctx context.Context, id uint) error
|
||||
}
|
||||
|
||||
// AssetLifecycleHandler 资产生命周期处理器
|
||||
type AssetLifecycleHandler struct {
|
||||
service AssetLifecycleService
|
||||
}
|
||||
|
||||
// NewAssetLifecycleHandler 创建资产生命周期处理器
|
||||
func NewAssetLifecycleHandler(service AssetLifecycleService) *AssetLifecycleHandler {
|
||||
return &AssetLifecycleHandler{service: service}
|
||||
}
|
||||
|
||||
// DeactivateIotCard 手动停用 IoT 卡
|
||||
// PATCH /api/admin/iot-cards/:id/deactivate
|
||||
func (h *AssetLifecycleHandler) DeactivateIotCard(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的卡ID")
|
||||
}
|
||||
|
||||
if err := h.service.DeactivateIotCard(c.UserContext(), uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, nil)
|
||||
}
|
||||
|
||||
// DeactivateDevice 手动停用设备
|
||||
// PATCH /api/admin/devices/:id/deactivate
|
||||
func (h *AssetLifecycleHandler) DeactivateDevice(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的设备ID")
|
||||
}
|
||||
|
||||
if err := h.service.DeactivateDevice(c.UserContext(), uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, nil)
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/personal_customer"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
@@ -25,45 +24,6 @@ func NewPersonalCustomerHandler(service *personal_customer.Service, logger *zap.
|
||||
}
|
||||
}
|
||||
|
||||
// SendCodeRequest 发送验证码请求
|
||||
type SendCodeRequest struct {
|
||||
Phone string `json:"phone" validate:"required,len=11"` // 手机号(11位)
|
||||
}
|
||||
|
||||
// SendCode 发送验证码
|
||||
// POST /api/c/v1/login/send-code
|
||||
func (h *PersonalCustomerHandler) SendCode(c *fiber.Ctx) error {
|
||||
var req SendCodeRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
// 发送验证码
|
||||
if err := h.service.SendVerificationCode(c.Context(), req.Phone); err != nil {
|
||||
h.logger.Error("发送验证码失败",
|
||||
zap.String("phone", req.Phone),
|
||||
zap.Error(err),
|
||||
)
|
||||
return errors.Wrap(errors.CodeInternalError, err, "发送验证码失败")
|
||||
}
|
||||
|
||||
return response.Success(c, fiber.Map{
|
||||
"message": "验证码已发送",
|
||||
})
|
||||
}
|
||||
|
||||
// LoginRequest 登录请求
|
||||
type LoginRequest struct {
|
||||
Phone string `json:"phone" validate:"required,len=11"` // 手机号(11位)
|
||||
Code string `json:"code" validate:"required,len=6"` // 验证码(6位)
|
||||
}
|
||||
|
||||
// LoginResponse 登录响应
|
||||
type LoginResponse struct {
|
||||
Token string `json:"token"` // 访问令牌
|
||||
Customer *PersonalCustomerDTO `json:"customer"` // 客户信息
|
||||
}
|
||||
|
||||
// PersonalCustomerDTO 个人客户 DTO
|
||||
type PersonalCustomerDTO struct {
|
||||
ID uint `json:"id"`
|
||||
@@ -74,87 +34,6 @@ type PersonalCustomerDTO struct {
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
// Login 登录(手机号 + 验证码)
|
||||
// POST /api/c/v1/login
|
||||
func (h *PersonalCustomerHandler) Login(c *fiber.Ctx) error {
|
||||
var req LoginRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
// 登录
|
||||
token, customer, err := h.service.LoginByPhone(c.Context(), req.Phone, req.Code)
|
||||
if err != nil {
|
||||
h.logger.Error("登录失败",
|
||||
zap.String("phone", req.Phone),
|
||||
zap.Error(err),
|
||||
)
|
||||
return errors.Wrap(errors.CodeInternalError, err, "登录失败")
|
||||
}
|
||||
|
||||
// 构造响应
|
||||
// 注意:Phone 字段已从 PersonalCustomer 模型移除,需要从 PersonalCustomerPhone 表查询
|
||||
resp := &LoginResponse{
|
||||
Token: token,
|
||||
Customer: &PersonalCustomerDTO{
|
||||
ID: customer.ID,
|
||||
Phone: req.Phone, // 使用请求中的手机号(临时方案)
|
||||
Nickname: customer.Nickname,
|
||||
AvatarURL: customer.AvatarURL,
|
||||
WxOpenID: customer.WxOpenID,
|
||||
Status: customer.Status,
|
||||
},
|
||||
}
|
||||
|
||||
return response.Success(c, resp)
|
||||
}
|
||||
|
||||
// WechatOAuthLogin 微信 OAuth 登录
|
||||
// POST /api/c/v1/wechat/auth
|
||||
func (h *PersonalCustomerHandler) WechatOAuthLogin(c *fiber.Ctx) error {
|
||||
var req dto.WechatOAuthRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
result, err := h.service.WechatOAuthLogin(c.Context(), req.Code)
|
||||
if err != nil {
|
||||
h.logger.Error("微信 OAuth 登录失败",
|
||||
zap.String("code", req.Code),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, result)
|
||||
}
|
||||
|
||||
// BindWechat 绑定微信
|
||||
// POST /api/c/v1/bind-wechat
|
||||
func (h *PersonalCustomerHandler) BindWechat(c *fiber.Ctx) error {
|
||||
var req dto.WechatOAuthRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
customerID, ok := c.Locals("customer_id").(uint)
|
||||
if !ok {
|
||||
return errors.New(errors.CodeUnauthorized, "未找到客户信息")
|
||||
}
|
||||
|
||||
if err := h.service.BindWechatWithCode(c.Context(), customerID, req.Code); err != nil {
|
||||
h.logger.Error("绑定微信失败",
|
||||
zap.Uint("customer_id", customerID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, fiber.Map{
|
||||
"message": "绑定成功",
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateProfileRequest 更新个人资料请求
|
||||
type UpdateProfileRequest struct {
|
||||
Nickname string `json:"nickname"` // 昵称
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
package h5
|
||||
|
||||
import (
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/auth"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/logger"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// AuthHandler H5认证处理器
|
||||
type AuthHandler struct {
|
||||
authService *auth.Service
|
||||
validator *validator.Validate
|
||||
}
|
||||
|
||||
// NewAuthHandler 创建H5认证处理器
|
||||
func NewAuthHandler(authService *auth.Service, validator *validator.Validate) *AuthHandler {
|
||||
return &AuthHandler{
|
||||
authService: authService,
|
||||
validator: validator,
|
||||
}
|
||||
}
|
||||
|
||||
// Login H5登录
|
||||
func (h *AuthHandler) Login(c *fiber.Ctx) error {
|
||||
var req dto.LoginRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
logger.GetAppLogger().Warn("参数验证失败",
|
||||
zap.String("path", c.Path()),
|
||||
zap.String("method", c.Method()),
|
||||
zap.Error(err),
|
||||
)
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
clientIP := c.IP()
|
||||
ctx := c.UserContext()
|
||||
|
||||
resp, err := h.authService.Login(ctx, &req, clientIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, resp)
|
||||
}
|
||||
|
||||
// Logout H5登出
|
||||
func (h *AuthHandler) Logout(c *fiber.Ctx) error {
|
||||
auth := c.Get("Authorization")
|
||||
accessToken := ""
|
||||
if len(auth) > 7 && auth[:7] == "Bearer " {
|
||||
accessToken = auth[7:]
|
||||
}
|
||||
|
||||
refreshToken := ""
|
||||
var req dto.RefreshTokenRequest
|
||||
if err := c.BodyParser(&req); err == nil {
|
||||
refreshToken = req.RefreshToken
|
||||
}
|
||||
|
||||
ctx := c.UserContext()
|
||||
|
||||
if err := h.authService.Logout(ctx, accessToken, refreshToken); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, nil)
|
||||
}
|
||||
|
||||
// RefreshToken 刷新访问令牌
|
||||
func (h *AuthHandler) RefreshToken(c *fiber.Ctx) error {
|
||||
var req dto.RefreshTokenRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
logger.GetAppLogger().Warn("参数验证失败",
|
||||
zap.String("path", c.Path()),
|
||||
zap.String("method", c.Method()),
|
||||
zap.Error(err),
|
||||
)
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
ctx := c.UserContext()
|
||||
|
||||
newAccessToken, err := h.authService.RefreshToken(ctx, req.RefreshToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp := &dto.RefreshTokenResponse{
|
||||
AccessToken: newAccessToken,
|
||||
ExpiresIn: 86400,
|
||||
}
|
||||
|
||||
return response.Success(c, resp)
|
||||
}
|
||||
|
||||
// GetMe 获取当前用户信息
|
||||
func (h *AuthHandler) GetMe(c *fiber.Ctx) error {
|
||||
userID := middleware.GetUserIDFromContext(c.UserContext())
|
||||
if userID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
ctx := c.UserContext()
|
||||
|
||||
userInfo, permissions, err := h.authService.GetCurrentUser(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"user": userInfo,
|
||||
"permissions": permissions,
|
||||
}
|
||||
|
||||
return response.Success(c, data)
|
||||
}
|
||||
|
||||
// ChangePassword 修改密码
|
||||
func (h *AuthHandler) ChangePassword(c *fiber.Ctx) error {
|
||||
userID := middleware.GetUserIDFromContext(c.UserContext())
|
||||
if userID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
var req dto.ChangePasswordRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
logger.GetAppLogger().Warn("参数验证失败",
|
||||
zap.String("path", c.Path()),
|
||||
zap.String("method", c.Method()),
|
||||
zap.Error(err),
|
||||
)
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
ctx := c.UserContext()
|
||||
|
||||
if err := h.authService.ChangePassword(ctx, userID, req.OldPassword, req.NewPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, nil)
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package h5
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
enterpriseDeviceService "github.com/break/junhong_cmp_fiber/internal/service/enterprise_device"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
)
|
||||
|
||||
type EnterpriseDeviceHandler struct {
|
||||
service *enterpriseDeviceService.Service
|
||||
}
|
||||
|
||||
func NewEnterpriseDeviceHandler(service *enterpriseDeviceService.Service) *EnterpriseDeviceHandler {
|
||||
return &EnterpriseDeviceHandler{service: service}
|
||||
}
|
||||
|
||||
func (h *EnterpriseDeviceHandler) ListDevices(c *fiber.Ctx) error {
|
||||
var req dto.H5EnterpriseDeviceListReq
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
serviceReq := &dto.EnterpriseDeviceListReq{
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
VirtualNo: req.VirtualNo,
|
||||
}
|
||||
|
||||
result, err := h.service.ListDevicesForEnterprise(c.UserContext(), serviceReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.SuccessWithPagination(c, result.List, result.Total, req.Page, req.PageSize)
|
||||
}
|
||||
|
||||
func (h *EnterpriseDeviceHandler) GetDeviceDetail(c *fiber.Ctx) error {
|
||||
deviceIDStr := c.Params("device_id")
|
||||
deviceID, err := strconv.ParseUint(deviceIDStr, 10, 64)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "设备ID格式错误")
|
||||
}
|
||||
|
||||
result, err := h.service.GetDeviceDetail(c.UserContext(), uint(deviceID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, result)
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
package h5
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
orderService "github.com/break/junhong_cmp_fiber/internal/service/order"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
)
|
||||
|
||||
type OrderHandler struct {
|
||||
service *orderService.Service
|
||||
}
|
||||
|
||||
func NewOrderHandler(service *orderService.Service) *OrderHandler {
|
||||
return &OrderHandler{service: service}
|
||||
}
|
||||
|
||||
func (h *OrderHandler) Create(c *fiber.Ctx) error {
|
||||
var req dto.CreateOrderRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
if req.PaymentMethod != model.PaymentMethodWallet {
|
||||
return errors.New(errors.CodeInvalidParam, "H5端只支持钱包支付")
|
||||
}
|
||||
|
||||
ctx := c.UserContext()
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
|
||||
var buyerType string
|
||||
var buyerID uint
|
||||
|
||||
switch userType {
|
||||
case constants.UserTypeAgent:
|
||||
buyerType = model.BuyerTypeAgent
|
||||
buyerID = middleware.GetShopIDFromContext(ctx)
|
||||
case constants.UserTypeEnterprise:
|
||||
return errors.New(errors.CodeForbidden, "企业账号不支持在线购买")
|
||||
case constants.UserTypePersonalCustomer:
|
||||
buyerType = model.BuyerTypePersonal
|
||||
buyerID = middleware.GetCustomerIDFromContext(ctx)
|
||||
default:
|
||||
return errors.New(errors.CodeForbidden, "不支持的用户类型")
|
||||
}
|
||||
|
||||
order, err := h.service.CreateH5Order(ctx, &req, buyerType, buyerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, order)
|
||||
}
|
||||
|
||||
func (h *OrderHandler) Get(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的订单ID")
|
||||
}
|
||||
|
||||
order, err := h.service.Get(c.UserContext(), uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, order)
|
||||
}
|
||||
|
||||
func (h *OrderHandler) List(c *fiber.Ctx) error {
|
||||
var req dto.OrderListRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
ctx := c.UserContext()
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
|
||||
var buyerType string
|
||||
var buyerID uint
|
||||
|
||||
switch userType {
|
||||
case constants.UserTypeAgent:
|
||||
buyerType = model.BuyerTypeAgent
|
||||
buyerID = middleware.GetShopIDFromContext(ctx)
|
||||
case constants.UserTypePersonalCustomer:
|
||||
buyerType = model.BuyerTypePersonal
|
||||
buyerID = middleware.GetCustomerIDFromContext(ctx)
|
||||
default:
|
||||
return errors.New(errors.CodeForbidden, "不支持的用户类型")
|
||||
}
|
||||
|
||||
orders, err := h.service.List(ctx, &req, buyerType, buyerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, orders)
|
||||
}
|
||||
|
||||
func (h *OrderHandler) WalletPay(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的订单ID")
|
||||
}
|
||||
|
||||
ctx := c.UserContext()
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
|
||||
var buyerType string
|
||||
var buyerID uint
|
||||
|
||||
switch userType {
|
||||
case constants.UserTypeAgent:
|
||||
buyerType = model.BuyerTypeAgent
|
||||
buyerID = middleware.GetShopIDFromContext(ctx)
|
||||
case constants.UserTypePersonalCustomer:
|
||||
buyerType = model.BuyerTypePersonal
|
||||
buyerID = middleware.GetCustomerIDFromContext(ctx)
|
||||
default:
|
||||
return errors.New(errors.CodeForbidden, "不支持的用户类型")
|
||||
}
|
||||
|
||||
if err := h.service.WalletPay(ctx, uint(id), buyerType, buyerID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, nil)
|
||||
}
|
||||
|
||||
// WechatPayJSAPI 微信 JSAPI 支付
|
||||
// POST /api/h5/orders/:id/wechat-pay/jsapi
|
||||
func (h *OrderHandler) WechatPayJSAPI(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的订单ID")
|
||||
}
|
||||
|
||||
var req dto.WechatPayJSAPIRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
ctx := c.UserContext()
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
|
||||
var buyerType string
|
||||
var buyerID uint
|
||||
|
||||
switch userType {
|
||||
case constants.UserTypeAgent:
|
||||
buyerType = model.BuyerTypeAgent
|
||||
buyerID = middleware.GetShopIDFromContext(ctx)
|
||||
case constants.UserTypePersonalCustomer:
|
||||
buyerType = model.BuyerTypePersonal
|
||||
buyerID = middleware.GetCustomerIDFromContext(ctx)
|
||||
default:
|
||||
return errors.New(errors.CodeForbidden, "不支持的用户类型")
|
||||
}
|
||||
|
||||
result, err := h.service.WechatPayJSAPI(ctx, uint(id), req.OpenID, buyerType, buyerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, result)
|
||||
}
|
||||
|
||||
// WechatPayH5 微信 H5 支付
|
||||
// POST /api/h5/orders/:id/wechat-pay/h5
|
||||
func (h *OrderHandler) WechatPayH5(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的订单ID")
|
||||
}
|
||||
|
||||
var req dto.WechatPayH5Request
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
ctx := c.UserContext()
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
|
||||
var buyerType string
|
||||
var buyerID uint
|
||||
|
||||
switch userType {
|
||||
case constants.UserTypeAgent:
|
||||
buyerType = model.BuyerTypeAgent
|
||||
buyerID = middleware.GetShopIDFromContext(ctx)
|
||||
case constants.UserTypePersonalCustomer:
|
||||
buyerType = model.BuyerTypePersonal
|
||||
buyerID = middleware.GetCustomerIDFromContext(ctx)
|
||||
default:
|
||||
return errors.New(errors.CodeForbidden, "不支持的用户类型")
|
||||
}
|
||||
|
||||
result, err := h.service.WechatPayH5(ctx, uint(id), &req.SceneInfo, buyerType, buyerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, result)
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package h5
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
packageService "github.com/break/junhong_cmp_fiber/internal/service/package"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PackageUsageHandler H5 端套餐使用情况 Handler
|
||||
type PackageUsageHandler struct {
|
||||
db *gorm.DB
|
||||
customerViewService *packageService.CustomerViewService
|
||||
}
|
||||
|
||||
// NewPackageUsageHandler 创建 H5 端套餐使用情况 Handler
|
||||
func NewPackageUsageHandler(db *gorm.DB, customerViewService *packageService.CustomerViewService) *PackageUsageHandler {
|
||||
return &PackageUsageHandler{
|
||||
db: db,
|
||||
customerViewService: customerViewService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetMyUsage 任务 15.2-15.5: 获取我的套餐使用情况
|
||||
// GET /api/h5/packages/my-usage
|
||||
func (h *PackageUsageHandler) GetMyUsage(c *fiber.Ctx) error {
|
||||
ctx := c.UserContext()
|
||||
|
||||
// 任务 15.3: 从 JWT 上下文中提取用户信息
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
|
||||
var carrierType string
|
||||
var carrierID uint
|
||||
|
||||
// 根据用户类型获取载体信息
|
||||
switch userType {
|
||||
case constants.UserTypePersonalCustomer:
|
||||
// 个人客户:查询其订单关联的 IoT 卡或设备
|
||||
customerID := middleware.GetCustomerIDFromContext(ctx)
|
||||
if customerID == 0 {
|
||||
return errors.New(errors.CodeInvalidParam, "未找到客户信息")
|
||||
}
|
||||
|
||||
// 查询该客户的套餐使用记录,获取载体信息
|
||||
var usage model.PackageUsage
|
||||
err := h.db.WithContext(ctx).
|
||||
Joins("JOIN tb_order ON tb_order.id = tb_package_usage.order_id").
|
||||
Where("tb_order.buyer_type = ? AND tb_order.buyer_id = ?", model.BuyerTypePersonal, customerID).
|
||||
Where("tb_package_usage.status IN ?", []int{constants.PackageUsageStatusActive, constants.PackageUsageStatusDepleted}).
|
||||
Order("tb_package_usage.activated_at DESC").
|
||||
First(&usage).Error
|
||||
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeNotFound, "未找到套餐使用记录")
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐使用记录失败")
|
||||
}
|
||||
|
||||
// 确定载体类型和 ID
|
||||
if usage.IotCardID > 0 {
|
||||
carrierType = "iot_card"
|
||||
carrierID = usage.IotCardID
|
||||
} else if usage.DeviceID > 0 {
|
||||
carrierType = "device"
|
||||
carrierID = usage.DeviceID
|
||||
} else {
|
||||
return errors.New(errors.CodeInvalidParam, "套餐使用记录未关联卡或设备")
|
||||
}
|
||||
|
||||
case constants.UserTypeAgent, constants.UserTypeEnterprise:
|
||||
// 代理和企业用户暂不支持通过此接口查询
|
||||
// 他们应该使用后台管理接口查询指定卡/设备的套餐情况
|
||||
return errors.New(errors.CodeForbidden, "此接口仅供个人客户使用")
|
||||
|
||||
default:
|
||||
return errors.New(errors.CodeForbidden, "不支持的用户类型")
|
||||
}
|
||||
|
||||
// 任务 15.4: 调用 CustomerViewService.GetMyUsage 获取流量数据
|
||||
usageData, err := h.customerViewService.GetMyUsage(ctx, carrierType, carrierID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 任务 15.5: 返回 PackageUsageCustomerViewResponse 响应
|
||||
return response.Success(c, usageData)
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
package h5
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
rechargeService "github.com/break/junhong_cmp_fiber/internal/service/recharge"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
)
|
||||
|
||||
// RechargeHandler 充值订单处理器
|
||||
// 提供充值订单的创建、预检、查询等接口
|
||||
type RechargeHandler struct {
|
||||
service *rechargeService.Service
|
||||
}
|
||||
|
||||
// NewRechargeHandler 创建充值订单处理器实例
|
||||
// 参数:
|
||||
// - service: 充值服务
|
||||
//
|
||||
// 返回:
|
||||
// - *RechargeHandler: 充值订单处理器实例
|
||||
func NewRechargeHandler(service *rechargeService.Service) *RechargeHandler {
|
||||
return &RechargeHandler{service: service}
|
||||
}
|
||||
|
||||
// Create 创建充值订单
|
||||
// POST /api/h5/wallets/recharge
|
||||
// 请求参数:
|
||||
// - resource_type: 资源类型(iot_card/device)
|
||||
// - resource_id: 资源ID(卡ID或设备ID)
|
||||
// - amount: 充值金额(分)
|
||||
// - payment_method: 支付方式(wechat/alipay)
|
||||
//
|
||||
// 响应:
|
||||
// - 成功: 返回充值订单信息(订单ID、订单号、金额、状态等)
|
||||
// - 失败: 返回错误信息
|
||||
func (h *RechargeHandler) Create(c *fiber.Ctx) error {
|
||||
var req dto.CreateRechargeRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
ctx := c.UserContext()
|
||||
// 获取个人客户ID作为用户ID
|
||||
userID := middleware.GetCustomerIDFromContext(ctx)
|
||||
if userID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized, "用户未登录")
|
||||
}
|
||||
|
||||
result, err := h.service.Create(ctx, &req, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, result)
|
||||
}
|
||||
|
||||
// RechargeCheck 充值预检
|
||||
// GET /api/h5/wallets/recharge-check
|
||||
// 请求参数:
|
||||
// - resource_type: 资源类型(iot_card/device)
|
||||
// - resource_id: 资源ID(卡ID或设备ID)
|
||||
//
|
||||
// 响应:
|
||||
// - 成功: 返回预检信息(是否需要强充、强充金额、最小/最大充值金额等)
|
||||
// - 失败: 返回错误信息
|
||||
func (h *RechargeHandler) RechargeCheck(c *fiber.Ctx) error {
|
||||
var req dto.RechargeCheckRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
// 验证必填参数
|
||||
if req.ResourceType == "" {
|
||||
return errors.New(errors.CodeInvalidParam, "资源类型不能为空")
|
||||
}
|
||||
if req.ResourceID == 0 {
|
||||
return errors.New(errors.CodeInvalidParam, "资源ID不能为空")
|
||||
}
|
||||
|
||||
ctx := c.UserContext()
|
||||
result, err := h.service.GetRechargeCheck(ctx, req.ResourceType, req.ResourceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 转换为 DTO 响应
|
||||
resp := &dto.RechargeCheckResponse{
|
||||
NeedForceRecharge: result.NeedForceRecharge,
|
||||
ForceRechargeAmount: result.ForceRechargeAmount,
|
||||
TriggerType: result.TriggerType,
|
||||
MinAmount: result.MinAmount,
|
||||
MaxAmount: result.MaxAmount,
|
||||
CurrentAccumulated: result.CurrentAccumulated,
|
||||
Threshold: result.Threshold,
|
||||
Message: result.Message,
|
||||
FirstCommissionPaid: result.FirstCommissionPaid,
|
||||
}
|
||||
|
||||
return response.Success(c, resp)
|
||||
}
|
||||
|
||||
// List 查询充值订单列表
|
||||
// GET /api/h5/wallets/recharges
|
||||
// 请求参数:
|
||||
// - page: 页码(从1开始,默认1)
|
||||
// - page_size: 每页数量(默认20,最大100)
|
||||
// - wallet_id: 钱包ID筛选(可选)
|
||||
// - status: 状态筛选(可选,1-待支付 2-已支付 3-已完成 4-已关闭 5-已退款)
|
||||
// - start_time: 开始时间筛选(可选)
|
||||
// - end_time: 结束时间筛选(可选)
|
||||
//
|
||||
// 响应:
|
||||
// - 成功: 返回充值订单列表(分页数据、总记录数、总页数)
|
||||
// - 失败: 返回错误信息
|
||||
func (h *RechargeHandler) List(c *fiber.Ctx) error {
|
||||
var req dto.RechargeListRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
ctx := c.UserContext()
|
||||
// 获取个人客户ID作为用户ID
|
||||
userID := middleware.GetCustomerIDFromContext(ctx)
|
||||
if userID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized, "用户未登录")
|
||||
}
|
||||
|
||||
result, err := h.service.List(ctx, &req, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, result)
|
||||
}
|
||||
|
||||
// Get 查询充值订单详情
|
||||
// GET /api/h5/wallets/recharges/:id
|
||||
// 路径参数:
|
||||
// - id: 充值订单ID
|
||||
//
|
||||
// 响应:
|
||||
// - 成功: 返回充值订单详情(订单ID、订单号、金额、状态、支付信息等)
|
||||
// - 失败: 返回错误信息
|
||||
func (h *RechargeHandler) Get(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的充值订单ID")
|
||||
}
|
||||
|
||||
ctx := c.UserContext()
|
||||
// 获取个人客户ID作为用户ID
|
||||
userID := middleware.GetCustomerIDFromContext(ctx)
|
||||
if userID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized, "用户未登录")
|
||||
}
|
||||
|
||||
result, err := h.service.GetByID(ctx, uint(id), userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, result)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package model
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -85,6 +86,12 @@ type AssetRechargeRecord struct {
|
||||
EnterpriseIDTag *uint `gorm:"column:enterprise_id_tag;index;comment:企业ID标签(多租户过滤)" json:"enterprise_id_tag,omitempty"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`
|
||||
OperatorType string `gorm:"column:operator_type;type:varchar(20);not null;default:'admin_user';comment:操作人类型" json:"operator_type"`
|
||||
Generation int `gorm:"column:generation;type:int;not null;default:1;comment:资产世代编号" json:"generation"`
|
||||
LinkedPackageIDs datatypes.JSON `gorm:"column:linked_package_ids;type:jsonb;default:'[]';comment:强充关联套餐ID列表" json:"linked_package_ids,omitempty"`
|
||||
LinkedOrderType string `gorm:"column:linked_order_type;type:varchar(20);comment:关联订单类型" json:"linked_order_type,omitempty"`
|
||||
LinkedCarrierType string `gorm:"column:linked_carrier_type;type:varchar(20);comment:关联载体类型" json:"linked_carrier_type,omitempty"`
|
||||
LinkedCarrierID *uint `gorm:"column:linked_carrier_id;type:bigint;comment:关联载体ID" json:"linked_carrier_id,omitempty"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"deleted_at,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,15 @@ import (
|
||||
|
||||
type Carrier struct {
|
||||
gorm.Model
|
||||
BaseModel `gorm:"embedded"`
|
||||
CarrierCode string `gorm:"column:carrier_code;type:varchar(50);uniqueIndex:idx_carrier_code,where:deleted_at IS NULL;not null;comment:运营商编码" json:"carrier_code"`
|
||||
CarrierName string `gorm:"column:carrier_name;type:varchar(100);not null;comment:运营商名称" json:"carrier_name"`
|
||||
CarrierType string `gorm:"column:carrier_type;type:varchar(20);not null;default:'CMCC';comment:运营商类型(CMCC/CUCC/CTCC/CBN)" json:"carrier_type"`
|
||||
Description string `gorm:"column:description;type:varchar(500);comment:运营商描述" json:"description"`
|
||||
Status int `gorm:"column:status;type:int;default:1;comment:状态 1-启用 0-禁用" json:"status"`
|
||||
BillingDay int `gorm:"column:billing_day;type:int;default:1;comment:运营商计费日(用于流量查询接口的计费周期计算,联通=27,其他=1)" json:"billing_day"`
|
||||
BaseModel `gorm:"embedded"`
|
||||
CarrierCode string `gorm:"column:carrier_code;type:varchar(50);uniqueIndex:idx_carrier_code,where:deleted_at IS NULL;not null;comment:运营商编码" json:"carrier_code"`
|
||||
CarrierName string `gorm:"column:carrier_name;type:varchar(100);not null;comment:运营商名称" json:"carrier_name"`
|
||||
CarrierType string `gorm:"column:carrier_type;type:varchar(20);not null;default:'CMCC';comment:运营商类型(CMCC/CUCC/CTCC/CBN)" json:"carrier_type"`
|
||||
Description string `gorm:"column:description;type:varchar(500);comment:运营商描述" json:"description"`
|
||||
Status int `gorm:"column:status;type:int;default:1;comment:状态 1-启用 0-禁用" json:"status"`
|
||||
BillingDay int `gorm:"column:billing_day;type:int;default:1;comment:运营商计费日(用于流量查询接口的计费周期计算,联通=27,其他=1)" json:"billing_day"`
|
||||
RealnameLinkType string `gorm:"column:realname_link_type;type:varchar(20);not null;default:'none';comment:实名链接类型 none-不支持 template-模板URL gateway-Gateway接口" json:"realname_link_type"`
|
||||
RealnameLinkTemplate string `gorm:"column:realname_link_template;type:varchar(500);default:'';comment:实名链接模板URL" json:"realname_link_template"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
|
||||
@@ -35,6 +35,8 @@ type Device struct {
|
||||
AccumulatedRecharge int64 `gorm:"column:accumulated_recharge;type:bigint;default:0;comment:累计充值金额(分,废弃,使用按系列追踪)" json:"accumulated_recharge"`
|
||||
AccumulatedRechargeBySeriesJSON string `gorm:"column:accumulated_recharge_by_series;type:jsonb;default:'{}';comment:按套餐系列追踪的累计充值金额" json:"-"`
|
||||
FirstRechargeTriggeredBySeriesJSON string `gorm:"column:first_recharge_triggered_by_series;type:jsonb;default:'{}';comment:按套餐系列追踪的首充触发状态" json:"-"`
|
||||
AssetStatus int `gorm:"column:asset_status;type:int;not null;default:1;comment:业务状态 1-在库 2-已销售 3-已换货 4-已停用" json:"asset_status"`
|
||||
Generation int `gorm:"column:generation;type:int;not null;default:1;comment:资产世代编号" json:"generation"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
package dto
|
||||
|
||||
type CreateCarrierRequest struct {
|
||||
CarrierCode string `json:"carrier_code" validate:"required,min=1,max=50" required:"true" minLength:"1" maxLength:"50" description:"运营商编码"`
|
||||
CarrierName string `json:"carrier_name" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"运营商名称"`
|
||||
CarrierType string `json:"carrier_type" validate:"required,oneof=CMCC CUCC CTCC CBN" required:"true" description:"运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)"`
|
||||
Description string `json:"description" validate:"omitempty,max=500" maxLength:"500" description:"运营商描述"`
|
||||
CarrierCode string `json:"carrier_code" validate:"required,min=1,max=50" required:"true" minLength:"1" maxLength:"50" description:"运营商编码"`
|
||||
CarrierName string `json:"carrier_name" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"运营商名称"`
|
||||
CarrierType string `json:"carrier_type" validate:"required,oneof=CMCC CUCC CTCC CBN" required:"true" description:"运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)"`
|
||||
Description string `json:"description" validate:"omitempty,max=500" maxLength:"500" description:"运营商描述"`
|
||||
RealnameLinkType *string `json:"realname_link_type" validate:"omitempty,oneof=none template gateway" description:"实名链接类型 none-不支持 template-模板URL gateway-Gateway接口"`
|
||||
RealnameLinkTemplate *string `json:"realname_link_template" validate:"omitempty,max=500" maxLength:"500" description:"实名链接模板URL,支持 {iccid}/{msisdn}/{virtual_no} 占位符"`
|
||||
}
|
||||
|
||||
type UpdateCarrierRequest struct {
|
||||
CarrierName *string `json:"carrier_name" validate:"omitempty,min=1,max=100" minLength:"1" maxLength:"100" description:"运营商名称"`
|
||||
Description *string `json:"description" validate:"omitempty,max=500" maxLength:"500" description:"运营商描述"`
|
||||
CarrierName *string `json:"carrier_name" validate:"omitempty,min=1,max=100" minLength:"1" maxLength:"100" description:"运营商名称"`
|
||||
Description *string `json:"description" validate:"omitempty,max=500" maxLength:"500" description:"运营商描述"`
|
||||
RealnameLinkType *string `json:"realname_link_type" validate:"omitempty,oneof=none template gateway" description:"实名链接类型 none-不支持 template-模板URL gateway-Gateway接口"`
|
||||
RealnameLinkTemplate *string `json:"realname_link_template" validate:"omitempty,max=500" maxLength:"500" description:"实名链接模板URL,支持 {iccid}/{msisdn}/{virtual_no} 占位符"`
|
||||
}
|
||||
|
||||
type CarrierListRequest struct {
|
||||
@@ -25,14 +29,16 @@ type UpdateCarrierStatusRequest struct {
|
||||
}
|
||||
|
||||
type CarrierResponse struct {
|
||||
ID uint `json:"id" description:"运营商ID"`
|
||||
CarrierCode string `json:"carrier_code" description:"运营商编码"`
|
||||
CarrierName string `json:"carrier_name" description:"运营商名称"`
|
||||
CarrierType string `json:"carrier_type" description:"运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)"`
|
||||
Description string `json:"description" description:"运营商描述"`
|
||||
Status int `json:"status" description:"状态 (1:启用, 0:禁用)"`
|
||||
CreatedAt string `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt string `json:"updated_at" description:"更新时间"`
|
||||
ID uint `json:"id" description:"运营商ID"`
|
||||
CarrierCode string `json:"carrier_code" description:"运营商编码"`
|
||||
CarrierName string `json:"carrier_name" description:"运营商名称"`
|
||||
CarrierType string `json:"carrier_type" description:"运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)"`
|
||||
Description string `json:"description" description:"运营商描述"`
|
||||
RealnameLinkType string `json:"realname_link_type" description:"实名链接类型 none-不支持 template-模板URL gateway-Gateway接口"`
|
||||
RealnameLinkTemplate string `json:"realname_link_template" description:"实名链接模板URL"`
|
||||
Status int `json:"status" description:"状态 (1:启用, 0:禁用)"`
|
||||
CreatedAt string `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt string `json:"updated_at" description:"更新时间"`
|
||||
}
|
||||
|
||||
type UpdateCarrierParams struct {
|
||||
|
||||
@@ -83,6 +83,7 @@ type PackageResponse struct {
|
||||
ShelfStatus int `json:"shelf_status" description:"上架状态 (1:上架, 2:下架)"`
|
||||
CreatedAt string `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt string `json:"updated_at" description:"更新时间"`
|
||||
RetailPrice *int64 `json:"retail_price,omitempty" description:"代理零售价(分),仅代理用户可见"`
|
||||
ProfitMargin *int64 `json:"profit_margin,omitempty" description:"利润空间(分,仅代理用户可见)"`
|
||||
CurrentCommissionRate string `json:"current_commission_rate,omitempty" description:"当前返佣比例(仅代理用户可见)"`
|
||||
TierInfo *CommissionTierInfo `json:"tier_info,omitempty" description:"梯度返佣信息(仅代理用户可见)"`
|
||||
|
||||
@@ -4,12 +4,20 @@ package dto
|
||||
type BatchUpdateCostPriceRequest struct {
|
||||
ShopID uint `json:"shop_id" validate:"required" required:"true" description:"店铺ID"`
|
||||
SeriesID *uint `json:"series_id" validate:"omitempty" description:"套餐系列ID(可选,不填则调整所有)"`
|
||||
PricingTarget string `json:"pricing_target" validate:"omitempty,oneof=cost_price retail_price" description:"调价目标 cost_price-成本价(默认) retail_price-零售价"`
|
||||
PriceAdjustment PriceAdjustment `json:"price_adjustment" validate:"required" required:"true" description:"价格调整配置"`
|
||||
ChangeReason string `json:"change_reason" validate:"omitempty,max=255" maxLength:"255" description:"变更原因"`
|
||||
}
|
||||
|
||||
// BatchUpdateCostPriceResponse 批量调价响应
|
||||
type BatchUpdateCostPriceResponse struct {
|
||||
UpdatedCount int `json:"updated_count" description:"更新数量"`
|
||||
AffectedIDs []uint `json:"affected_ids" description:"受影响的分配ID列表"`
|
||||
UpdatedCount int `json:"updated_count" description:"更新数量"`
|
||||
AffectedIDs []uint `json:"affected_ids" description:"受影响的分配ID列表"`
|
||||
Skipped []BatchPricingSkipped `json:"skipped,omitempty" description:"跳过的记录"`
|
||||
}
|
||||
|
||||
// BatchPricingSkipped 批量调价跳过记录
|
||||
type BatchPricingSkipped struct {
|
||||
AllocationID uint `json:"allocation_id" description:"分配ID"`
|
||||
Reason string `json:"reason" description:"跳过原因"`
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ type IotCard struct {
|
||||
StoppedAt *time.Time `gorm:"column:stopped_at;comment:停机时间" json:"stopped_at,omitempty"`
|
||||
ResumedAt *time.Time `gorm:"column:resumed_at;comment:最近复机时间" json:"resumed_at,omitempty"`
|
||||
StopReason string `gorm:"column:stop_reason;type:varchar(50);comment:停机原因(traffic_exhausted=流量耗尽,manual=手动停机,arrears=欠费)" json:"stop_reason,omitempty"`
|
||||
AssetStatus int `gorm:"column:asset_status;type:int;not null;default:1;comment:业务状态 1-在库 2-已销售 3-已换货 4-已停用" json:"asset_status"`
|
||||
Generation int `gorm:"column:generation;type:int;not null;default:1;comment:资产世代编号" json:"generation"`
|
||||
IsStandalone bool `gorm:"column:is_standalone;type:boolean;default:true;not null;comment:是否为独立卡(未绑定设备) 由触发器自动维护" json:"is_standalone"`
|
||||
VirtualNo string `gorm:"column:virtual_no;type:varchar(50);uniqueIndex:idx_iot_card_virtual_no,where:deleted_at IS NULL AND virtual_no IS NOT NULL AND virtual_no <> '';comment:虚拟号(可空,全局唯一)" json:"virtual_no,omitempty"`
|
||||
}
|
||||
|
||||
@@ -40,6 +40,10 @@ type Order struct {
|
||||
SellerCostPrice int64 `gorm:"column:seller_cost_price;type:bigint;default:0;comment:销售成本价(分,用于计算利润)" json:"seller_cost_price"`
|
||||
SeriesID *uint `gorm:"column:series_id;index;comment:系列ID(用于查询分配配置)" json:"series_id,omitempty"`
|
||||
|
||||
// 订单来源和世代
|
||||
Source string `gorm:"column:source;type:varchar(20);not null;default:'admin';comment:订单来源 admin-后台 client-客户端" json:"source"`
|
||||
Generation int `gorm:"column:generation;type:int;not null;default:1;comment:资产世代编号" json:"generation"`
|
||||
|
||||
// 代购信息
|
||||
IsPurchaseOnBehalf bool `gorm:"column:is_purchase_on_behalf;type:boolean;default:false;comment:是否为代购订单" json:"is_purchase_on_behalf"`
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ type PackageUsage struct {
|
||||
DataResetCycle string `gorm:"column:data_reset_cycle;type:varchar(20);comment:流量重置周期(从Package复制,用于历史记录)" json:"data_reset_cycle"`
|
||||
LastResetAt *time.Time `gorm:"column:last_reset_at;comment:最后一次流量重置时间" json:"last_reset_at"`
|
||||
NextResetAt *time.Time `gorm:"column:next_reset_at;index:idx_package_usage_next_reset_at;comment:下次流量重置时间(用于定时任务查询)" json:"next_reset_at"`
|
||||
Generation int `gorm:"column:generation;type:int;not null;default:1;comment:资产世代编号" json:"generation"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
// 手机号、ICCID、设备号通过关联表存储
|
||||
type PersonalCustomer struct {
|
||||
gorm.Model
|
||||
WxOpenID string `gorm:"column:wx_open_id;type:varchar(100);uniqueIndex:idx_personal_customer_wx_open_id,where:deleted_at IS NULL;not null;comment:微信OpenID(唯一标识)" json:"wx_open_id"`
|
||||
WxOpenID string `gorm:"column:wx_open_id;type:varchar(100);index:idx_personal_customer_wx_open_id;not null;comment:微信OpenID(唯一标识)" json:"wx_open_id"`
|
||||
WxUnionID string `gorm:"column:wx_union_id;type:varchar(100);index;not null;comment:微信UnionID" json:"wx_union_id"`
|
||||
Nickname string `gorm:"column:nickname;type:varchar(100);comment:微信昵称" json:"nickname"`
|
||||
AvatarURL string `gorm:"column:avatar_url;type:varchar(500);comment:微信头像URL" json:"avatar_url"`
|
||||
|
||||
@@ -14,6 +14,7 @@ type ShopPackageAllocation struct {
|
||||
SeriesAllocationID *uint `gorm:"column:series_allocation_id;index;comment:关联的系列分配ID" json:"series_allocation_id"`
|
||||
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"`
|
||||
ShelfStatus int `gorm:"column:shelf_status;type:int;default:1;not null;comment:上架状态 1-上架 2-下架" json:"shelf_status"`
|
||||
RetailPrice int64 `gorm:"column:retail_price;type:bigint;not null;default:0;comment:代理面向终端客户的零售价(分)" json:"retail_price"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
|
||||
@@ -59,6 +59,9 @@ func RegisterAdminRoutes(router fiber.Router, handlers *bootstrap.Handlers, midd
|
||||
if handlers.Device != nil {
|
||||
registerDeviceRoutes(authGroup, handlers.Device, handlers.DeviceImport, doc, basePath)
|
||||
}
|
||||
if handlers.AssetLifecycle != nil {
|
||||
registerAssetLifecycleRoutes(authGroup, handlers.AssetLifecycle, doc, basePath)
|
||||
}
|
||||
if handlers.AssetAllocationRecord != nil {
|
||||
registerAssetAllocationRecordRoutes(authGroup, handlers.AssetAllocationRecord, doc, basePath)
|
||||
}
|
||||
|
||||
28
internal/routes/asset_lifecycle.go
Normal file
28
internal/routes/asset_lifecycle.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/admin"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/openapi"
|
||||
)
|
||||
|
||||
// registerAssetLifecycleRoutes 注册资产手动停用路由
|
||||
func registerAssetLifecycleRoutes(router fiber.Router, handler *admin.AssetLifecycleHandler, doc *openapi.Generator, basePath string) {
|
||||
Register(router, doc, basePath, "PATCH", "/iot-cards/:id/deactivate", handler.DeactivateIotCard, RouteSpec{
|
||||
Summary: "手动停用IoT卡",
|
||||
Tags: []string{"IoT卡管理"},
|
||||
Input: new(dto.IDReq),
|
||||
Output: nil,
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(router, doc, basePath, "PATCH", "/devices/:id/deactivate", handler.DeactivateDevice, RouteSpec{
|
||||
Summary: "手动停用设备",
|
||||
Tags: []string{"设备管理"},
|
||||
Input: new(dto.IDReq),
|
||||
Output: nil,
|
||||
Auth: true,
|
||||
})
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/bootstrap"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/openapi"
|
||||
)
|
||||
|
||||
// RegisterH5Routes 注册H5相关路由
|
||||
func RegisterH5Routes(router fiber.Router, handlers *bootstrap.Handlers, middlewares *bootstrap.Middlewares, doc *openapi.Generator, basePath string) {
|
||||
// 认证路由已迁移到 /api/auth,参见 RegisterAuthRoutes
|
||||
authGroup := router.Group("", middlewares.H5Auth)
|
||||
|
||||
if handlers.H5Order != nil {
|
||||
registerH5OrderRoutes(authGroup, handlers.H5Order, doc, basePath)
|
||||
}
|
||||
if handlers.H5Recharge != nil {
|
||||
registerH5RechargeRoutes(authGroup, handlers.H5Recharge, doc, basePath)
|
||||
}
|
||||
if handlers.EnterpriseDeviceH5 != nil {
|
||||
registerH5EnterpriseDeviceRoutes(authGroup, handlers.EnterpriseDeviceH5, doc, basePath)
|
||||
}
|
||||
if handlers.H5PackageUsage != nil {
|
||||
registerH5PackageUsageRoutes(authGroup, handlers.H5PackageUsage, doc, basePath)
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/h5"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/openapi"
|
||||
)
|
||||
|
||||
func registerH5EnterpriseDeviceRoutes(router fiber.Router, handler *h5.EnterpriseDeviceHandler, doc *openapi.Generator, basePath string) {
|
||||
devices := router.Group("/devices")
|
||||
groupPath := basePath + "/devices"
|
||||
|
||||
Register(devices, doc, groupPath, "GET", "", handler.ListDevices, RouteSpec{
|
||||
Summary: "企业设备列表(H5)",
|
||||
Tags: []string{"H5-企业设备"},
|
||||
Input: new(dto.H5EnterpriseDeviceListReq),
|
||||
Output: new(dto.EnterpriseDeviceListResp),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(devices, doc, groupPath, "GET", "/:device_id", handler.GetDeviceDetail, RouteSpec{
|
||||
Summary: "获取设备详情(H5)",
|
||||
Tags: []string{"H5-企业设备"},
|
||||
Input: new(dto.DeviceDetailReq),
|
||||
Output: new(dto.EnterpriseDeviceDetailResp),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/h5"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/openapi"
|
||||
)
|
||||
|
||||
// registerH5PackageUsageRoutes 注册 H5 端套餐使用情况路由
|
||||
func registerH5PackageUsageRoutes(router fiber.Router, handler *h5.PackageUsageHandler, doc *openapi.Generator, basePath string) {
|
||||
packages := router.Group("/packages")
|
||||
groupPath := basePath + "/packages"
|
||||
|
||||
Register(packages, doc, groupPath, "GET", "/my-usage", handler.GetMyUsage, RouteSpec{
|
||||
Summary: "获取我的套餐使用情况",
|
||||
Tags: []string{"H5-套餐"},
|
||||
Input: nil,
|
||||
Output: new(dto.PackageUsageCustomerViewResponse),
|
||||
Auth: true,
|
||||
})
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/admin"
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/callback"
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/h5"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/openapi"
|
||||
)
|
||||
@@ -53,57 +52,6 @@ func registerAdminOrderRoutes(router fiber.Router, handler *admin.OrderHandler,
|
||||
})
|
||||
}
|
||||
|
||||
// registerH5OrderRoutes 注册H5订单路由
|
||||
func registerH5OrderRoutes(router fiber.Router, handler *h5.OrderHandler, doc *openapi.Generator, basePath string) {
|
||||
Register(router, doc, basePath, "POST", "/orders", handler.Create, RouteSpec{
|
||||
Summary: "创建订单",
|
||||
Tags: []string{"H5 订单"},
|
||||
Input: new(dto.CreateOrderRequest),
|
||||
Output: new(dto.OrderResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(router, doc, basePath, "GET", "/orders", handler.List, RouteSpec{
|
||||
Summary: "获取订单列表",
|
||||
Tags: []string{"H5 订单"},
|
||||
Input: new(dto.OrderListRequest),
|
||||
Output: new(dto.OrderListResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(router, doc, basePath, "GET", "/orders/:id", handler.Get, RouteSpec{
|
||||
Summary: "获取订单详情",
|
||||
Tags: []string{"H5 订单"},
|
||||
Input: new(dto.GetOrderRequest),
|
||||
Output: new(dto.OrderResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(router, doc, basePath, "POST", "/orders/:id/wallet-pay", handler.WalletPay, RouteSpec{
|
||||
Summary: "钱包支付",
|
||||
Tags: []string{"H5 订单"},
|
||||
Input: new(dto.CancelOrderRequest),
|
||||
Output: nil,
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(router, doc, basePath, "POST", "/orders/:id/wechat-pay/jsapi", handler.WechatPayJSAPI, RouteSpec{
|
||||
Summary: "微信 JSAPI 支付",
|
||||
Tags: []string{"H5 订单"},
|
||||
Input: new(dto.WechatPayJSAPIParams),
|
||||
Output: new(dto.WechatPayJSAPIResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(router, doc, basePath, "POST", "/orders/:id/wechat-pay/h5", handler.WechatPayH5, RouteSpec{
|
||||
Summary: "微信 H5 支付",
|
||||
Tags: []string{"H5 订单"},
|
||||
Input: new(dto.WechatPayH5Params),
|
||||
Output: new(dto.WechatPayH5Response),
|
||||
Auth: true,
|
||||
})
|
||||
}
|
||||
|
||||
// registerPaymentCallbackRoutes 注册支付回调路由
|
||||
func registerPaymentCallbackRoutes(router fiber.Router, handler *callback.PaymentHandler, doc *openapi.Generator, basePath string) {
|
||||
Register(router, doc, basePath, "POST", "/wechat-pay", handler.WechatPayCallback, RouteSpec{
|
||||
|
||||
@@ -6,60 +6,16 @@ import (
|
||||
"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/openapi"
|
||||
)
|
||||
|
||||
// RegisterPersonalCustomerRoutes 注册个人客户路由
|
||||
// 路由挂载在 /api/c/v1 下
|
||||
func RegisterPersonalCustomerRoutes(router fiber.Router, doc *openapi.Generator, basePath string, handlers *bootstrap.Handlers, personalAuthMiddleware *middleware.PersonalAuthMiddleware) {
|
||||
// 公开路由(不需要认证)
|
||||
publicGroup := router.Group("")
|
||||
|
||||
// 发送验证码
|
||||
Register(publicGroup, doc, basePath, "POST", "/login/send-code", handlers.PersonalCustomer.SendCode, RouteSpec{
|
||||
Summary: "发送验证码",
|
||||
Description: "向指定手机号发送登录验证码",
|
||||
Tags: []string{"个人客户 - 认证"},
|
||||
Auth: false,
|
||||
Input: &apphandler.SendCodeRequest{},
|
||||
Output: nil,
|
||||
})
|
||||
|
||||
// 登录
|
||||
Register(publicGroup, doc, basePath, "POST", "/login", handlers.PersonalCustomer.Login, RouteSpec{
|
||||
Summary: "手机号登录",
|
||||
Description: "使用手机号和验证码登录",
|
||||
Tags: []string{"个人客户 - 认证"},
|
||||
Auth: false,
|
||||
Input: &apphandler.LoginRequest{},
|
||||
Output: &apphandler.LoginResponse{},
|
||||
})
|
||||
|
||||
// 微信 OAuth 登录(公开)
|
||||
Register(publicGroup, doc, basePath, "POST", "/wechat/auth", handlers.PersonalCustomer.WechatOAuthLogin, RouteSpec{
|
||||
Summary: "微信授权登录",
|
||||
Description: "使用微信授权码登录,自动创建或关联用户",
|
||||
Tags: []string{"个人客户 - 认证"},
|
||||
Auth: false,
|
||||
Input: &dto.WechatOAuthRequest{},
|
||||
Output: &dto.WechatOAuthResponse{},
|
||||
})
|
||||
|
||||
// 需要认证的路由
|
||||
authGroup := router.Group("")
|
||||
authGroup.Use(personalAuthMiddleware.Authenticate())
|
||||
|
||||
// 绑定微信
|
||||
Register(authGroup, doc, basePath, "POST", "/bind-wechat", handlers.PersonalCustomer.BindWechat, RouteSpec{
|
||||
Summary: "绑定微信",
|
||||
Description: "绑定微信账号到当前个人客户",
|
||||
Tags: []string{"个人客户 - 账户"},
|
||||
Auth: true,
|
||||
Input: &dto.WechatOAuthRequest{},
|
||||
Output: nil,
|
||||
})
|
||||
|
||||
// 获取个人资料
|
||||
Register(authGroup, doc, basePath, "GET", "/profile", handlers.PersonalCustomer.GetProfile, RouteSpec{
|
||||
Summary: "获取个人资料",
|
||||
|
||||
@@ -1,44 +1 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/h5"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/openapi"
|
||||
)
|
||||
|
||||
// registerH5RechargeRoutes 注册H5充值路由
|
||||
func registerH5RechargeRoutes(router fiber.Router, handler *h5.RechargeHandler, doc *openapi.Generator, basePath string) {
|
||||
Register(router, doc, basePath, "POST", "/wallets/recharge", handler.Create, RouteSpec{
|
||||
Summary: "创建充值订单",
|
||||
Tags: []string{"H5 充值"},
|
||||
Input: new(dto.CreateRechargeRequest),
|
||||
Output: new(dto.RechargeResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(router, doc, basePath, "GET", "/wallets/recharge-check", handler.RechargeCheck, RouteSpec{
|
||||
Summary: "充值预检",
|
||||
Tags: []string{"H5 充值"},
|
||||
Input: new(dto.RechargeCheckRequest),
|
||||
Output: new(dto.RechargeCheckResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(router, doc, basePath, "GET", "/wallets/recharges", handler.List, RouteSpec{
|
||||
Summary: "获取充值订单列表",
|
||||
Tags: []string{"H5 充值"},
|
||||
Input: new(dto.RechargeListRequest),
|
||||
Output: new(dto.RechargeListResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(router, doc, basePath, "GET", "/wallets/recharges/:id", handler.Get, RouteSpec{
|
||||
Summary: "获取充值订单详情",
|
||||
Tags: []string{"H5 充值"},
|
||||
Input: new(dto.GetRechargeRequest),
|
||||
Output: new(dto.RechargeResponse),
|
||||
Auth: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -28,15 +28,11 @@ func RegisterRoutesWithDoc(app *fiber.App, handlers *bootstrap.Handlers, middlew
|
||||
adminGroup := app.Group("/api/admin")
|
||||
RegisterAdminRoutes(adminGroup, handlers, middlewares, doc, "/api/admin")
|
||||
|
||||
// 4. H5 域 (挂载在 /api/h5)
|
||||
h5Group := app.Group("/api/h5")
|
||||
RegisterH5Routes(h5Group, handlers, middlewares, doc, "/api/h5")
|
||||
|
||||
// 5. 个人客户路由 (挂载在 /api/c/v1)
|
||||
// 4. 个人客户路由 (挂载在 /api/c/v1)
|
||||
personalGroup := app.Group("/api/c/v1")
|
||||
RegisterPersonalCustomerRoutes(personalGroup, doc, "/api/c/v1", handlers, middlewares.PersonalAuth)
|
||||
|
||||
// 6. 支付回调路由 (挂载在 /api/callback,无需认证)
|
||||
// 5. 支付回调路由 (挂载在 /api/callback,无需认证)
|
||||
if handlers.PaymentCallback != nil {
|
||||
callbackGroup := app.Group("/api/callback")
|
||||
registerPaymentCallbackRoutes(callbackGroup, handlers.PaymentCallback, doc, "/api/callback")
|
||||
|
||||
88
internal/service/asset/lifecycle_service.go
Normal file
88
internal/service/asset/lifecycle_service.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package asset
|
||||
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var deactivatableAssetStatuses = []int{constants.AssetStatusInStock, constants.AssetStatusSold}
|
||||
|
||||
// LifecycleService 资产生命周期服务
|
||||
type LifecycleService struct {
|
||||
db *gorm.DB
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
}
|
||||
|
||||
// NewLifecycleService 创建资产生命周期服务
|
||||
func NewLifecycleService(db *gorm.DB, iotCardStore *postgres.IotCardStore, deviceStore *postgres.DeviceStore) *LifecycleService {
|
||||
return &LifecycleService{
|
||||
db: db,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceStore: deviceStore,
|
||||
}
|
||||
}
|
||||
|
||||
// DeactivateIotCard 手动停用 IoT 卡
|
||||
func (s *LifecycleService) DeactivateIotCard(ctx context.Context, id uint) error {
|
||||
card, err := s.iotCardStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if stderrors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New(errors.CodeIotCardNotFound)
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
|
||||
}
|
||||
|
||||
if !canDeactivateAsset(card.AssetStatus) {
|
||||
return errors.New(errors.CodeForbidden, "当前状态不允许停用")
|
||||
}
|
||||
|
||||
result := s.db.WithContext(ctx).Model(&model.IotCard{}).
|
||||
Where("id = ? AND asset_status IN ?", id, deactivatableAssetStatuses).
|
||||
Update("asset_status", constants.AssetStatusDeactivated)
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "停用IoT卡失败")
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeConflict, "状态已变更,请刷新后重试")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeactivateDevice 手动停用设备
|
||||
func (s *LifecycleService) DeactivateDevice(ctx context.Context, id uint) error {
|
||||
device, err := s.deviceStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if stderrors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New(errors.CodeNotFound, "设备不存在")
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败")
|
||||
}
|
||||
|
||||
if !canDeactivateAsset(device.AssetStatus) {
|
||||
return errors.New(errors.CodeForbidden, "当前状态不允许停用")
|
||||
}
|
||||
|
||||
result := s.db.WithContext(ctx).Model(&model.Device{}).
|
||||
Where("id = ? AND asset_status IN ?", id, deactivatableAssetStatuses).
|
||||
Update("asset_status", constants.AssetStatusDeactivated)
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "停用设备失败")
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeConflict, "状态已变更,请刷新后重试")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func canDeactivateAsset(assetStatus int) bool {
|
||||
return assetStatus == constants.AssetStatusInStock || assetStatus == constants.AssetStatusSold
|
||||
}
|
||||
@@ -41,6 +41,12 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateCarrierRequest) (*d
|
||||
Description: req.Description,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
if req.RealnameLinkType != nil {
|
||||
carrier.RealnameLinkType = *req.RealnameLinkType
|
||||
}
|
||||
if req.RealnameLinkTemplate != nil {
|
||||
carrier.RealnameLinkTemplate = *req.RealnameLinkTemplate
|
||||
}
|
||||
carrier.Creator = currentUserID
|
||||
|
||||
if err := s.carrierStore.Create(ctx, carrier); err != nil {
|
||||
@@ -81,6 +87,15 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateCarrierReq
|
||||
if req.Description != nil {
|
||||
carrier.Description = *req.Description
|
||||
}
|
||||
if req.RealnameLinkType != nil {
|
||||
carrier.RealnameLinkType = *req.RealnameLinkType
|
||||
}
|
||||
if req.RealnameLinkTemplate != nil {
|
||||
carrier.RealnameLinkTemplate = *req.RealnameLinkTemplate
|
||||
}
|
||||
if carrier.RealnameLinkType == "template" && carrier.RealnameLinkTemplate == "" {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "模板URL类型必须提供实名链接模板")
|
||||
}
|
||||
carrier.Updater = currentUserID
|
||||
|
||||
if err := s.carrierStore.Update(ctx, carrier); err != nil {
|
||||
@@ -169,13 +184,15 @@ func (s *Service) UpdateStatus(ctx context.Context, id uint, status int) error {
|
||||
|
||||
func (s *Service) toResponse(c *model.Carrier) *dto.CarrierResponse {
|
||||
return &dto.CarrierResponse{
|
||||
ID: c.ID,
|
||||
CarrierCode: c.CarrierCode,
|
||||
CarrierName: c.CarrierName,
|
||||
CarrierType: c.CarrierType,
|
||||
Description: c.Description,
|
||||
Status: c.Status,
|
||||
CreatedAt: c.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: c.UpdatedAt.Format(time.RFC3339),
|
||||
ID: c.ID,
|
||||
CarrierCode: c.CarrierCode,
|
||||
CarrierName: c.CarrierName,
|
||||
CarrierType: c.CarrierType,
|
||||
Description: c.Description,
|
||||
RealnameLinkType: c.RealnameLinkType,
|
||||
RealnameLinkTemplate: c.RealnameLinkTemplate,
|
||||
Status: c.Status,
|
||||
CreatedAt: c.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: c.UpdatedAt.Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/commission_stats"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
@@ -201,7 +202,7 @@ func (s *Service) CalculateCostDiffCommission(ctx context.Context, order *model.
|
||||
}
|
||||
|
||||
func (s *Service) triggerOneTimeCommissionForCardInTx(ctx context.Context, tx *gorm.DB, order *model.Order, cardID uint) error {
|
||||
if order.IsPurchaseOnBehalf {
|
||||
if order.IsPurchaseOnBehalf || order.Source != constants.OrderSourceClient {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -285,7 +286,7 @@ func (s *Service) TriggerOneTimeCommissionForCard(ctx context.Context, order *mo
|
||||
}
|
||||
|
||||
func (s *Service) triggerOneTimeCommissionForDeviceInTx(ctx context.Context, tx *gorm.DB, order *model.Order, deviceID uint) error {
|
||||
if order.IsPurchaseOnBehalf {
|
||||
if order.IsPurchaseOnBehalf || order.Source != constants.OrderSourceClient {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -564,6 +564,17 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde
|
||||
}
|
||||
}
|
||||
|
||||
// 从资产获取当前 generation(用于订单快照)
|
||||
var assetGeneration int
|
||||
if validationResult.Card != nil {
|
||||
assetGeneration = validationResult.Card.Generation
|
||||
} else if validationResult.Device != nil {
|
||||
assetGeneration = validationResult.Device.Generation
|
||||
}
|
||||
if assetGeneration == 0 {
|
||||
assetGeneration = 1
|
||||
}
|
||||
|
||||
order := &model.Order{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: userID,
|
||||
@@ -571,6 +582,8 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde
|
||||
},
|
||||
OrderNo: s.orderStore.GenerateOrderNo(),
|
||||
OrderType: req.OrderType,
|
||||
Source: constants.OrderSourceAdmin,
|
||||
Generation: assetGeneration,
|
||||
BuyerType: orderBuyerType,
|
||||
BuyerID: orderBuyerID,
|
||||
IotCardID: req.IotCardID,
|
||||
|
||||
@@ -533,9 +533,9 @@ func (s *Service) toResponse(ctx context.Context, pkg *model.Package) *dto.Packa
|
||||
allocation, err := s.packageAllocationStore.GetByShopAndPackage(ctx, shopID, pkg.ID)
|
||||
if err == nil && allocation != nil {
|
||||
resp.CostPrice = allocation.CostPrice
|
||||
profitMargin := pkg.SuggestedRetailPrice - allocation.CostPrice
|
||||
resp.RetailPrice = &allocation.RetailPrice
|
||||
profitMargin := allocation.RetailPrice - allocation.CostPrice
|
||||
resp.ProfitMargin = &profitMargin
|
||||
// 代理查询时,shelf_status 返回自己分配记录的值,而非平台全局值
|
||||
resp.ShelfStatus = allocation.ShelfStatus
|
||||
}
|
||||
}
|
||||
@@ -595,9 +595,9 @@ func (s *Service) toResponseWithAllocation(_ context.Context, pkg *model.Package
|
||||
if allocationMap != nil {
|
||||
if allocation, ok := allocationMap[pkg.ID]; ok {
|
||||
resp.CostPrice = allocation.CostPrice
|
||||
profitMargin := pkg.SuggestedRetailPrice - allocation.CostPrice
|
||||
resp.RetailPrice = &allocation.RetailPrice
|
||||
profitMargin := allocation.RetailPrice - allocation.CostPrice
|
||||
resp.ProfitMargin = &profitMargin
|
||||
// 代理查询时,shelf_status 返回自己分配记录的值,而非平台全局值
|
||||
resp.ShelfStatus = allocation.ShelfStatus
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,44 +133,57 @@ func (s *Service) validatePackages(ctx context.Context, packageIDs []uint, serie
|
||||
}
|
||||
|
||||
if sellerShopID > 0 {
|
||||
// 代理渠道:检查卖家代理的 allocation.shelf_status,不检查 package.shelf_status
|
||||
if err := s.validateAgentShelfStatus(ctx, sellerShopID, pkgID); err != nil {
|
||||
return nil, 0, err
|
||||
// 代理渠道:检查上架状态并获取分配记录,使用零售价
|
||||
allocation, allocErr := s.validateAgentAllocation(ctx, sellerShopID, pkgID)
|
||||
if allocErr != nil {
|
||||
return nil, 0, allocErr
|
||||
}
|
||||
// 零售价低于成本价时视为不可购买,防止亏损售卖
|
||||
if allocation.RetailPrice < allocation.CostPrice {
|
||||
return nil, 0, errors.New(errors.CodeInvalidParam, "套餐价格配置异常,暂不可购买")
|
||||
}
|
||||
totalPrice += allocation.RetailPrice
|
||||
} else {
|
||||
// 平台自营渠道:检查 package.shelf_status
|
||||
if pkg.ShelfStatus != constants.ShelfStatusOn {
|
||||
return nil, 0, errors.New(errors.CodeInvalidParam, "套餐已下架")
|
||||
}
|
||||
totalPrice += pkg.SuggestedRetailPrice
|
||||
}
|
||||
|
||||
packages = append(packages, pkg)
|
||||
totalPrice += pkg.SuggestedRetailPrice
|
||||
}
|
||||
|
||||
return packages, totalPrice, nil
|
||||
}
|
||||
|
||||
// validateAgentShelfStatus 校验卖家代理的分配记录上架状态
|
||||
func (s *Service) validateAgentShelfStatus(ctx context.Context, sellerShopID, packageID uint) error {
|
||||
// 使用不带数据权限过滤的查询,避免 buyer ctx 的权限限制干扰系统级校验
|
||||
// validateAgentAllocation 校验卖家代理的分配记录上架状态,并返回分配记录
|
||||
func (s *Service) validateAgentAllocation(ctx context.Context, sellerShopID, packageID uint) (*model.ShopPackageAllocation, error) {
|
||||
allocation, err := s.packageAllocationStore.GetByShopAndPackageForSystem(ctx, sellerShopID, packageID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeInvalidParam, "套餐已下架")
|
||||
return nil, errors.New(errors.CodeInvalidParam, "套餐已下架")
|
||||
}
|
||||
return errors.Wrap(errors.CodeInternalError, err, "查询套餐分配记录失败")
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询套餐分配记录失败")
|
||||
}
|
||||
|
||||
if allocation.ShelfStatus != constants.ShelfStatusOn {
|
||||
return errors.New(errors.CodeInvalidParam, "套餐已下架")
|
||||
return nil, errors.New(errors.CodeInvalidParam, "套餐已下架")
|
||||
}
|
||||
|
||||
return nil
|
||||
return allocation, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetPurchasePrice(ctx context.Context, pkg *model.Package, buyerType string) int64 {
|
||||
return pkg.SuggestedRetailPrice
|
||||
// GetPurchasePrice 获取购买价格
|
||||
// 代理渠道(sellerShopID > 0)返回 allocation.RetailPrice,平台渠道返回 Package.SuggestedRetailPrice
|
||||
func (s *Service) GetPurchasePrice(ctx context.Context, pkg *model.Package, sellerShopID uint) (int64, error) {
|
||||
if sellerShopID > 0 {
|
||||
allocation, err := s.packageAllocationStore.GetByShopAndPackageForSystem(ctx, sellerShopID, pkg.ID)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(errors.CodeInternalError, err, "查询套餐分配记录失败")
|
||||
}
|
||||
return allocation.RetailPrice, nil
|
||||
}
|
||||
return pkg.SuggestedRetailPrice, nil
|
||||
}
|
||||
|
||||
// ValidateAdminOfflineCardPurchase 后台 offline 订单专用卡验证
|
||||
|
||||
@@ -306,18 +306,17 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string,
|
||||
// 6. 事务处理:更新订单状态、增加余额、更新累计充值、触发佣金
|
||||
now := time.Now()
|
||||
err = s.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 6.1 更新充值订单状态(带状态检查,实现乐观锁)
|
||||
// 6.1 更新充值订单状态(带状态检查,使用事务内 tx 确保原子性)
|
||||
oldStatus := constants.RechargeStatusPending
|
||||
if err := s.assetRechargeStore.UpdateStatusWithOptimisticLock(ctx, recharge.ID, &oldStatus, constants.RechargeStatusPaid, &now, nil); err != nil {
|
||||
if err := s.assetRechargeStore.UpdateStatusWithOptimisticLockDB(ctx, tx, recharge.ID, &oldStatus, constants.RechargeStatusPaid, &now, nil); err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// 状态已变更,幂等处理
|
||||
return nil
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新充值订单状态失败")
|
||||
}
|
||||
|
||||
// 6.2 更新支付信息
|
||||
if err := s.assetRechargeStore.UpdatePaymentInfo(ctx, recharge.ID, &paymentMethod, &paymentTransactionID); err != nil {
|
||||
// 6.2 更新支付信息(使用事务内 tx)
|
||||
if err := s.assetRechargeStore.UpdatePaymentInfoWithDB(ctx, tx, recharge.ID, &paymentMethod, &paymentTransactionID); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新支付信息失败")
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ func (s *Service) BatchAllocate(ctx context.Context, req *dto.BatchAllocatePacka
|
||||
PackageID: pkg.ID,
|
||||
AllocatorShopID: allocatorShopID,
|
||||
CostPrice: costPrice,
|
||||
RetailPrice: pkg.SuggestedRetailPrice,
|
||||
SeriesAllocationID: &seriesAllocation.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
|
||||
@@ -65,37 +65,86 @@ func (s *Service) BatchUpdatePricing(ctx context.Context, req *dto.BatchUpdateCo
|
||||
return nil, errors.New(errors.CodeInvalidParam, "没有找到符合条件的分配记录")
|
||||
}
|
||||
|
||||
pricingTarget := req.PricingTarget
|
||||
if pricingTarget == "" {
|
||||
pricingTarget = "cost_price"
|
||||
}
|
||||
|
||||
updatedCount := 0
|
||||
now := time.Now()
|
||||
|
||||
affectedIDs := make([]uint, 0)
|
||||
skipped := make([]dto.BatchPricingSkipped, 0)
|
||||
|
||||
err = s.db.Transaction(func(tx *gorm.DB) error {
|
||||
for _, allocation := range allocations {
|
||||
oldPrice := allocation.CostPrice
|
||||
newPrice := s.calculateAdjustedPrice(oldPrice, &req.PriceAdjustment)
|
||||
if pricingTarget == "retail_price" {
|
||||
oldRetailPrice := allocation.RetailPrice
|
||||
newRetailPrice := s.calculateAdjustedPrice(oldRetailPrice, &req.PriceAdjustment)
|
||||
if newRetailPrice == oldRetailPrice {
|
||||
continue
|
||||
}
|
||||
if newRetailPrice < allocation.CostPrice {
|
||||
skipped = append(skipped, dto.BatchPricingSkipped{
|
||||
AllocationID: allocation.ID,
|
||||
Reason: "零售价不能低于成本价",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if newPrice == oldPrice {
|
||||
continue
|
||||
}
|
||||
history := &model.ShopPackageAllocationPriceHistory{
|
||||
AllocationID: allocation.ID,
|
||||
OldCostPrice: oldRetailPrice,
|
||||
NewCostPrice: newRetailPrice,
|
||||
ChangeReason: req.ChangeReason + "(零售价调整)",
|
||||
ChangedBy: currentUserID,
|
||||
EffectiveFrom: now,
|
||||
}
|
||||
if err := tx.Create(history).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "创建价格历史失败")
|
||||
}
|
||||
|
||||
history := &model.ShopPackageAllocationPriceHistory{
|
||||
AllocationID: allocation.ID,
|
||||
OldCostPrice: oldPrice,
|
||||
NewCostPrice: newPrice,
|
||||
ChangeReason: req.ChangeReason,
|
||||
ChangedBy: currentUserID,
|
||||
EffectiveFrom: now,
|
||||
}
|
||||
allocation.RetailPrice = newRetailPrice
|
||||
allocation.Updater = currentUserID
|
||||
if err := tx.Save(allocation).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "更新零售价失败")
|
||||
}
|
||||
} else {
|
||||
oldPrice := allocation.CostPrice
|
||||
newPrice := s.calculateAdjustedPrice(oldPrice, &req.PriceAdjustment)
|
||||
if newPrice == oldPrice {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := tx.Create(history).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "创建价格历史失败")
|
||||
}
|
||||
// cost_price 锁定检查:存在下级分配记录时跳过
|
||||
var subCount int64
|
||||
tx.Model(&model.ShopPackageAllocation{}).
|
||||
Where("allocator_shop_id = ? AND package_id = ? AND deleted_at IS NULL", allocation.ShopID, allocation.PackageID).
|
||||
Count(&subCount)
|
||||
if subCount > 0 {
|
||||
skipped = append(skipped, dto.BatchPricingSkipped{
|
||||
AllocationID: allocation.ID,
|
||||
Reason: "存在下级分配记录,请先回收后再修改成本价",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
allocation.CostPrice = newPrice
|
||||
allocation.Updater = currentUserID
|
||||
if err := tx.Save(allocation).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "更新成本价失败")
|
||||
history := &model.ShopPackageAllocationPriceHistory{
|
||||
AllocationID: allocation.ID,
|
||||
OldCostPrice: oldPrice,
|
||||
NewCostPrice: newPrice,
|
||||
ChangeReason: req.ChangeReason,
|
||||
ChangedBy: currentUserID,
|
||||
EffectiveFrom: now,
|
||||
}
|
||||
if err := tx.Create(history).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "创建价格历史失败")
|
||||
}
|
||||
|
||||
allocation.CostPrice = newPrice
|
||||
allocation.Updater = currentUserID
|
||||
if err := tx.Save(allocation).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "更新成本价失败")
|
||||
}
|
||||
}
|
||||
|
||||
affectedIDs = append(affectedIDs, allocation.ID)
|
||||
@@ -112,6 +161,7 @@ func (s *Service) BatchUpdatePricing(ctx context.Context, req *dto.BatchUpdateCo
|
||||
return &dto.BatchUpdateCostPriceResponse{
|
||||
UpdatedCount: updatedCount,
|
||||
AffectedIDs: affectedIDs,
|
||||
Skipped: skipped,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -159,13 +159,13 @@ func (s *Service) buildGrantResponse(ctx context.Context, allocation *model.Shop
|
||||
// 合并全局 operator 和代理 amount
|
||||
tiers := make([]dto.GrantCommissionTierItem, 0, len(config.Tiers))
|
||||
for _, globalTier := range config.Tiers {
|
||||
tiers = append(tiers, dto.GrantCommissionTierItem{
|
||||
Operator: globalTier.Operator,
|
||||
Dimension: globalTier.Dimension,
|
||||
StatScope: globalTier.StatScope,
|
||||
Threshold: globalTier.Threshold,
|
||||
Amount: agentAmountMap[globalTier.Threshold],
|
||||
})
|
||||
tiers = append(tiers, dto.GrantCommissionTierItem{
|
||||
Operator: globalTier.Operator,
|
||||
Dimension: globalTier.Dimension,
|
||||
StatScope: globalTier.StatScope,
|
||||
Threshold: globalTier.Threshold,
|
||||
Amount: agentAmountMap[globalTier.Threshold],
|
||||
})
|
||||
}
|
||||
resp.CommissionTiers = tiers
|
||||
}
|
||||
@@ -218,7 +218,7 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesGrantRequ
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "检查授权重复失败")
|
||||
}
|
||||
if exists {
|
||||
return nil, errors.New(errors.CodeConflict, "该代理已存在此系列授权")
|
||||
return nil, errors.New(errors.CodeConflict, "该代理已存在此系列授权")
|
||||
}
|
||||
|
||||
// 3. 确定 allocatorShopID(代理操作者必须自己有授权才能向下分配)
|
||||
@@ -332,6 +332,7 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesGrantRequ
|
||||
PackageID: item.PackageID,
|
||||
AllocatorShopID: allocatorShopID,
|
||||
CostPrice: item.CostPrice,
|
||||
RetailPrice: pkg.SuggestedRetailPrice,
|
||||
SeriesAllocationID: &allocation.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: constants.StatusEnabled,
|
||||
@@ -341,7 +342,6 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesGrantRequ
|
||||
if err := txPkgStore.Create(ctx, pkgAlloc); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建套餐分配失败")
|
||||
}
|
||||
// 写成本价历史
|
||||
_ = txHistoryStore.Create(ctx, &model.ShopPackageAllocationPriceHistory{
|
||||
AllocationID: pkgAlloc.ID,
|
||||
OldCostPrice: 0,
|
||||
@@ -632,6 +632,16 @@ func (s *Service) ManagePackages(ctx context.Context, id uint, req *dto.ManageGr
|
||||
if findErr == nil {
|
||||
// 已有记录:更新成本价并写历史
|
||||
oldPrice := existing.CostPrice
|
||||
if oldPrice != item.CostPrice {
|
||||
// cost_price 锁定检查:存在下级分配记录时禁止修改
|
||||
var subCount int64
|
||||
tx.Model(&model.ShopPackageAllocation{}).
|
||||
Where("allocator_shop_id = ? AND package_id = ? AND deleted_at IS NULL", allocation.ShopID, item.PackageID).
|
||||
Count(&subCount)
|
||||
if subCount > 0 {
|
||||
return errors.New(errors.CodeForbidden, "存在下级分配记录,请先回收后再修改成本价")
|
||||
}
|
||||
}
|
||||
existing.CostPrice = item.CostPrice
|
||||
existing.Updater = operatorID
|
||||
if updateErr := txPkgStore.Update(ctx, existing); updateErr != nil {
|
||||
@@ -648,24 +658,22 @@ func (s *Service) ManagePackages(ctx context.Context, id uint, req *dto.ManageGr
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// W1: 校验套餐归属于该系列,防止跨系列套餐混入
|
||||
pkg, pkgErr := s.packageStore.GetByID(ctx, item.PackageID)
|
||||
if pkgErr != nil || pkg.SeriesID != allocation.SeriesID {
|
||||
return errors.New(errors.CodeInvalidParam, "套餐不属于该系列,无法添加到此授权")
|
||||
}
|
||||
// W2: 代理操作时,校验分配者已拥有此套餐授权,防止越权分配
|
||||
if allocation.AllocatorShopID > 0 {
|
||||
_, authErr := s.shopPackageAllocationStore.GetByShopAndPackageForSystem(ctx, allocation.AllocatorShopID, item.PackageID)
|
||||
if authErr != nil {
|
||||
return errors.New(errors.CodeForbidden, "无权限分配该套餐")
|
||||
pkg, pkgErr := s.packageStore.GetByID(ctx, item.PackageID)
|
||||
if pkgErr != nil || pkg.SeriesID != allocation.SeriesID {
|
||||
return errors.New(errors.CodeInvalidParam, "套餐不属于该系列,无法添加到此授权")
|
||||
}
|
||||
if allocation.AllocatorShopID > 0 {
|
||||
_, authErr := s.shopPackageAllocationStore.GetByShopAndPackageForSystem(ctx, allocation.AllocatorShopID, item.PackageID)
|
||||
if authErr != nil {
|
||||
return errors.New(errors.CodeForbidden, "无权限分配该套餐")
|
||||
}
|
||||
}
|
||||
}
|
||||
// 新建分配
|
||||
pkgAlloc := &model.ShopPackageAllocation{
|
||||
ShopID: allocation.ShopID,
|
||||
PackageID: item.PackageID,
|
||||
AllocatorShopID: allocation.AllocatorShopID,
|
||||
CostPrice: item.CostPrice,
|
||||
RetailPrice: pkg.SuggestedRetailPrice,
|
||||
SeriesAllocationID: &allocation.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: constants.StatusEnabled,
|
||||
|
||||
@@ -189,6 +189,11 @@ func (s *AssetRechargeStore) List(ctx context.Context, params *ListAssetRecharge
|
||||
|
||||
// UpdatePaymentInfo 更新支付信息
|
||||
func (s *AssetRechargeStore) UpdatePaymentInfo(ctx context.Context, id uint, paymentMethod *string, paymentTransactionID *string) error {
|
||||
return s.UpdatePaymentInfoWithDB(ctx, s.db, id, paymentMethod, paymentTransactionID)
|
||||
}
|
||||
|
||||
// UpdatePaymentInfoWithDB 更新支付信息(支持传入事务 tx)
|
||||
func (s *AssetRechargeStore) UpdatePaymentInfoWithDB(ctx context.Context, db *gorm.DB, id uint, paymentMethod *string, paymentTransactionID *string) error {
|
||||
updates := map[string]interface{}{}
|
||||
if paymentMethod != nil {
|
||||
updates["payment_method"] = paymentMethod
|
||||
@@ -201,7 +206,7 @@ func (s *AssetRechargeStore) UpdatePaymentInfo(ctx context.Context, id uint, pay
|
||||
return nil
|
||||
}
|
||||
|
||||
result := s.db.WithContext(ctx).Model(&model.AssetRechargeRecord{}).Where("id = ?", id).Updates(updates)
|
||||
result := db.WithContext(ctx).Model(&model.AssetRechargeRecord{}).Where("id = ?", id).Updates(updates)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
@@ -213,6 +218,11 @@ func (s *AssetRechargeStore) UpdatePaymentInfo(ctx context.Context, id uint, pay
|
||||
|
||||
// UpdateStatusWithOptimisticLock 更新充值状态(支持乐观锁)
|
||||
func (s *AssetRechargeStore) UpdateStatusWithOptimisticLock(ctx context.Context, id uint, oldStatus *int, newStatus int, paidAt interface{}, completedAt interface{}) error {
|
||||
return s.UpdateStatusWithOptimisticLockDB(ctx, s.db, id, oldStatus, newStatus, paidAt, completedAt)
|
||||
}
|
||||
|
||||
// UpdateStatusWithOptimisticLockDB 更新充值状态(支持传入事务 tx)
|
||||
func (s *AssetRechargeStore) UpdateStatusWithOptimisticLockDB(ctx context.Context, db *gorm.DB, id uint, oldStatus *int, newStatus int, paidAt interface{}, completedAt interface{}) error {
|
||||
updates := map[string]interface{}{
|
||||
"status": newStatus,
|
||||
}
|
||||
@@ -223,7 +233,7 @@ func (s *AssetRechargeStore) UpdateStatusWithOptimisticLock(ctx context.Context,
|
||||
updates["completed_at"] = completedAt
|
||||
}
|
||||
|
||||
query := s.db.WithContext(ctx).Model(&model.AssetRechargeRecord{}).Where("id = ?", id)
|
||||
query := db.WithContext(ctx).Model(&model.AssetRechargeRecord{}).Where("id = ?", id)
|
||||
|
||||
if oldStatus != nil {
|
||||
query = query.Where("status = ?", *oldStatus)
|
||||
|
||||
Reference in New Issue
Block a user