fix: 修正零售价架构错误 + 清理旧微信配置 + 归档提案 + 前端接口文档
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m12s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m12s
1. 修正 retail_price 架构:
- 删除 batch-pricing 接口的 pricing_target 字段和 retail_price 分支
(上级只能改下级成本价,不能改零售价)
- 新增 PATCH /api/admin/packages/:id/retail-price 接口
(代理自己改自己的零售价,校验 retail_price >= cost_price)
2. 清理旧微信 YAML 配置(已全部迁移到数据库 tb_wechat_config):
- 删除 config.yaml 中 wechat.official_account 配置节
- 删除 NewOfficialAccountApp() 旧工厂函数
- 清理 personal_customer service 中的死代码(旧登录/绑定微信方法)
- 清理 docker-compose.prod.yml 中旧微信环境变量和证书挂载注释
3. 归档四个已完成提案到 openspec/changes/archive/
4. 新增前端接口变更说明文档(docs/前端接口变更说明.md)
5. 修正归档提案和 specs 中关于 pricing_target 的错误描述
This commit is contained in:
@@ -15,15 +15,14 @@ import (
|
||||
// Dependencies 封装所有基础依赖
|
||||
// 这些是应用启动时初始化的核心组件
|
||||
type Dependencies struct {
|
||||
DB *gorm.DB // PostgreSQL 数据库连接
|
||||
Redis *redis.Client // Redis 客户端
|
||||
Logger *zap.Logger // 应用日志器
|
||||
JWTManager *auth.JWTManager // JWT 管理器(个人客户认证)
|
||||
TokenManager *auth.TokenManager // Token 管理器(后台和H5认证)
|
||||
VerificationService *verification.Service // 验证码服务
|
||||
QueueClient *queue.Client // Asynq 任务队列客户端
|
||||
StorageService *storage.Service // 对象存储服务(可选,配置缺失时为 nil)
|
||||
GatewayClient *gateway.Client // Gateway API 客户端(可选,配置缺失时为 nil)
|
||||
WechatOfficialAccount wechat.OfficialAccountServiceInterface // 微信公众号服务(可选)
|
||||
WechatPayment wechat.PaymentServiceInterface // 微信支付服务(可选)
|
||||
DB *gorm.DB // PostgreSQL 数据库连接
|
||||
Redis *redis.Client // Redis 客户端
|
||||
Logger *zap.Logger // 应用日志器
|
||||
JWTManager *auth.JWTManager // JWT 管理器(个人客户认证)
|
||||
TokenManager *auth.TokenManager // Token 管理器(后台和H5认证)
|
||||
VerificationService *verification.Service // 验证码服务
|
||||
QueueClient *queue.Client // Asynq 任务队列客户端
|
||||
StorageService *storage.Service // 对象存储服务(可选,配置缺失时为 nil)
|
||||
GatewayClient *gateway.Client // Gateway API 客户端(可选,配置缺失时为 nil)
|
||||
WechatPayment wechat.PaymentServiceInterface // 微信支付服务(可选)
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
AccountAudit: accountAudit,
|
||||
Role: roleSvc.New(s.Role, s.Permission, s.RolePermission),
|
||||
Permission: permissionSvc.New(s.Permission, s.AccountRole, s.RolePermission, account, deps.Redis),
|
||||
PersonalCustomer: personalCustomerSvc.NewService(s.PersonalCustomer, s.PersonalCustomerPhone, deps.VerificationService, deps.JWTManager, deps.WechatOfficialAccount, deps.Logger),
|
||||
PersonalCustomer: personalCustomerSvc.NewService(s.PersonalCustomer, s.PersonalCustomerPhone, deps.VerificationService, deps.JWTManager, deps.Logger),
|
||||
ClientAuth: clientAuthSvc.New(
|
||||
deps.DB,
|
||||
s.PersonalCustomerOpenID,
|
||||
|
||||
@@ -128,3 +128,21 @@ func (h *PackageHandler) UpdateShelfStatus(c *fiber.Ctx) error {
|
||||
|
||||
return response.Success(c, nil)
|
||||
}
|
||||
|
||||
func (h *PackageHandler) UpdateRetailPrice(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.UpdateRetailPriceRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
if err := h.service.UpdateRetailPrice(c.UserContext(), uint(id), req.RetailPrice); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, nil)
|
||||
}
|
||||
|
||||
@@ -56,6 +56,11 @@ type UpdatePackageShelfStatusRequest struct {
|
||||
ShelfStatus int `json:"shelf_status" validate:"required,oneof=1 2" required:"true" description:"上架状态 (1:上架, 2:下架)"`
|
||||
}
|
||||
|
||||
// UpdateRetailPriceRequest 更新零售价请求
|
||||
type UpdateRetailPriceRequest struct {
|
||||
RetailPrice int64 `json:"retail_price" validate:"required,min=0" required:"true" minimum:"0" description:"零售价(单位:分)"`
|
||||
}
|
||||
|
||||
// CommissionTierInfo 返佣梯度信息
|
||||
type CommissionTierInfo struct {
|
||||
CurrentRate string `json:"current_rate" description:"当前返佣比例"`
|
||||
@@ -111,6 +116,12 @@ type UpdatePackageShelfStatusParams struct {
|
||||
UpdatePackageShelfStatusRequest
|
||||
}
|
||||
|
||||
// UpdateRetailPriceParams 更新零售价聚合参数
|
||||
type UpdateRetailPriceParams struct {
|
||||
IDReq
|
||||
UpdateRetailPriceRequest
|
||||
}
|
||||
|
||||
// PackagePageResult 套餐分页结果
|
||||
type PackagePageResult struct {
|
||||
List []*PackageResponse `json:"list" description:"套餐列表"`
|
||||
|
||||
@@ -4,7 +4,6 @@ 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:"变更原因"`
|
||||
}
|
||||
|
||||
@@ -67,4 +67,12 @@ func registerPackageRoutes(router fiber.Router, handler *admin.PackageHandler, d
|
||||
Output: nil,
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(packages, doc, groupPath, "PATCH", "/:id/retail-price", handler.UpdateRetailPrice, RouteSpec{
|
||||
Summary: "修改零售价(代理)",
|
||||
Tags: []string{"套餐管理"},
|
||||
Input: new(dto.UpdateRetailPriceParams),
|
||||
Output: nil,
|
||||
Auth: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -456,6 +456,42 @@ func (s *Service) UpdateShelfStatus(ctx context.Context, id uint, shelfStatus in
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateRetailPrice 代理修改自己店铺的套餐零售价
|
||||
func (s *Service) UpdateRetailPrice(ctx context.Context, packageID uint, retailPrice int64) error {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
if userType != constants.UserTypeAgent {
|
||||
return errors.New(errors.CodeForbidden, "仅代理用户可修改零售价")
|
||||
}
|
||||
|
||||
shopID := middleware.GetShopIDFromContext(ctx)
|
||||
if shopID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized, "当前用户不属于任何店铺")
|
||||
}
|
||||
|
||||
allocation, err := s.packageAllocationStore.GetByShopAndPackage(ctx, shopID, packageID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeNotFound, "该套餐未分配给您")
|
||||
}
|
||||
return errors.Wrap(errors.CodeInternalError, err, "获取分配记录失败")
|
||||
}
|
||||
|
||||
if retailPrice < allocation.CostPrice {
|
||||
return errors.New(errors.CodeInvalidParam, "零售价不能低于成本价")
|
||||
}
|
||||
|
||||
if err := s.packageAllocationStore.UpdateRetailPrice(ctx, allocation.ID, retailPrice, currentUserID); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "更新零售价失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateAgentShelfStatus 代理上下架路径:更新分配记录的 shelf_status
|
||||
func (s *Service) updateAgentShelfStatus(ctx context.Context, packageID uint, shelfStatus int, updaterID uint) error {
|
||||
shopID := middleware.GetShopIDFromContext(ctx)
|
||||
|
||||
@@ -6,24 +6,21 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/verification"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auth"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/wechat"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Service 个人客户服务
|
||||
type Service struct {
|
||||
store *postgres.PersonalCustomerStore
|
||||
phoneStore *postgres.PersonalCustomerPhoneStore
|
||||
verificationService *verification.Service
|
||||
jwtManager *auth.JWTManager
|
||||
wechatOfficialAccount wechat.OfficialAccountServiceInterface
|
||||
logger *zap.Logger
|
||||
store *postgres.PersonalCustomerStore
|
||||
phoneStore *postgres.PersonalCustomerPhoneStore
|
||||
verificationService *verification.Service
|
||||
jwtManager *auth.JWTManager
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewService 创建个人客户服务实例
|
||||
@@ -32,16 +29,14 @@ func NewService(
|
||||
phoneStore *postgres.PersonalCustomerPhoneStore,
|
||||
verificationService *verification.Service,
|
||||
jwtManager *auth.JWTManager,
|
||||
wechatOfficialAccount wechat.OfficialAccountServiceInterface,
|
||||
logger *zap.Logger,
|
||||
) *Service {
|
||||
return &Service{
|
||||
store: store,
|
||||
phoneStore: phoneStore,
|
||||
verificationService: verificationService,
|
||||
jwtManager: jwtManager,
|
||||
wechatOfficialAccount: wechatOfficialAccount,
|
||||
logger: logger,
|
||||
store: store,
|
||||
phoneStore: phoneStore,
|
||||
verificationService: verificationService,
|
||||
jwtManager: jwtManager,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,115 +50,6 @@ func (s *Service) VerifyCode(ctx context.Context, phone string, code string) err
|
||||
return s.verificationService.VerifyCode(ctx, phone, code)
|
||||
}
|
||||
|
||||
// LoginByPhone 通过手机号登录
|
||||
// 如果手机号不存在,自动创建新的个人客户
|
||||
// 注意:此方法是临时实现,完整的登录流程应该是先微信授权,再绑定手机号
|
||||
func (s *Service) LoginByPhone(ctx context.Context, phone string, code string) (string, *model.PersonalCustomer, error) {
|
||||
// 验证验证码
|
||||
if err := s.verificationService.VerifyCode(ctx, phone, code); err != nil {
|
||||
s.logger.Warn("验证码验证失败",
|
||||
zap.String("phone", phone),
|
||||
zap.Error(err),
|
||||
)
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// 查找或创建个人客户
|
||||
customer, err := s.store.GetByPhone(ctx, phone)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// 客户不存在,创建新客户
|
||||
// 注意:临时实现,使用空的微信信息(正式应该先微信授权)
|
||||
customer = &model.PersonalCustomer{
|
||||
WxOpenID: "", // 临时为空,后续需绑定微信
|
||||
WxUnionID: "", // 临时为空,后续需绑定微信
|
||||
Status: 1, // 默认启用
|
||||
}
|
||||
if err := s.store.Create(ctx, customer); err != nil {
|
||||
s.logger.Error("创建个人客户失败",
|
||||
zap.String("phone", phone),
|
||||
zap.Error(err),
|
||||
)
|
||||
return "", nil, errors.Wrap(errors.CodeInternalError, err, "创建个人客户失败")
|
||||
}
|
||||
|
||||
// 创建手机号绑定记录
|
||||
// TODO: 这里需要通过 PersonalCustomerPhoneStore 来创建
|
||||
// 暂时跳过,等待 PersonalCustomerPhoneStore 实现
|
||||
|
||||
s.logger.Info("创建新个人客户",
|
||||
zap.Uint("customer_id", customer.ID),
|
||||
zap.String("phone", phone),
|
||||
)
|
||||
} else {
|
||||
s.logger.Error("查询个人客户失败",
|
||||
zap.String("phone", phone),
|
||||
zap.Error(err),
|
||||
)
|
||||
return "", nil, errors.Wrap(errors.CodeInternalError, err, "查询个人客户失败")
|
||||
}
|
||||
}
|
||||
|
||||
// 检查客户状态
|
||||
if customer.Status == 0 {
|
||||
s.logger.Warn("个人客户已被禁用",
|
||||
zap.Uint("customer_id", customer.ID),
|
||||
zap.String("phone", phone),
|
||||
)
|
||||
return "", nil, errors.New(errors.CodeForbidden, "账号已被禁用")
|
||||
}
|
||||
|
||||
// 生成 Token(临时传递 phone,后续应该从 Token 中移除 phone 字段)
|
||||
token, err := s.jwtManager.GeneratePersonalCustomerToken(customer.ID, phone)
|
||||
if err != nil {
|
||||
s.logger.Error("生成 Token 失败",
|
||||
zap.Uint("customer_id", customer.ID),
|
||||
zap.String("phone", phone),
|
||||
zap.Error(err),
|
||||
)
|
||||
return "", nil, errors.Wrap(errors.CodeInternalError, err, "生成 Token 失败")
|
||||
}
|
||||
|
||||
s.logger.Info("个人客户登录成功",
|
||||
zap.Uint("customer_id", customer.ID),
|
||||
zap.String("phone", phone),
|
||||
)
|
||||
|
||||
return token, customer, nil
|
||||
}
|
||||
|
||||
// BindWechat 绑定微信信息
|
||||
func (s *Service) BindWechat(ctx context.Context, customerID uint, wxOpenID, wxUnionID string) error {
|
||||
// 获取客户
|
||||
customer, err := s.store.GetByID(ctx, customerID)
|
||||
if err != nil {
|
||||
s.logger.Error("查询个人客户失败",
|
||||
zap.Uint("customer_id", customerID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return errors.Wrap(errors.CodeInternalError, err, "查询个人客户失败")
|
||||
}
|
||||
|
||||
// 更新微信信息
|
||||
customer.WxOpenID = wxOpenID
|
||||
customer.WxUnionID = wxUnionID
|
||||
|
||||
if err := s.store.Update(ctx, customer); err != nil {
|
||||
s.logger.Error("更新微信信息失败",
|
||||
zap.Uint("customer_id", customerID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return errors.Wrap(errors.CodeInternalError, err, "更新微信信息失败")
|
||||
}
|
||||
|
||||
s.logger.Info("绑定微信信息成功",
|
||||
zap.Uint("customer_id", customerID),
|
||||
zap.String("wx_open_id", wxOpenID),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateProfile 更新个人资料
|
||||
func (s *Service) UpdateProfile(ctx context.Context, customerID uint, nickname, avatarURL string) error {
|
||||
customer, err := s.store.GetByID(ctx, customerID)
|
||||
@@ -241,190 +127,3 @@ func (s *Service) GetProfileWithPhone(ctx context.Context, customerID uint) (*mo
|
||||
|
||||
return customer, phone, nil
|
||||
}
|
||||
|
||||
// WechatOAuthLogin 微信 OAuth 登录
|
||||
// 通过微信授权码登录,如果用户不存在则自动创建
|
||||
func (s *Service) WechatOAuthLogin(ctx context.Context, code string) (*dto.WechatOAuthResponse, error) {
|
||||
// 检查微信服务是否已配置
|
||||
if s.wechatOfficialAccount == nil {
|
||||
s.logger.Error("微信公众号服务未配置")
|
||||
return nil, errors.New(errors.CodeWechatOAuthFailed, "微信服务未配置")
|
||||
}
|
||||
|
||||
// 通过授权码获取用户详细信息
|
||||
userInfo, err := s.wechatOfficialAccount.GetUserInfoDetailed(ctx, code)
|
||||
if err != nil {
|
||||
s.logger.Error("获取微信用户信息失败",
|
||||
zap.String("code", code),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 通过 OpenID 查找现有客户
|
||||
customer, err := s.store.GetByWxOpenID(ctx, userInfo.OpenID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// 客户不存在,创建新客户
|
||||
customer = &model.PersonalCustomer{
|
||||
WxOpenID: userInfo.OpenID,
|
||||
WxUnionID: userInfo.UnionID,
|
||||
Nickname: userInfo.Nickname,
|
||||
AvatarURL: userInfo.Avatar,
|
||||
Status: 1, // 默认启用
|
||||
}
|
||||
if err := s.store.Create(ctx, customer); err != nil {
|
||||
s.logger.Error("创建微信用户失败",
|
||||
zap.String("open_id", userInfo.OpenID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "创建用户失败")
|
||||
}
|
||||
s.logger.Info("通过微信创建新用户",
|
||||
zap.Uint("customer_id", customer.ID),
|
||||
zap.String("open_id", userInfo.OpenID),
|
||||
)
|
||||
} else {
|
||||
s.logger.Error("查询微信用户失败",
|
||||
zap.String("open_id", userInfo.OpenID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询用户失败")
|
||||
}
|
||||
} else {
|
||||
// 客户已存在,更新昵称和头像(如果有变化)
|
||||
needUpdate := false
|
||||
if userInfo.Nickname != "" && customer.Nickname != userInfo.Nickname {
|
||||
customer.Nickname = userInfo.Nickname
|
||||
needUpdate = true
|
||||
}
|
||||
if userInfo.Avatar != "" && customer.AvatarURL != userInfo.Avatar {
|
||||
customer.AvatarURL = userInfo.Avatar
|
||||
needUpdate = true
|
||||
}
|
||||
if needUpdate {
|
||||
if err := s.store.Update(ctx, customer); err != nil {
|
||||
s.logger.Warn("更新微信用户信息失败",
|
||||
zap.Uint("customer_id", customer.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
// 不阻断登录流程
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查客户状态
|
||||
if customer.Status == 0 {
|
||||
s.logger.Warn("微信用户已被禁用",
|
||||
zap.Uint("customer_id", customer.ID),
|
||||
zap.String("open_id", userInfo.OpenID),
|
||||
)
|
||||
return nil, errors.New(errors.CodeForbidden, "账号已被禁用")
|
||||
}
|
||||
|
||||
// 生成 JWT Token
|
||||
token, err := s.jwtManager.GeneratePersonalCustomerToken(customer.ID, "")
|
||||
if err != nil {
|
||||
s.logger.Error("生成 Token 失败",
|
||||
zap.Uint("customer_id", customer.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "生成 Token 失败")
|
||||
}
|
||||
|
||||
// 获取主手机号(如果有)
|
||||
phone := ""
|
||||
primaryPhone, err := s.phoneStore.GetPrimaryPhone(ctx, customer.ID)
|
||||
if err == nil {
|
||||
phone = primaryPhone.Phone
|
||||
}
|
||||
|
||||
s.logger.Info("微信 OAuth 登录成功",
|
||||
zap.Uint("customer_id", customer.ID),
|
||||
zap.String("open_id", userInfo.OpenID),
|
||||
)
|
||||
|
||||
return &dto.WechatOAuthResponse{
|
||||
AccessToken: token,
|
||||
ExpiresIn: 24 * 60 * 60, // 24 小时
|
||||
Customer: &dto.PersonalCustomerResponse{
|
||||
ID: customer.ID,
|
||||
Phone: phone,
|
||||
Nickname: customer.Nickname,
|
||||
AvatarURL: customer.AvatarURL,
|
||||
WxOpenID: customer.WxOpenID,
|
||||
WxUnionID: customer.WxUnionID,
|
||||
Status: customer.Status,
|
||||
CreatedAt: customer.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: customer.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BindWechatWithCode 通过微信授权码绑定微信
|
||||
// customerID: 当前登录的客户 ID
|
||||
// code: 微信授权码
|
||||
func (s *Service) BindWechatWithCode(ctx context.Context, customerID uint, code string) error {
|
||||
// 检查微信服务是否已配置
|
||||
if s.wechatOfficialAccount == nil {
|
||||
s.logger.Error("微信公众号服务未配置")
|
||||
return errors.New(errors.CodeWechatOAuthFailed, "微信服务未配置")
|
||||
}
|
||||
|
||||
// 获取客户信息
|
||||
customer, err := s.store.GetByID(ctx, customerID)
|
||||
if err != nil {
|
||||
s.logger.Error("查询个人客户失败",
|
||||
zap.Uint("customer_id", customerID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return errors.Wrap(errors.CodeInternalError, err, "查询客户失败")
|
||||
}
|
||||
|
||||
// 获取微信用户信息
|
||||
userInfo, err := s.wechatOfficialAccount.GetUserInfoDetailed(ctx, code)
|
||||
if err != nil {
|
||||
s.logger.Error("获取微信用户信息失败",
|
||||
zap.Uint("customer_id", customerID),
|
||||
zap.String("code", code),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查该 OpenID 是否已被其他用户绑定
|
||||
existingCustomer, err := s.store.GetByWxOpenID(ctx, userInfo.OpenID)
|
||||
if err == nil && existingCustomer.ID != customerID {
|
||||
s.logger.Warn("微信账号已被其他用户绑定",
|
||||
zap.Uint("customer_id", customerID),
|
||||
zap.Uint("existing_customer_id", existingCustomer.ID),
|
||||
zap.String("open_id", userInfo.OpenID),
|
||||
)
|
||||
return errors.New(errors.CodeConflict, "该微信账号已被其他用户绑定")
|
||||
}
|
||||
|
||||
// 更新微信信息
|
||||
customer.WxOpenID = userInfo.OpenID
|
||||
customer.WxUnionID = userInfo.UnionID
|
||||
if userInfo.Nickname != "" {
|
||||
customer.Nickname = userInfo.Nickname
|
||||
}
|
||||
if userInfo.Avatar != "" {
|
||||
customer.AvatarURL = userInfo.Avatar
|
||||
}
|
||||
|
||||
if err := s.store.Update(ctx, customer); err != nil {
|
||||
s.logger.Error("绑定微信信息失败",
|
||||
zap.Uint("customer_id", customerID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return errors.Wrap(errors.CodeInternalError, err, "绑定微信失败")
|
||||
}
|
||||
|
||||
s.logger.Info("绑定微信成功",
|
||||
zap.Uint("customer_id", customerID),
|
||||
zap.String("open_id", userInfo.OpenID),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -65,11 +65,6 @@ 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)
|
||||
@@ -77,74 +72,41 @@ func (s *Service) BatchUpdatePricing(ctx context.Context, req *dto.BatchUpdateCo
|
||||
|
||||
err = s.db.Transaction(func(tx *gorm.DB) error {
|
||||
for _, allocation := range allocations {
|
||||
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
|
||||
}
|
||||
oldPrice := allocation.CostPrice
|
||||
newPrice := s.calculateAdjustedPrice(oldPrice, &req.PriceAdjustment)
|
||||
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, "创建价格历史失败")
|
||||
}
|
||||
// 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.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
|
||||
}
|
||||
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, "创建价格历史失败")
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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, "更新成本价失败")
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -139,6 +139,16 @@ func (s *ShopPackageAllocationStore) UpdateShelfStatus(ctx context.Context, id u
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (s *ShopPackageAllocationStore) UpdateRetailPrice(ctx context.Context, id uint, retailPrice int64, updater uint) error {
|
||||
return s.db.WithContext(ctx).
|
||||
Model(&model.ShopPackageAllocation{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"retail_price": retailPrice,
|
||||
"updater": updater,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (s *ShopPackageAllocationStore) GetByShopID(ctx context.Context, shopID uint) ([]*model.ShopPackageAllocation, error) {
|
||||
var allocations []*model.ShopPackageAllocation
|
||||
query := s.db.WithContext(ctx).Where("shop_id = ? AND status = 1", shopID)
|
||||
|
||||
Reference in New Issue
Block a user