Merge remote-tracking branch 'refs/remotes/origin/main' into HEAD
Deploy olons-admin production / deploy (push) Successful in 8m1s
Deploy olons-admin production / deploy (push) Successful in 8m1s
This commit is contained in:
@@ -35,6 +35,7 @@ type ProductColumns struct {
|
||||
CoverUrl string // 封面URL
|
||||
DetailHtml string // 商品详情
|
||||
IsRecommended string // 是否推荐
|
||||
ClaimedCount string // 已领取展示数
|
||||
PublishStatus string // 发布状态
|
||||
StartAt string // 展示开始时间
|
||||
EndAt string // 展示结束时间
|
||||
@@ -65,6 +66,7 @@ var productColumns = ProductColumns{
|
||||
CoverUrl: "cover_url",
|
||||
DetailHtml: "detail_html",
|
||||
IsRecommended: "is_recommended",
|
||||
ClaimedCount: "claimed_count",
|
||||
PublishStatus: "publish_status",
|
||||
StartAt: "start_at",
|
||||
EndAt: "end_at",
|
||||
|
||||
@@ -133,6 +133,12 @@ func (s *sSysAppMember) View(ctx context.Context, in *sysin.AppMemberViewInp) (r
|
||||
}
|
||||
|
||||
func (s *sSysAppMember) Edit(ctx context.Context, in *sysin.AppMemberEditInp) error {
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
return s.edit(ctx, in)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *sSysAppMember) edit(ctx context.Context, in *sysin.AppMemberEditInp) error {
|
||||
if in.Id == 0 {
|
||||
return gerror.New("后台不支持创建会员,请由会员在C端完成注册")
|
||||
}
|
||||
@@ -233,7 +239,82 @@ func (s *sSysAppMember) Edit(ctx context.Context, in *sysin.AppMemberEditInp) er
|
||||
return err
|
||||
}
|
||||
}
|
||||
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "member", Action: "member.edit", TargetType: "app_member", TargetId: in.Id, Before: before, After: g.Map{"username": in.Username, "mobileChanged": in.Mobile != "", "parentId": in.ParentId, "riskStatus": in.RiskStatus, "autoDispatch": in.AutoDispatch, "memberCategory": in.MemberCategory}})
|
||||
if in.AvailableAmount != nil {
|
||||
if err = s.setAvailableAmount(ctx, in.Id, *in.AvailableAmount); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "member", Action: "member.edit", TargetType: "app_member", TargetId: in.Id, Before: before, After: g.Map{"username": in.Username, "mobileChanged": in.Mobile != "", "parentId": in.ParentId, "riskStatus": in.RiskStatus, "autoDispatch": in.AutoDispatch, "memberCategory": in.MemberCategory, "availableAmount": in.AvailableAmount}})
|
||||
}
|
||||
|
||||
func (s *sSysAppMember) setAvailableAmount(ctx context.Context, memberId uint64, target int64) error {
|
||||
wc := dao.WalletAccount.Columns()
|
||||
var account *entity.WalletAccount
|
||||
if err := s.walletModel(ctx).Where(wc.MemberId, memberId).Where(wc.Currency, "CNY").LockUpdate().Scan(&account); err != nil {
|
||||
return gerror.Wrap(err, "读取会员钱包失败")
|
||||
}
|
||||
if account == nil {
|
||||
if _, err := s.walletModel(ctx).
|
||||
Fields(wc.MemberId, wc.Currency).
|
||||
Hook(hook.SaveTenant).
|
||||
Data(g.Map{wc.MemberId: memberId, wc.Currency: "CNY"}).
|
||||
Insert(); err != nil {
|
||||
return gerror.Wrap(err, "初始化会员钱包失败")
|
||||
}
|
||||
if err := s.walletModel(ctx).Where(wc.MemberId, memberId).Where(wc.Currency, "CNY").LockUpdate().Scan(&account); err != nil {
|
||||
return gerror.Wrap(err, "读取会员钱包失败")
|
||||
}
|
||||
}
|
||||
if account == nil {
|
||||
return gerror.New("会员钱包不存在")
|
||||
}
|
||||
if account.AvailableAmount == target {
|
||||
return nil
|
||||
}
|
||||
|
||||
direction := consts.WalletDirectionIncome
|
||||
amount := target - account.AvailableAmount
|
||||
if amount < 0 {
|
||||
direction = consts.WalletDirectionExpense
|
||||
amount = -amount
|
||||
}
|
||||
now := gtime.Now()
|
||||
ledgerNo := "WL" + now.Format("YmdHis") + grand.S(7)
|
||||
bizNo := "ME" + now.Format("YmdHis") + grand.S(7)
|
||||
idempotencyKey := fmt.Sprintf("member-edit-%d-%d-%s", memberId, now.UnixMilli(), grand.S(6))
|
||||
lc := dao.WalletLedger.Columns()
|
||||
if _, err := dao.WalletLedger.Ctx(ctx).
|
||||
Fields(lc.LedgerNo, lc.AccountId, lc.MemberId, lc.BizType, lc.BizNo, lc.EntryType, lc.Direction, lc.Amount, lc.BeforeAmount, lc.AfterAmount, lc.Currency, lc.IdempotencyKey, lc.Remark, lc.OperatorId, lc.OccurredAt).
|
||||
Hook(hook.SaveTenant).
|
||||
Data(g.Map{
|
||||
lc.LedgerNo: ledgerNo, lc.AccountId: account.Id, lc.MemberId: memberId,
|
||||
lc.BizType: "member_edit", lc.BizNo: bizNo, lc.EntryType: fmt.Sprint(consts.WalletAdjustOther),
|
||||
lc.Direction: direction, lc.Amount: amount, lc.BeforeAmount: account.AvailableAmount,
|
||||
lc.AfterAmount: target, lc.Currency: account.Currency, lc.IdempotencyKey: idempotencyKey,
|
||||
lc.Remark: "后台编辑会员余额", lc.OperatorId: contexts.GetUserId(ctx), lc.OccurredAt: now,
|
||||
}).Insert(); err != nil {
|
||||
return gerror.Wrap(err, "写入账变流水失败")
|
||||
}
|
||||
|
||||
walletData := g.Map{wc.AvailableAmount: target, wc.Version: gdb.Raw(wc.Version + "+1")}
|
||||
if direction == consts.WalletDirectionIncome {
|
||||
walletData[wc.TotalIncome] = gdb.Raw(wc.TotalIncome + "+" + fmt.Sprint(amount))
|
||||
} else {
|
||||
walletData[wc.TotalExpense] = gdb.Raw(wc.TotalExpense + "+" + fmt.Sprint(amount))
|
||||
}
|
||||
result, err := s.walletModel(ctx).WherePri(account.Id).Where(wc.Version, account.Version).Data(walletData).Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "更新会员余额失败")
|
||||
}
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows != 1 {
|
||||
return gerror.New("会员余额已变化,请重试")
|
||||
}
|
||||
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{
|
||||
Module: "wallet", Action: "wallet.balance.edit", TargetType: "wallet_account", TargetId: account.Id,
|
||||
Before: g.Map{"availableAmount": account.AvailableAmount}, After: g.Map{"availableAmount": target, "ledgerNo": ledgerNo},
|
||||
Reason: "后台编辑会员余额",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *sSysAppMember) saveProfile(ctx context.Context, in *sysin.AppMemberEditInp) error {
|
||||
|
||||
@@ -124,7 +124,7 @@ func (s *sSysProduct) Edit(ctx context.Context, in *sysin.ProductEditInp) (id ui
|
||||
id = in.Id
|
||||
} else {
|
||||
in.ProductNo = "PD" + gtime.Now().Format("YmdHis") + grand.S(5)
|
||||
result, e := s.Model(ctx).Fields(c.ProductNo, c.CategoryId, c.ProductType, c.PaymentType, c.Name, c.ShopName, c.PriceAmount, c.Currency, c.CoverAttachmentId, c.CoverUrl, c.DetailHtml, c.IsRecommended, c.PublishStatus, c.StartAt, c.EndAt, c.Sort, c.Remark, c.CreatedBy, c.UpdatedBy).Hook(hook.SaveTenant).Data(g.Map{c.ProductNo: in.ProductNo, c.CategoryId: in.CategoryId, c.ProductType: in.ProductType, c.PaymentType: in.PaymentType, c.Name: in.Name, c.ShopName: in.ShopName, c.PriceAmount: in.PriceAmount, c.Currency: in.Currency, c.CoverAttachmentId: in.CoverAttachmentId, c.CoverUrl: in.CoverUrl, c.DetailHtml: in.DetailHtml, c.IsRecommended: in.IsRecommended, c.PublishStatus: consts.BizPublishDraft, c.StartAt: in.StartAt, c.EndAt: in.EndAt, c.Sort: in.Sort, c.Remark: in.Remark, c.CreatedBy: uid, c.UpdatedBy: uid}).Insert()
|
||||
result, e := s.Model(ctx).Fields(c.ProductNo, c.CategoryId, c.ProductType, c.PaymentType, c.Name, c.ShopName, c.PriceAmount, c.Currency, c.CoverAttachmentId, c.CoverUrl, c.DetailHtml, c.IsRecommended, c.ClaimedCount, c.PublishStatus, c.StartAt, c.EndAt, c.Sort, c.Remark, c.CreatedBy, c.UpdatedBy).Hook(hook.SaveTenant).Data(g.Map{c.ProductNo: in.ProductNo, c.CategoryId: in.CategoryId, c.ProductType: in.ProductType, c.PaymentType: in.PaymentType, c.Name: in.Name, c.ShopName: in.ShopName, c.PriceAmount: in.PriceAmount, c.Currency: in.Currency, c.CoverAttachmentId: in.CoverAttachmentId, c.CoverUrl: in.CoverUrl, c.DetailHtml: in.DetailHtml, c.IsRecommended: in.IsRecommended, c.ClaimedCount: grand.N(100, 500), c.PublishStatus: consts.BizPublishDraft, c.StartAt: in.StartAt, c.EndAt: in.EndAt, c.Sort: in.Sort, c.Remark: in.Remark, c.CreatedBy: uid, c.UpdatedBy: uid}).Insert()
|
||||
if e != nil {
|
||||
return 0, gerror.Wrap(e, "创建商品失败")
|
||||
}
|
||||
|
||||
@@ -41,7 +41,8 @@ func (s *sSysRechargeOrder) baseModel(ctx context.Context) *gdb.Model {
|
||||
func (s *sSysRechargeOrder) listModel(ctx context.Context) *gdb.Model {
|
||||
rc, mc, pc, wc, cc := dao.RechargeOrder.Columns(), dao.AppMember.Columns(), dao.AppMemberProfile.Columns(), dao.WalletAccount.Columns(), dao.PaymentChannel.Columns()
|
||||
ac := dao.AppMemberPaymentAccount.Columns()
|
||||
realName := fmt.Sprintf("COALESCE(NULLIF(%s,''),(SELECT %s FROM %s pa WHERE pa.%s=%s AND pa.%s IS NULL ORDER BY pa.%s ASC,pa.%s DESC LIMIT 1),'') real_name_mask", col("p", pc.RealNameMask), ac.HolderMask, dao.AppMemberPaymentAccount.Table(), ac.MemberId, col("r", rc.MemberId), ac.DeletedAt, ac.IsDefault, ac.Id)
|
||||
remarkName := fmt.Sprintf("CASE WHEN %s LIKE '姓名:%%' THEN SUBSTRING_INDEX(%s,'姓名:',-1) ELSE '' END", col("r", rc.Remark), col("r", rc.Remark))
|
||||
realName := fmt.Sprintf("COALESCE(NULLIF(%s,''),(SELECT NULLIF(%s,'') FROM %s pa WHERE pa.%s=%s AND pa.%s IS NULL ORDER BY pa.%s ASC,pa.%s DESC LIMIT 1),NULLIF(%s,''),'') real_name_mask", col("p", pc.RealNameMask), ac.HolderMask, dao.AppMemberPaymentAccount.Table(), ac.MemberId, col("r", rc.MemberId), ac.DeletedAt, ac.IsDefault, ac.Id, remarkName)
|
||||
return s.baseModel(ctx).
|
||||
Fields(col("r", rc.Id), col("r", rc.RechargeNo), col("r", rc.MemberId), col("m", mc.Username), col("m", mc.Mobile), realName, fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", wc.AvailableAmount)), col("r", rc.ChannelId), fmt.Sprintf("COALESCE(%s,'') channel_name", col("c", cc.Name)), col("r", rc.RequestAmount), col("r", rc.PaidAmount), col("r", rc.Currency), col("r", rc.ExternalNo), col("r", rc.VoucherAttachmentIds), col("r", rc.RechargeType), col("r", rc.RechargeStatus), col("r", rc.Remark), col("r", rc.ReviewReasonCode), col("r", rc.ReviewReason), col("r", rc.ApprovalRequestId), col("r", rc.LedgerId), col("r", rc.PaidAt), col("r", rc.ReviewedAt), col("r", rc.CreditedAt), col("r", rc.CreatedAt))
|
||||
}
|
||||
|
||||
@@ -36,12 +36,13 @@ func (s *sSysTradeOrder) listModel(ctx context.Context) *gdb.Model {
|
||||
canReview := fmt.Sprintf("EXISTS(SELECT 1 FROM %s ts WHERE ts.%s=%s AND ts.%s=%d) can_review", dao.TaskSubmission.Table(), sc.TradeOrderId, col("o", oc.Id), sc.SubmissionStatus, consts.TaskSubmissionPending)
|
||||
dispatchMode := fmt.Sprintf("CASE COALESCE(JSON_UNQUOTE(JSON_EXTRACT(%s,'$.dispatch_mode')),'') WHEN 'auto' THEN %d WHEN 'manual' THEN %d ELSE %s END auto_dispatch", col("o", oc.RuleSnapshot), consts.TradeAutoDispatchEnabled, consts.TradeAutoDispatchDisabled, col("m", mc.AutoDispatch))
|
||||
paymentStatus := fmt.Sprintf("COALESCE(JSON_UNQUOTE(JSON_EXTRACT(%s,'$.manual_payment_status')),'') payment_status", col("o", oc.RuleSnapshot))
|
||||
scheduledMatchAt := fmt.Sprintf("COALESCE(JSON_UNQUOTE(JSON_EXTRACT(%s,'$.scheduled_match_at')),'') scheduled_match_at", col("o", oc.RuleSnapshot))
|
||||
return s.model(ctx).As("o").
|
||||
LeftJoin(dao.AppMember.Table()+" m", col("m", mc.Id)+"="+col("o", oc.MemberId)).
|
||||
LeftJoin(dao.AppMemberProfile.Table()+" p", col("p", pc.MemberId)+"="+col("o", oc.MemberId)).
|
||||
LeftJoin(dao.WalletAccount.Table()+" w", col("w", wc.MemberId)+"="+col("o", oc.MemberId)+" AND "+col("w", wc.Currency)+"='CNY'").
|
||||
LeftJoin(dao.ApprovalRequest.Table()+" a", col("a", ac.Id)+"="+col("o", oc.ApprovalRequestId)).
|
||||
Fields(col("o", oc.Id), col("o", oc.OrderNo), col("o", oc.MemberId), col("m", mc.Username), col("m", mc.Mobile), col("p", pc.RealNameMask), dispatchMode, paymentStatus, fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", wc.AvailableAmount)), fmt.Sprintf("COALESCE(%s,0) frozen_amount", col("w", wc.FrozenAmount)), todayCount, canReview, col("o", oc.ProductName), col("o", oc.OrderType), col("o", oc.Quantity), col("o", oc.UnitPriceAmount), col("o", oc.TotalAmount), col("o", oc.CommissionAmount), col("o", oc.Currency), col("o", oc.Message), col("o", oc.OrderStatus), col("o", oc.FreezeStatus), col("o", oc.ApprovalRequestId), fmt.Sprintf("COALESCE(%s,0) approval_status", col("a", ac.ApprovalStatus)), col("o", oc.MatchedAt), col("o", oc.CompletedAt), col("o", oc.UnfrozenAt), col("o", oc.CreatedAt))
|
||||
Fields(col("o", oc.Id), col("o", oc.OrderNo), col("o", oc.MemberId), col("m", mc.Username), col("m", mc.Mobile), col("p", pc.RealNameMask), dispatchMode, paymentStatus, scheduledMatchAt, fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", wc.AvailableAmount)), fmt.Sprintf("COALESCE(%s,0) frozen_amount", col("w", wc.FrozenAmount)), todayCount, canReview, col("o", oc.ProductName), col("o", oc.OrderType), col("o", oc.Quantity), col("o", oc.UnitPriceAmount), col("o", oc.TotalAmount), col("o", oc.CommissionAmount), col("o", oc.Currency), col("o", oc.Message), col("o", oc.OrderStatus), col("o", oc.FreezeStatus), col("o", oc.ApprovalRequestId), fmt.Sprintf("COALESCE(%s,0) approval_status", col("a", ac.ApprovalStatus)), col("o", oc.MatchedAt), col("o", oc.CompletedAt), col("o", oc.UnfrozenAt), col("o", oc.CreatedAt))
|
||||
}
|
||||
|
||||
func (s *sSysTradeOrder) List(ctx context.Context, in *sysin.TradeOrderListInp) (list []*sysin.TradeOrderListModel, total int, err error) {
|
||||
@@ -75,7 +76,7 @@ func (s *sSysTradeOrder) List(ctx context.Context, in *sysin.TradeOrderListInp)
|
||||
return
|
||||
}
|
||||
for _, row := range list {
|
||||
row.CanMatch = row.AutoDispatch == consts.TradeAutoDispatchDisabled && row.OrderStatus == consts.TradeOrderMatching && row.ApprovalRequestId == 0
|
||||
row.CanMatch = canAdminMatchOrder(row.AutoDispatch, row.OrderStatus, row.ApprovalRequestId, row.ScheduledMatchAt, time.Now())
|
||||
manualPending := row.AutoDispatch == consts.TradeAutoDispatchDisabled && row.OrderStatus == consts.TradeOrderPending && row.FreezeStatus == consts.TradeFreezeNormal
|
||||
row.CanChangeAmount = manualPending
|
||||
row.CanForcePay = manualPending
|
||||
@@ -86,6 +87,17 @@ func (s *sSysTradeOrder) List(ctx context.Context, in *sysin.TradeOrderListInp)
|
||||
return
|
||||
}
|
||||
|
||||
func canAdminMatchOrder(dispatchMode, orderStatus int, approvalRequestId uint64, scheduledMatchAt string, now time.Time) bool {
|
||||
if orderStatus != consts.TradeOrderMatching || approvalRequestId > 0 {
|
||||
return false
|
||||
}
|
||||
if dispatchMode != consts.TradeAutoDispatchEnabled || scheduledMatchAt == "" {
|
||||
return true
|
||||
}
|
||||
matchAt, err := time.Parse(time.RFC3339, scheduledMatchAt)
|
||||
return err == nil && now.UTC().Before(matchAt.UTC())
|
||||
}
|
||||
|
||||
func (s *sSysTradeOrder) View(ctx context.Context, in *sysin.TradeOrderViewInp) (res *sysin.TradeOrderViewModel, err error) {
|
||||
var base *sysin.TradeOrderListModel
|
||||
if err = s.listModel(ctx).Where(col("o", dao.TradeOrder.Columns().Id), in.Id).Scan(&base); err != nil {
|
||||
@@ -187,8 +199,20 @@ func (s *sSysTradeOrder) Match(ctx context.Context, in *sysin.TradeOrderMatchInp
|
||||
dispatchMode = "manual"
|
||||
}
|
||||
}
|
||||
if dispatchMode != "manual" {
|
||||
return gerror.New("自动派单订单由系统匹配,不能人工匹配")
|
||||
if dispatchMode == "" {
|
||||
dispatchMode = "auto"
|
||||
}
|
||||
if dispatchMode == "auto" && order.RuleSnapshot != nil {
|
||||
scheduledMatchAt := order.RuleSnapshot.Get("scheduled_match_at").String()
|
||||
if scheduledMatchAt != "" {
|
||||
matchAt, parseErr := time.Parse(time.RFC3339, scheduledMatchAt)
|
||||
if parseErr != nil {
|
||||
return gerror.New("自动派单时间格式不正确,请刷新订单后重试")
|
||||
}
|
||||
if !time.Now().UTC().Before(matchAt.UTC()) {
|
||||
return gerror.New("自动派单已到期,订单正在由系统自动匹配")
|
||||
}
|
||||
}
|
||||
}
|
||||
var product *entity.Product
|
||||
pc := dao.Product.Columns()
|
||||
@@ -207,7 +231,7 @@ func (s *sSysTradeOrder) Match(ctx context.Context, in *sysin.TradeOrderMatchInp
|
||||
if product.StartAt != nil && product.StartAt.After(now) || product.EndAt != nil && product.EndAt.Before(now) {
|
||||
return gerror.New("商品当前不在可匹配时间范围内")
|
||||
}
|
||||
if product.PriceAmount <= 0 || product.PriceAmount > math.MaxInt64/int64(in.Quantity) {
|
||||
if in.Quantity == 0 || product.PriceAmount <= 0 || product.PriceAmount > math.MaxInt64/int64(in.Quantity) {
|
||||
return gerror.New("商品价格或数量不合法")
|
||||
}
|
||||
levelId := order.MemberLevelId
|
||||
@@ -236,6 +260,9 @@ func (s *sSysTradeOrder) Match(ctx context.Context, in *sysin.TradeOrderMatchInp
|
||||
return gerror.New("会员等级生效版本不存在")
|
||||
}
|
||||
totalAmount := product.PriceAmount * int64(in.Quantity)
|
||||
if levelVersion.CommissionRate > 0 && uint64(levelVersion.CommissionRate) > uint64(math.MaxInt64/totalAmount) {
|
||||
return gerror.New("佣金规则超出可计算范围")
|
||||
}
|
||||
commissionAmount := totalAmount * int64(levelVersion.CommissionRate) / 10000
|
||||
snapshotData := g.Map{}
|
||||
if order.RuleSnapshot != nil {
|
||||
@@ -243,20 +270,25 @@ func (s *sSysTradeOrder) Match(ctx context.Context, in *sysin.TradeOrderMatchInp
|
||||
snapshotData[key] = value
|
||||
}
|
||||
}
|
||||
snapshotData["source"], snapshotData["dispatch_mode"] = "admin_manual_match", "manual"
|
||||
snapshotData["source"], snapshotData["match_source"], snapshotData["dispatch_mode"] = "admin_manual_match", "admin_manual_match", dispatchMode
|
||||
snapshotData["product_id"], snapshotData["product_no"] = product.Id, product.ProductNo
|
||||
snapshotData["unit_price_amount"], snapshotData["quantity"] = product.PriceAmount, in.Quantity
|
||||
snapshotData["member_level_id"], snapshotData["level_version_id"] = levelId, levelVersion.Id
|
||||
snapshotData["commission_rate"], snapshotData["manual_payment_status"] = levelVersion.CommissionRate, "pending"
|
||||
snapshotData["confirmation_required"], snapshotData["deadline_at"] = true, ""
|
||||
snapshotData["commission_rate"], snapshotData["manual_payment_status"] = levelVersion.CommissionRate, "settled"
|
||||
snapshotData["confirmation_required"], snapshotData["deadline_at"] = false, ""
|
||||
snapshotData["settled_by"], snapshotData["settled_at"] = "admin", time.Now().UTC().Format(time.RFC3339)
|
||||
snapshot := gjson.New(snapshotData)
|
||||
commissionLedgerId, err := s.settleMatchedOrder(ctx, order, dispatchMode, totalAmount, commissionAmount, now)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
oc := dao.TradeOrder.Columns()
|
||||
data := g.Map{
|
||||
oc.ProductId: product.Id, oc.ProductName: product.Name,
|
||||
oc.Quantity: in.Quantity, oc.UnitPriceAmount: product.PriceAmount, oc.TotalAmount: totalAmount,
|
||||
oc.CommissionAmount: commissionAmount, oc.Currency: product.Currency, oc.MemberLevelId: levelId,
|
||||
oc.RuleSnapshot: snapshot, oc.Message: "后台已匹配大额订单,等待会员确认付款", oc.OrderStatus: consts.TradeOrderPending,
|
||||
oc.FreezeStatus: consts.TradeFreezeNormal, oc.MatchedAt: now, oc.UpdatedBy: contexts.GetUserId(ctx),
|
||||
oc.RuleSnapshot: snapshot, oc.Message: "后台已匹配并完成订单,余额及佣金已结算", oc.OrderStatus: consts.TradeOrderCompleted,
|
||||
oc.FreezeStatus: consts.TradeFreezeNormal, oc.MatchedAt: now, oc.CompletedAt: now, oc.UpdatedBy: contexts.GetUserId(ctx),
|
||||
}
|
||||
result, err := s.model(ctx).WherePri(order.Id).Where(oc.OrderStatus, consts.TradeOrderMatching).Data(data).Update()
|
||||
if err != nil {
|
||||
@@ -266,6 +298,9 @@ func (s *sSysTradeOrder) Match(ctx context.Context, in *sysin.TradeOrderMatchInp
|
||||
if rows != 1 {
|
||||
return gerror.New("订单状态已变化,请刷新后重试")
|
||||
}
|
||||
if err = s.insertMatchedSubmission(ctx, order, levelId, commissionAmount, commissionLedgerId, now, dispatchMode); err != nil {
|
||||
return err
|
||||
}
|
||||
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{
|
||||
Module: "trade", Action: "trade_order.match", TargetType: "trade_order", TargetId: order.Id,
|
||||
Before: order, After: data, ChangeJson: gjson.New(g.Map{"productId": product.Id, "quantity": in.Quantity, "totalAmount": totalAmount}),
|
||||
@@ -274,6 +309,107 @@ func (s *sSysTradeOrder) Match(ctx context.Context, in *sysin.TradeOrderMatchInp
|
||||
})
|
||||
}
|
||||
|
||||
func (s *sSysTradeOrder) settleMatchedOrder(ctx context.Context, order *entity.TradeOrder, dispatchMode string, totalAmount, commissionAmount int64, now *gtime.Time) (uint64, error) {
|
||||
wc := dao.WalletAccount.Columns()
|
||||
var account *entity.WalletAccount
|
||||
if err := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).
|
||||
Where(wc.MemberId, order.MemberId).Where(wc.Currency, "CNY").LockUpdate().Scan(&account); err != nil {
|
||||
return 0, gerror.Wrap(err, "读取会员钱包失败")
|
||||
}
|
||||
if account == nil || account.AccountStatus != 1 {
|
||||
return 0, gerror.New("会员钱包不存在或状态异常")
|
||||
}
|
||||
if totalAmount <= 0 || account.AvailableAmount < math.MinInt64+totalAmount {
|
||||
return 0, gerror.New("订单金额超出钱包处理范围")
|
||||
}
|
||||
if commissionAmount < 0 || account.AvailableAmount > math.MaxInt64-commissionAmount {
|
||||
return 0, gerror.New("订单佣金超出钱包处理范围")
|
||||
}
|
||||
if dispatchMode == "auto" && account.AvailableAmount < totalAmount {
|
||||
return 0, gerror.New("自动派单商品金额不能超过会员可用余额")
|
||||
}
|
||||
|
||||
principalAfter := account.AvailableAmount - totalAmount
|
||||
settledAfter := principalAfter + commissionAmount
|
||||
if dispatchMode == "auto" {
|
||||
settledAfter = account.AvailableAmount + commissionAmount
|
||||
if account.FrozenAmount > math.MaxInt64-totalAmount {
|
||||
return 0, gerror.New("订单金额超出钱包冻结范围")
|
||||
}
|
||||
}
|
||||
accountData := g.Map{
|
||||
wc.AvailableAmount: settledAfter,
|
||||
wc.TotalIncome: gdb.Raw(wc.TotalIncome + "+" + fmt.Sprint(commissionAmount)),
|
||||
wc.Version: gdb.Raw(wc.Version + "+1"),
|
||||
}
|
||||
if dispatchMode == "manual" {
|
||||
accountData[wc.TotalExpense] = gdb.Raw(wc.TotalExpense + "+" + fmt.Sprint(totalAmount))
|
||||
}
|
||||
result, err := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(account.Id).Where(wc.Version, account.Version).Data(accountData).Update()
|
||||
if err != nil {
|
||||
return 0, gerror.Wrap(err, "结算订单余额失败")
|
||||
}
|
||||
if affected, _ := result.RowsAffected(); affected != 1 {
|
||||
return 0, gerror.New("会员余额已变化,请刷新后重试")
|
||||
}
|
||||
|
||||
if dispatchMode == "auto" {
|
||||
holdFrozen := account.FrozenAmount + totalAmount
|
||||
entries := []struct {
|
||||
code, bizType, entryType string
|
||||
direction int
|
||||
before, after int64
|
||||
suffix, remark string
|
||||
}{
|
||||
{"AA", "task_principal_hold", "available", consts.WalletDirectionExpense, account.AvailableAmount, principalAfter, "admin-auto:available", "人工提前匹配自动单,扣除可用余额"},
|
||||
{"AF", "task_principal_hold", "frozen", consts.WalletDirectionIncome, account.FrozenAmount, holdFrozen, "admin-auto:frozen", "人工提前匹配自动单,冻结订单本金"},
|
||||
{"RF", "task_principal_release", "frozen", consts.WalletDirectionExpense, holdFrozen, account.FrozenAmount, "admin-auto:release-frozen", "自动单完成释放冻结本金"},
|
||||
{"RA", "task_principal_return", "available", consts.WalletDirectionIncome, principalAfter, account.AvailableAmount, "admin-auto:return-available", "自动单完成返回本金"},
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if _, err = insertMatchedOrderLedger(ctx, order, account, "WL"+fmt.Sprint(order.Id)+entry.code, entry.bizType, order.OrderNo, entry.entryType, entry.direction, totalAmount, entry.before, entry.after, entry.suffix, entry.remark, now); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if _, err = insertMatchedOrderLedger(ctx, order, account, fmt.Sprintf("WL%dMP", order.Id), "task_principal_expense", order.OrderNo, "available", consts.WalletDirectionExpense, totalAmount, account.AvailableAmount, principalAfter, "admin-manual:principal", "手动单匹配完成扣除订单金额(允许负余额)", now); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
if commissionAmount == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
commissionBefore := principalAfter
|
||||
if dispatchMode == "auto" {
|
||||
commissionBefore = account.AvailableAmount
|
||||
}
|
||||
return insertMatchedOrderLedger(ctx, order, account, fmt.Sprintf("WL%dMC", order.Id), "commission", fmt.Sprintf("TSADMIN%d", order.Id), "available", consts.WalletDirectionIncome, commissionAmount, commissionBefore, settledAfter, "admin-match:commission", "后台匹配订单完成返佣", now)
|
||||
}
|
||||
|
||||
func (s *sSysTradeOrder) insertMatchedSubmission(ctx context.Context, order *entity.TradeOrder, levelId uint64, commissionAmount int64, ledgerId uint64, now *gtime.Time, dispatchMode string) error {
|
||||
sc := dao.TaskSubmission.Columns()
|
||||
uid := contexts.GetUserId(ctx)
|
||||
reason := "手动派单由后台匹配并直接完成"
|
||||
if dispatchMode == "auto" {
|
||||
reason = "自动派单到期前由后台匹配并直接完成"
|
||||
}
|
||||
_, err := dao.TaskSubmission.Ctx(ctx).
|
||||
Fields(sc.SubmissionNo, sc.TradeOrderId, sc.TaskId, sc.MemberId, sc.MemberLevelId, sc.EvidenceText, sc.EvidenceAttachmentIds,
|
||||
sc.SubmissionStatus, sc.CommissionAmount, sc.SuccessRate, sc.ReviewedBy, sc.ReviewedAt, sc.ReviewReasonCode,
|
||||
sc.ReviewReason, sc.AllowResubmit, sc.LedgerId, sc.SettledAt, sc.CreatedBy, sc.UpdatedBy).
|
||||
Hook(hook.SaveTenant).Data(g.Map{
|
||||
sc.SubmissionNo: fmt.Sprintf("TSADMIN%d", order.Id), sc.TradeOrderId: order.Id, sc.TaskId: order.TaskId,
|
||||
sc.MemberId: order.MemberId, sc.MemberLevelId: levelId, sc.EvidenceText: "", sc.EvidenceAttachmentIds: gjson.New(g.Array{}),
|
||||
sc.SubmissionStatus: consts.TaskSubmissionSettled, sc.CommissionAmount: commissionAmount, sc.SuccessRate: 10000,
|
||||
sc.ReviewedBy: uid, sc.ReviewedAt: now, sc.ReviewReasonCode: "ADMIN_MATCHED", sc.ReviewReason: reason,
|
||||
sc.AllowResubmit: consts.StatusDisable, sc.LedgerId: ledgerId, sc.SettledAt: now, sc.CreatedBy: uid, sc.UpdatedBy: uid,
|
||||
}).Insert()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "创建已结算任务记录失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sSysTradeOrder) UpdateAmount(ctx context.Context, in *sysin.TradeOrderUpdateAmountInp) error {
|
||||
amount, err := parseCents(in.Amount)
|
||||
if err != nil || amount <= 0 || amount < int64(in.Quantity) {
|
||||
@@ -477,20 +613,30 @@ func cloneOrderSnapshot(order *entity.TradeOrder) g.Map {
|
||||
|
||||
func insertTradeMatchLedger(ctx context.Context, order *entity.TradeOrder, account *entity.WalletAccount,
|
||||
ledgerNo, entryType string, direction int, amount, beforeAmount, afterAmount int64, suffix, remark string) error {
|
||||
_, err := insertMatchedOrderLedger(ctx, order, account, ledgerNo, "task_principal_hold", order.OrderNo, entryType, direction, amount, beforeAmount, afterAmount, suffix, remark, gtime.Now())
|
||||
return err
|
||||
}
|
||||
|
||||
func insertMatchedOrderLedger(ctx context.Context, order *entity.TradeOrder, account *entity.WalletAccount,
|
||||
ledgerNo, bizType, bizNo, entryType string, direction int, amount, beforeAmount, afterAmount int64, suffix, remark string, occurredAt *gtime.Time) (uint64, error) {
|
||||
lc := dao.WalletLedger.Columns()
|
||||
_, err := dao.WalletLedger.Ctx(ctx).
|
||||
result, err := dao.WalletLedger.Ctx(ctx).
|
||||
Fields(lc.LedgerNo, lc.AccountId, lc.MemberId, lc.BizType, lc.BizNo, lc.EntryType, lc.Direction,
|
||||
lc.Amount, lc.BeforeAmount, lc.AfterAmount, lc.Currency, lc.IdempotencyKey, lc.Remark, lc.OperatorId, lc.OccurredAt).
|
||||
Hook(hook.SaveTenant).
|
||||
Data(g.Map{lc.LedgerNo: ledgerNo, lc.AccountId: account.Id, lc.MemberId: order.MemberId,
|
||||
lc.BizType: "task_principal_hold", lc.BizNo: order.OrderNo, lc.EntryType: entryType, lc.Direction: direction,
|
||||
lc.BizType: bizType, lc.BizNo: bizNo, lc.EntryType: entryType, lc.Direction: direction,
|
||||
lc.Amount: amount, lc.BeforeAmount: beforeAmount, lc.AfterAmount: afterAmount, lc.Currency: account.Currency,
|
||||
lc.IdempotencyKey: fmt.Sprintf("task:%d:%s", order.Id, suffix), lc.Remark: remark,
|
||||
lc.OperatorId: contexts.GetUserId(ctx), lc.OccurredAt: gtime.Now()}).Insert()
|
||||
lc.OperatorId: contexts.GetUserId(ctx), lc.OccurredAt: occurredAt}).Insert()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "记录订单扣款流水失败")
|
||||
return 0, gerror.Wrap(err, "记录订单扣款流水失败")
|
||||
}
|
||||
return nil
|
||||
ledgerId, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, gerror.Wrap(err, "读取订单流水ID失败")
|
||||
}
|
||||
return uint64(ledgerId), nil
|
||||
}
|
||||
|
||||
func (s *sSysTradeOrder) Apply(ctx context.Context, in *sysin.TradeOrderApplyInp) (res *sysin.FinanceApprovalModel, err error) {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hotgo/internal/consts"
|
||||
)
|
||||
|
||||
func TestCanAdminMatchOrder(t *testing.T) {
|
||||
now := time.Date(2026, 10, 18, 12, 0, 0, 0, time.UTC)
|
||||
tests := []struct {
|
||||
name string
|
||||
dispatchMode int
|
||||
orderStatus int
|
||||
approvalRequestId uint64
|
||||
scheduledMatchAt string
|
||||
want bool
|
||||
}{
|
||||
{name: "manual matching order", dispatchMode: consts.TradeAutoDispatchDisabled, orderStatus: consts.TradeOrderMatching, want: true},
|
||||
{name: "auto before schedule", dispatchMode: consts.TradeAutoDispatchEnabled, orderStatus: consts.TradeOrderMatching, scheduledMatchAt: now.Add(time.Minute).Format(time.RFC3339), want: true},
|
||||
{name: "auto at schedule", dispatchMode: consts.TradeAutoDispatchEnabled, orderStatus: consts.TradeOrderMatching, scheduledMatchAt: now.Format(time.RFC3339), want: false},
|
||||
{name: "auto after schedule", dispatchMode: consts.TradeAutoDispatchEnabled, orderStatus: consts.TradeOrderMatching, scheduledMatchAt: now.Add(-time.Minute).Format(time.RFC3339), want: false},
|
||||
{name: "legacy auto without schedule", dispatchMode: consts.TradeAutoDispatchEnabled, orderStatus: consts.TradeOrderMatching, want: true},
|
||||
{name: "invalid schedule", dispatchMode: consts.TradeAutoDispatchEnabled, orderStatus: consts.TradeOrderMatching, scheduledMatchAt: "invalid", want: false},
|
||||
{name: "already completed", dispatchMode: consts.TradeAutoDispatchDisabled, orderStatus: consts.TradeOrderCompleted, want: false},
|
||||
{name: "approval pending", dispatchMode: consts.TradeAutoDispatchDisabled, orderStatus: consts.TradeOrderMatching, approvalRequestId: 9, want: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := canAdminMatchOrder(tt.dispatchMode, tt.orderStatus, tt.approvalRequestId, tt.scheduledMatchAt, now)
|
||||
if got != tt.want {
|
||||
t.Fatalf("canAdminMatchOrder() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ type Product struct {
|
||||
CoverUrl any // 封面URL
|
||||
DetailHtml any // 商品详情
|
||||
IsRecommended any // 是否推荐
|
||||
ClaimedCount any // 已领取展示数
|
||||
PublishStatus any // 发布状态
|
||||
StartAt *gtime.Time // 展示开始时间
|
||||
EndAt *gtime.Time // 展示结束时间
|
||||
|
||||
@@ -24,6 +24,7 @@ type Product struct {
|
||||
CoverUrl string `json:"coverUrl" orm:"cover_url" description:"封面URL"`
|
||||
DetailHtml string `json:"detailHtml" orm:"detail_html" description:"商品详情"`
|
||||
IsRecommended int `json:"isRecommended" orm:"is_recommended" description:"是否推荐"`
|
||||
ClaimedCount uint `json:"claimedCount" orm:"claimed_count" description:"已领取展示数"`
|
||||
PublishStatus int `json:"publishStatus" orm:"publish_status" description:"发布状态"`
|
||||
StartAt *gtime.Time `json:"startAt" orm:"start_at" description:"展示开始时间"`
|
||||
EndAt *gtime.Time `json:"endAt" orm:"end_at" description:"展示结束时间"`
|
||||
|
||||
@@ -74,28 +74,29 @@ type AppMemberViewModel struct {
|
||||
}
|
||||
|
||||
type AppMemberEditInp struct {
|
||||
Id uint64 `json:"id"`
|
||||
Username string `json:"username" v:"required|length:3,64#用户名不能为空|用户名长度为3到64个字符"`
|
||||
Mobile string `json:"mobile" v:"phone-loose#手机号格式不正确"`
|
||||
Password string `json:"password"`
|
||||
TradePassword string `json:"tradePassword"`
|
||||
RealName string `json:"realName" v:"max-length:64#姓名不能超过64个字符"`
|
||||
IdNo string `json:"idNo" v:"max-length:32#证件号不能超过32个字符"`
|
||||
ParentId uint64 `json:"parentId"`
|
||||
LevelId uint64 `json:"levelId"`
|
||||
RiskStatus int `json:"riskStatus" d:"1"`
|
||||
CreditScore int `json:"creditScore" d:"100" v:"between:0,1000#信用分范围为0到1000"`
|
||||
AutoDispatch int `json:"autoDispatch" d:"1"`
|
||||
MemberCategory int `json:"memberCategory"`
|
||||
IsTestAccount int `json:"isTestAccount"`
|
||||
Id uint64 `json:"id"`
|
||||
Username string `json:"username" v:"required|length:3,64#用户名不能为空|用户名长度为3到64个字符"`
|
||||
Mobile string `json:"mobile" v:"phone-loose#手机号格式不正确"`
|
||||
Password string `json:"password"`
|
||||
TradePassword string `json:"tradePassword"`
|
||||
RealName string `json:"realName" v:"max-length:64#姓名不能超过64个字符"`
|
||||
IdNo string `json:"idNo" v:"max-length:32#证件号不能超过32个字符"`
|
||||
ParentId uint64 `json:"parentId"`
|
||||
LevelId uint64 `json:"levelId"`
|
||||
RiskStatus int `json:"riskStatus" d:"1"`
|
||||
CreditScore int `json:"creditScore" d:"100" v:"between:0,1000#信用分范围为0到1000"`
|
||||
AutoDispatch int `json:"autoDispatch" d:"1"`
|
||||
MemberCategory int `json:"memberCategory"`
|
||||
IsTestAccount int `json:"isTestAccount"`
|
||||
AvailableAmount *int64 `json:"availableAmount"`
|
||||
}
|
||||
|
||||
func (in *AppMemberEditInp) Filter(ctx context.Context) error {
|
||||
if in.Id == 0 {
|
||||
return gerror.New("后台不支持创建会员,请由会员在C端完成注册")
|
||||
}
|
||||
if in.Password != "" && (len(in.Password) < 8 || !containsLetterAndNumber(in.Password)) {
|
||||
return gerror.New("密码至少8位且必须同时包含字母和数字")
|
||||
if in.Password != "" && (len([]rune(in.Password)) < 6 || len([]byte(in.Password)) > 64) {
|
||||
return gerror.New("密码长度为6到64个字符")
|
||||
}
|
||||
if in.TradePassword != "" && len(in.TradePassword) < 6 {
|
||||
return gerror.New("交易密码不能少于6位")
|
||||
@@ -118,18 +119,6 @@ func (in *AppMemberEditInp) Filter(ctx context.Context) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func containsLetterAndNumber(value string) bool {
|
||||
letter, number := false, false
|
||||
for _, r := range value {
|
||||
if r >= '0' && r <= '9' {
|
||||
number = true
|
||||
}
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') {
|
||||
letter = true
|
||||
}
|
||||
}
|
||||
return letter && number
|
||||
}
|
||||
|
||||
type AppMemberDeleteInp struct {
|
||||
Id uint64 `json:"id" v:"required|min:1#会员ID不能为空|会员ID不能为空"`
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
type AppMemberRegisterInp struct {
|
||||
Username string `json:"username" v:"required|length:3,64#用户名不能为空|用户名长度为3到64个字符"`
|
||||
Mobile string `json:"mobile" v:"required|phone-loose#手机号不能为空|手机号格式不正确"`
|
||||
Password string `json:"password" v:"required|length:8,64#密码不能为空|密码长度为8到64个字符"`
|
||||
Password string `json:"password" v:"required|length:6,64#密码不能为空|密码长度为6到64个字符"`
|
||||
InviteCode string `json:"inviteCode" v:"required#请输入邀请码"`
|
||||
RegisterOrigin string `json:"registerOrigin" v:"max-length:512#注册来源不能超过512个字符"`
|
||||
RegisterHost string `json:"registerHost" v:"max-length:255#注册域名不能超过255个字符"`
|
||||
@@ -24,9 +24,6 @@ func (in *AppMemberRegisterInp) Filter(ctx context.Context) error {
|
||||
if !regexp.MustCompile(`^[0-9]{6}$`).MatchString(in.InviteCode) {
|
||||
return gerror.New("邀请码必须为6位数字")
|
||||
}
|
||||
if !containsLetterAndNumber(in.Password) {
|
||||
return gerror.New("密码至少8位且必须同时包含字母和数字")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ type ProductUpdateFields struct {
|
||||
type ProductInsertFields struct {
|
||||
ProductUpdateFields
|
||||
ProductNo string
|
||||
ClaimedCount uint
|
||||
PublishStatus int
|
||||
CreatedBy uint64
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ type TradeOrderListModel struct {
|
||||
CreatedAt *gtime.Time `json:"createdAt"`
|
||||
ApprovalStatus int `json:"approvalStatus"`
|
||||
PaymentStatus string `json:"paymentStatus"`
|
||||
ScheduledMatchAt string `json:"scheduledMatchAt"`
|
||||
CanMatch bool `json:"canMatch"`
|
||||
CanChangeAmount bool `json:"canChangeAmount"`
|
||||
CanForcePay bool `json:"canForcePay"`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 360" role="img" aria-label="Olons 平台横幅">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 360" role="img" aria-label="悦选平台横幅">
|
||||
<defs><linearGradient id="g" x1="0" x2="1"><stop stop-color="#1ba8ee"/><stop offset="1" stop-color="#3136dc"/></linearGradient></defs>
|
||||
<rect width="1200" height="360" rx="28" fill="url(#g)"/>
|
||||
<circle cx="1010" cy="70" r="180" fill="#fff" opacity=".10"/><circle cx="1120" cy="320" r="220" fill="#fff" opacity=".08"/>
|
||||
<text x="90" y="160" fill="#fff" font-size="66" font-family="Arial, sans-serif" font-weight="700">OLONS</text>
|
||||
<text x="90" y="160" fill="#fff" font-size="66" font-family="PingFang SC, Microsoft YaHei, sans-serif" font-weight="700">悦选</text>
|
||||
<text x="92" y="230" fill="#fff" font-size="34" font-family="Arial, sans-serif" opacity=".9">精选商品 · 安心服务</text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 680 B After Width: | Height: | Size: 704 B |
@@ -1,7 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" role="img" aria-label="Olons 商品占位图">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" role="img" aria-label="悦选商品占位图">
|
||||
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#eaf7ff"/><stop offset="1" stop-color="#dfe3ff"/></linearGradient></defs>
|
||||
<rect width="800" height="800" rx="48" fill="url(#g)"/>
|
||||
<path d="M220 300h360l-28 300H248z" fill="#fff" stroke="#2878e8" stroke-width="22"/>
|
||||
<path d="M310 315c0-118 180-118 180 0" fill="none" stroke="#2878e8" stroke-width="22" stroke-linecap="round"/>
|
||||
<text x="400" y="690" text-anchor="middle" fill="#2878e8" font-size="54" font-family="Arial, sans-serif" font-weight="700">OLONS</text>
|
||||
<text x="400" y="690" text-anchor="middle" fill="#2878e8" font-size="54" font-family="PingFang SC, Microsoft YaHei, sans-serif" font-weight="700">悦选</text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 663 B After Width: | Height: | Size: 687 B |
@@ -2,16 +2,16 @@
|
||||
UPDATE `hg_site_config_version`
|
||||
SET `config_json` = JSON_SET(
|
||||
`config_json`,
|
||||
'$.siteName', IF(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(`config_json`,'$.siteName')),'')='' OR JSON_UNQUOTE(JSON_EXTRACT(`config_json`,'$.siteName')) LIKE '演示站点%', 'Olons', JSON_UNQUOTE(JSON_EXTRACT(`config_json`,'$.siteName'))),
|
||||
'$.siteName', IF(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(`config_json`,'$.siteName')),'')='' OR JSON_UNQUOTE(JSON_EXTRACT(`config_json`,'$.siteName')) LIKE '演示站点%' OR JSON_UNQUOTE(JSON_EXTRACT(`config_json`,'$.siteName'))='Olons', '悦选', JSON_UNQUOTE(JSON_EXTRACT(`config_json`,'$.siteName'))),
|
||||
'$.privacyPolicyUrl', IF(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(`config_json`,'$.privacyPolicyUrl')),'')='', 'https://olns-web.118im.com/admin/#/app/content/privacy_policy', JSON_UNQUOTE(JSON_EXTRACT(`config_json`,'$.privacyPolicyUrl'))),
|
||||
'$.tradeBalanceAmount', IF(COALESCE(JSON_EXTRACT(`config_json`,'$.tradeBalanceAmount'),0)<=0,10000,JSON_EXTRACT(`config_json`,'$.tradeBalanceAmount')),
|
||||
'$.tradeWaitSeconds', IF(COALESCE(JSON_EXTRACT(`config_json`,'$.tradeWaitSeconds'),0)<=0,1200,JSON_EXTRACT(`config_json`,'$.tradeWaitSeconds')),
|
||||
'$.dailyTradeLimit', IF(COALESCE(JSON_EXTRACT(`config_json`,'$.dailyTradeLimit'),0)<=0,30,JSON_EXTRACT(`config_json`,'$.dailyTradeLimit')),
|
||||
'$.dailyRewardLimit', IF(COALESCE(JSON_EXTRACT(`config_json`,'$.dailyRewardLimit'),0)<=0,3,JSON_EXTRACT(`config_json`,'$.dailyRewardLimit')),
|
||||
'$.minimumRechargeAmount', IF(COALESCE(JSON_EXTRACT(`config_json`,'$.minimumRechargeAmount'),0)<=0,10000,JSON_EXTRACT(`config_json`,'$.minimumRechargeAmount')),
|
||||
'$.autoOrderDelaySeconds', IF(COALESCE(JSON_EXTRACT(`config_json`,'$.autoOrderDelaySeconds'),0)<=0,10,JSON_EXTRACT(`config_json`,'$.autoOrderDelaySeconds')),
|
||||
'$.autoOrderMinAmount', IF(COALESCE(JSON_EXTRACT(`config_json`,'$.autoOrderMinAmount'),0)<=0,100,JSON_EXTRACT(`config_json`,'$.autoOrderMinAmount')),
|
||||
'$.autoOrderMaxAmount', IF(COALESCE(JSON_EXTRACT(`config_json`,'$.autoOrderMaxAmount'),0)<=0,10000000,JSON_EXTRACT(`config_json`,'$.autoOrderMaxAmount')),
|
||||
'$.tradeBalanceAmount', CAST(IF(COALESCE(JSON_EXTRACT(`config_json`,'$.tradeBalanceAmount'),0)<=0,10000,JSON_UNQUOTE(JSON_EXTRACT(`config_json`,'$.tradeBalanceAmount'))) AS UNSIGNED),
|
||||
'$.tradeWaitSeconds', CAST(IF(COALESCE(JSON_EXTRACT(`config_json`,'$.tradeWaitSeconds'),0)<=0,1200,JSON_UNQUOTE(JSON_EXTRACT(`config_json`,'$.tradeWaitSeconds'))) AS UNSIGNED),
|
||||
'$.dailyTradeLimit', CAST(IF(COALESCE(JSON_EXTRACT(`config_json`,'$.dailyTradeLimit'),0)<=0,30,JSON_UNQUOTE(JSON_EXTRACT(`config_json`,'$.dailyTradeLimit'))) AS UNSIGNED),
|
||||
'$.dailyRewardLimit', CAST(IF(COALESCE(JSON_EXTRACT(`config_json`,'$.dailyRewardLimit'),0)<=0,3,JSON_UNQUOTE(JSON_EXTRACT(`config_json`,'$.dailyRewardLimit'))) AS UNSIGNED),
|
||||
'$.minimumRechargeAmount', CAST(IF(COALESCE(JSON_EXTRACT(`config_json`,'$.minimumRechargeAmount'),0)<=0,10000,JSON_UNQUOTE(JSON_EXTRACT(`config_json`,'$.minimumRechargeAmount'))) AS UNSIGNED),
|
||||
'$.autoOrderDelaySeconds', CAST(IF(COALESCE(JSON_EXTRACT(`config_json`,'$.autoOrderDelaySeconds'),0)<=0,10,JSON_UNQUOTE(JSON_EXTRACT(`config_json`,'$.autoOrderDelaySeconds'))) AS UNSIGNED),
|
||||
'$.autoOrderMinAmount', CAST(IF(COALESCE(JSON_EXTRACT(`config_json`,'$.autoOrderMinAmount'),0)<=0,100,JSON_UNQUOTE(JSON_EXTRACT(`config_json`,'$.autoOrderMinAmount'))) AS UNSIGNED),
|
||||
'$.autoOrderMaxAmount', CAST(IF(COALESCE(JSON_EXTRACT(`config_json`,'$.autoOrderMaxAmount'),0)<=0,10000000,JSON_UNQUOTE(JSON_EXTRACT(`config_json`,'$.autoOrderMaxAmount'))) AS UNSIGNED),
|
||||
'$.commissionRates', IF(JSON_LENGTH(COALESCE(JSON_EXTRACT(`config_json`,'$.commissionRates'),JSON_ARRAY()))<>6,JSON_ARRAY(0.10,0.05,0.03,0.02,0.01,0),JSON_EXTRACT(`config_json`,'$.commissionRates'))
|
||||
), `updated_at`=NOW()
|
||||
WHERE `config_group`='portal';
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
-- 手动派单允许后台匹配后直接完成,会员可用余额可因订单扣款变为负数。
|
||||
SET @wallet_available_check_exists = (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.TABLE_CONSTRAINTS
|
||||
WHERE CONSTRAINT_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'hg_wallet_account'
|
||||
AND CONSTRAINT_NAME = 'chk_wallet_available_nonnegative'
|
||||
AND CONSTRAINT_TYPE = 'CHECK'
|
||||
);
|
||||
SET @drop_wallet_available_check = IF(
|
||||
@wallet_available_check_exists > 0,
|
||||
'ALTER TABLE `hg_wallet_account` DROP CHECK `chk_wallet_available_nonnegative`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE drop_wallet_available_check_stmt FROM @drop_wallet_available_check;
|
||||
EXECUTE drop_wallet_available_check_stmt;
|
||||
DEALLOCATE PREPARE drop_wallet_available_check_stmt;
|
||||
@@ -0,0 +1,23 @@
|
||||
-- 手工商城领取数作为固定展示数据保存,页面和接口不再随机计算。
|
||||
SET @product_claimed_count_exists = (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'hg_product'
|
||||
AND COLUMN_NAME = 'claimed_count'
|
||||
);
|
||||
SET @add_product_claimed_count = IF(
|
||||
@product_claimed_count_exists = 0,
|
||||
'ALTER TABLE `hg_product` ADD COLUMN `claimed_count` int unsigned NOT NULL DEFAULT 0 COMMENT ''已领取展示数'' AFTER `is_recommended`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE add_product_claimed_count_stmt FROM @add_product_claimed_count;
|
||||
EXECUTE add_product_claimed_count_stmt;
|
||||
DEALLOCATE PREPARE add_product_claimed_count_stmt;
|
||||
|
||||
-- 仅为尚未设置展示数的手工商品生成一次,后续只读取数据库中的值。
|
||||
UPDATE `hg_product`
|
||||
SET `claimed_count` = FLOOR(100 + RAND() * 401)
|
||||
WHERE `product_type` = 2
|
||||
AND `claimed_count` = 0
|
||||
AND `deleted_at` IS NULL;
|
||||
Reference in New Issue
Block a user