refactor: 虎皮棋适配器内联 payment_order_service + member 服务拆 4 表文件 + file_storage 移入 common

This commit is contained in:
2026-07-31 15:08:36 +08:00
parent 033860d571
commit 527f2e0d18
12 changed files with 383 additions and 330 deletions
@@ -1,4 +1,4 @@
package service
package common
import (
"errors"
+8 -15
View File
@@ -6,10 +6,7 @@ import (
"fmt"
commonHttp "slogan-agent/common"
"slogan-agent/styleagent/dao"
"slogan-agent/styleagent/model/dto"
"slogan-agent/styleagent/model/entity"
"slogan-agent/styleagent/payment"
"slogan-agent/styleagent/service"
"github.com/gogf/gf/v2/frame/g"
@@ -22,7 +19,7 @@ var Member = new(member)
// PlanList 会员套餐列表
func (c *member) PlanList(ctx context.Context, req *dto.MemberPlanListReq) (res *dto.MemberPlanListRes, err error) {
list, err := service.MemberService.PlanList(ctx)
list, err := service.MemberPlanService.PlanList(ctx)
if err != nil {
return nil, err
}
@@ -31,7 +28,7 @@ func (c *member) PlanList(ctx context.Context, req *dto.MemberPlanListReq) (res
// Status 我的会员状态
func (c *member) Status(ctx context.Context, req *dto.MemberStatusReq) (res *dto.MemberStatusRes, err error) {
st, err := service.MemberService.Status(ctx, commonHttp.GetUserId(g.RequestFromCtx(ctx)))
st, err := service.MemberPlanService.Status(ctx, commonHttp.GetUserId(g.RequestFromCtx(ctx)))
if err != nil {
return nil, err
}
@@ -42,7 +39,7 @@ func (c *member) Status(ctx context.Context, req *dto.MemberStatusReq) (res *dto
// OrderCreate 下单 → 返回支付 URL
func (c *member) OrderCreate(ctx context.Context, req *dto.MemberOrderCreateReq) (res *dto.MemberOrderCreateRes, err error) {
order, payURL, err := service.MemberService.CreateOrder(ctx, commonHttp.GetUserId(g.RequestFromCtx(ctx)), req.PlanId)
order, payURL, err := service.PaymentOrderService.CreateMemberOrder(ctx, commonHttp.GetUserId(g.RequestFromCtx(ctx)), req.PlanId)
if err != nil {
return nil, err
}
@@ -51,7 +48,7 @@ func (c *member) OrderCreate(ctx context.Context, req *dto.MemberOrderCreateReq)
// OrderStatus 订单状态(App 轮询)
func (c *member) OrderStatus(ctx context.Context, req *dto.MemberOrderStatusReq) (res *dto.MemberOrderStatusRes, err error) {
order, err := service.MemberService.OrderStatus(ctx, req.OrderNo)
order, err := service.PaymentOrderService.OrderStatus(ctx, req.OrderNo)
if err != nil || order == nil {
return nil, errors.New("订单不存在")
}
@@ -75,21 +72,17 @@ func MemberNotify(r *ghttp.Request) {
for k, v := range r.GetRequestMap() {
params[k] = fmt.Sprint(v)
}
ok := payment.VerifyNotify(params, hash, g.Cfg().MustGet(ctx, "payment.xunhu_appsecret", "").String())
ok := service.PaymentOrderService.VerifyNotify(params, hash, g.Cfg().MustGet(ctx, "payment.xunhu_appsecret", "").String())
if !ok {
_ = dao.PayNotifyLog.Insert(ctx, &entity.PayNotifyLog{
OrderNo: orderNo, Body: body, Sign: hash, RemoteIp: remoteIP, Status: "bad_sign",
})
_ = service.PayNotifyLogService.Insert(ctx, orderNo, body, hash, remoteIP, "bad_sign")
r.Response.Write("fail")
r.ExitAll()
return
}
state, err := service.MemberService.HandlePaidNotify(ctx, orderNo, r.Get("transaction_id").String(), body)
_ = dao.PayNotifyLog.Insert(ctx, &entity.PayNotifyLog{
OrderNo: orderNo, Body: body, Sign: hash, RemoteIp: remoteIP, Status: state,
})
state, err := service.PaymentOrderService.HandlePaidNotify(ctx, orderNo, r.Get("transaction_id").String(), body)
_ = service.PayNotifyLogService.Insert(ctx, orderNo, body, hash, remoteIP, state)
// duplicate(幂等重复回调)同样返回 success,避免支付渠道无限重试
if err != nil || state == "no_order" {
r.Response.Write("fail")
-129
View File
@@ -1,129 +0,0 @@
package payment
import (
"context"
"crypto/md5"
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"time"
"github.com/gogf/gf/v2/frame/g"
)
// 虎皮棋聚合支付适配器:签名/HTTP 细节全部收敛在本包,业务层不感知。
// 注意:签名规则与字段名以官方最新文档为准(当前实现为经典 md5 约定)。
type Config struct {
AppId string
AppSecret string
NotifyUrl string
Channel string // 逗号分隔,如 "alipay,wechat"
ApiBase string
Enabled bool
}
func GetConfig(ctx context.Context) Config {
cfg := Config{
AppId: g.Cfg().MustGet(ctx, "payment.xunhu_appid", "").String(),
AppSecret: g.Cfg().MustGet(ctx, "payment.xunhu_appsecret", "").String(),
NotifyUrl: g.Cfg().MustGet(ctx, "payment.notify_url", "").String(),
Channel: g.Cfg().MustGet(ctx, "payment.channel", "alipay").String(),
ApiBase: g.Cfg().MustGet(ctx, "payment.api_base", "https://api.xunhupay.com").String(),
}
cfg.Enabled = cfg.AppId != "" && cfg.AppSecret != ""
return cfg
}
// CreateOrder 创建支付单,返回收银台/支付 URL(金额单位:分)
func CreateOrder(ctx context.Context, orderNo string, amountFen int) (payURL string, err error) {
cfg := GetConfig(ctx)
if !cfg.Enabled {
return "", errors.New("支付未开通,请在 config.yml 配置 payment")
}
channel := "alipay"
if first := strings.Split(cfg.Channel, ",")[0]; first != "" {
channel = first
}
params := map[string]string{
"appid": cfg.AppId,
"trade_order_id": orderNo,
"total_fee": fmt.Sprintf("%.2f", float64(amountFen)/100),
"title": "形象会员",
"notify_url": cfg.NotifyUrl,
"type": channel,
"version": "1.1",
"nonce_str": nonce(),
}
params["hash"] = Sign(params, cfg.AppSecret)
var resp struct {
Errcode int `json:"errcode"`
Errmsg string `json:"errmsg"`
Url string `json:"url"`
}
// 注意:Post 的最后一个参数不会自动解析响应体,需手动读取后反序列化
respRaw, err := g.Client().SetTimeout(10*time.Second).Post(context.Background(), cfg.ApiBase+"/payment/do.html", params)
if err != nil {
return "", fmt.Errorf("虎皮棋下单失败: %w", err)
}
defer respRaw.Close()
if err := json.Unmarshal(respRaw.ReadAll(), &resp); err != nil {
return "", fmt.Errorf("虎皮棋下单失败: %w", err)
}
if resp.Errcode != 0 {
return "", fmt.Errorf("虎皮棋下单失败: %s", resp.Errmsg)
}
if resp.Url == "" {
return "", errors.New("虎皮棋下单失败: 返回为空")
}
return resp.Url, nil
}
// Sign 参数名升序拼接 key=value,追加 secret 后 md5 hex
func Sign(params map[string]string, secret string) string {
keys := make([]string, 0, len(params))
for k := range params {
if params[k] == "" {
continue
}
keys = append(keys, k)
}
sort.Strings(keys)
var sb strings.Builder
for i, k := range keys {
if i > 0 {
sb.WriteString("&")
}
sb.WriteString(k)
sb.WriteString("=")
sb.WriteString(params[k])
}
sb.WriteString(secret)
sum := md5.Sum([]byte(sb.String()))
return hex.EncodeToString(sum[:])
}
// VerifyNotify 验签:复制参数去掉 hash 后重算签名比较
func VerifyNotify(params map[string]string, hash, secret string) bool {
if hash == "" || secret == "" {
return false
}
cp := make(map[string]string, len(params))
for k, v := range params {
if k != "hash" {
cp[k] = v
}
}
return Sign(cp, secret) == strings.ToLower(hash)
}
func nonce() string {
b := make([]byte, 8)
_, _ = rand.Read(b)
return hex.EncodeToString(b)
}
-49
View File
@@ -1,49 +0,0 @@
package payment
import "testing"
func TestSignDeterministic(t *testing.T) {
params := map[string]string{
"appid": "1000", "trade_order_id": "ORDER001", "total_fee": "29.90",
}
s1 := Sign(params, "secret123")
s2 := Sign(params, "secret123")
if s1 != s2 {
t.Fatalf("相同参数签名应一致: %s != %s", s1, s2)
}
if s1 == "" {
t.Fatal("签名不应为空")
}
}
func TestSignChangesWithSecret(t *testing.T) {
params := map[string]string{"appid": "1000", "trade_order_id": "ORDER001"}
if Sign(params, "a") == Sign(params, "b") {
t.Fatal("不同 secret 签名应不同")
}
}
func TestVerifyNotify(t *testing.T) {
params := map[string]string{
"appid": "1000", "trade_order_id": "ORDER001", "total_fee": "29.90",
"status": "OD", "hash": "",
}
hash := Sign(params, "secret123")
if !VerifyNotify(params, hash, "secret123") {
t.Fatal("正确签名应通过验签")
}
params["total_fee"] = "0.01"
if VerifyNotify(params, hash, "secret123") {
t.Fatal("篡改参数后应验签失败")
}
if VerifyNotify(params, hash, "wrong-secret") {
t.Fatal("错误 secret 应验签失败")
}
}
func TestGetConfigDisabledWhenEmpty(t *testing.T) {
cfg := GetConfig(t.Context())
if cfg.Enabled {
t.Fatal("默认配置(key 为空)应 disabled")
}
}
+52
View File
@@ -0,0 +1,52 @@
package service
import (
"context"
"encoding/json"
"time"
"slogan-agent/styleagent/dao"
"slogan-agent/styleagent/model/entity"
)
type memberPlanService struct{}
var MemberPlanService = new(memberPlanService)
func (s *memberPlanService) PlanList(ctx context.Context) ([]*entity.MemberPlan, error) {
return dao.MemberPlan.ListEnabled(ctx)
}
type MemberStatus struct {
IsVip bool `json:"is_vip"`
ExpireAt string `json:"expire_at"`
PlanName string `json:"plan_name"`
Benefits []string `json:"benefits"`
}
func (s *memberPlanService) Status(ctx context.Context, userId int64) (*MemberStatus, error) {
st := &MemberStatus{Benefits: make([]string, 0)}
um, err := dao.UserMember.GetByUser(ctx, userId)
if err != nil {
return nil, err
}
if um == nil || um.ExpireAt == nil || um.ExpireAt.Time.Before(time.Now()) {
return st, nil
}
st.IsVip = true
st.ExpireAt = um.ExpireAt.Format("Y-m-d H:i:s")
if plan, _ := dao.MemberPlan.GetOne(ctx, um.PlanId); plan != nil {
st.PlanName = plan.Name
st.Benefits = parseBenefits(plan.Features)
}
return st, nil
}
func parseBenefits(features string) []string {
var list []string
_ = json.Unmarshal([]byte(features), &list)
if list == nil {
list = make([]string, 0)
}
return list
}
-132
View File
@@ -1,132 +0,0 @@
package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"slogan-agent/styleagent/consts"
"slogan-agent/styleagent/dao"
"slogan-agent/styleagent/model/entity"
"slogan-agent/styleagent/payment"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
type memberService struct{}
var MemberService = new(memberService)
func (s *memberService) PlanList(ctx context.Context) ([]*entity.MemberPlan, error) {
return dao.MemberPlan.ListEnabled(ctx)
}
type MemberStatus struct {
IsVip bool `json:"is_vip"`
ExpireAt string `json:"expire_at"`
PlanName string `json:"plan_name"`
Benefits []string `json:"benefits"`
}
func (s *memberService) Status(ctx context.Context, userId int64) (*MemberStatus, error) {
st := &MemberStatus{Benefits: make([]string, 0)}
um, err := dao.UserMember.GetByUser(ctx, userId)
if err != nil {
return nil, err
}
if um == nil || um.ExpireAt == nil || um.ExpireAt.Time.Before(time.Now()) {
return st, nil
}
st.IsVip = true
st.ExpireAt = um.ExpireAt.Format("Y-m-d H:i:s")
if plan, _ := dao.MemberPlan.GetOne(ctx, um.PlanId); plan != nil {
st.PlanName = plan.Name
st.Benefits = parseBenefits(plan.Features)
}
return st, nil
}
// CreateOrder 下单:生成业务订单号 → 虎皮棋下单 → 返回支付 URL
func (s *memberService) CreateOrder(ctx context.Context, userId, planId int64) (*entity.PaymentOrder, string, error) {
plan, err := dao.MemberPlan.GetOne(ctx, planId)
if err != nil {
return nil, "", err
}
if plan == nil {
return nil, "", errors.New("套餐不存在")
}
order := &entity.PaymentOrder{
OrderNo: fmt.Sprintf("M%d%d", time.Now().UnixNano()/1e6, userId%1000),
UserId: userId,
PlanId: planId,
AmountFen: plan.PriceFen,
Channel: "alipay",
Status: consts.PayStatusPending,
}
if _, err := dao.PaymentOrder.Insert(ctx, order); err != nil {
return nil, "", err
}
payURL, err := payment.CreateOrder(ctx, order.OrderNo, plan.PriceFen)
if err != nil {
return nil, "", err
}
return order, payURL, nil
}
func (s *memberService) OrderStatus(ctx context.Context, orderNo string) (*entity.PaymentOrder, error) {
return dao.PaymentOrder.GetByOrderNo(ctx, orderNo)
}
// HandlePaidNotify 验签已在 handler 完成;状态机 pending→paid 幂等,成功开通/续期
func (s *memberService) HandlePaidNotify(ctx context.Context, orderNo, tradeNo, notifyRaw string) (string, error) {
order, err := dao.PaymentOrder.GetByOrderNo(ctx, orderNo)
if err != nil {
return "no_order", err
}
if order == nil {
return "no_order", nil
}
ok, err := dao.PaymentOrder.MarkPaid(ctx, orderNo, tradeNo, notifyRaw)
if err != nil {
return "no_order", err
}
if !ok {
return "duplicate", nil // 已是 paid 或已关闭
}
days := 30
if plan, _ := dao.MemberPlan.GetOne(ctx, order.PlanId); plan != nil {
days = plan.DurationDays
}
um, _ := dao.UserMember.GetByUser(ctx, order.UserId)
var oldExpire *gtime.Time
if um != nil {
oldExpire = um.ExpireAt
}
expireAt := NextExpire(oldExpire, days)
if err := dao.UserMember.Upsert(ctx, order.UserId, order.PlanId, expireAt, consts.MemberSourceVipPay); err != nil {
return "no_order", err
}
g.Log().Infof(ctx, "会员开通成功 user=%d order=%s expire=%s", order.UserId, orderNo, expireAt)
return "ok", nil
}
// NextExpire 续期计算:未过期在原有效期上叠加,过期/无记录从现在起算
func NextExpire(old *gtime.Time, days int) string {
base := time.Now()
if old != nil && old.Time.After(base) {
base = old.Time
}
return base.Add(time.Duration(days) * 24 * time.Hour).Format("2006-01-02 15:04:05")
}
func parseBenefits(features string) []string {
var list []string
_ = json.Unmarshal([]byte(features), &list)
if list == nil {
list = make([]string, 0)
}
return list
}
@@ -0,0 +1,19 @@
package service
import (
"context"
"slogan-agent/styleagent/dao"
"slogan-agent/styleagent/model/entity"
)
type payNotifyLogService struct{}
var PayNotifyLogService = new(payNotifyLogService)
// Insert 回调日志全量入库(审计)
func (s *payNotifyLogService) Insert(ctx context.Context, orderNo, body, sign, remoteIP, status string) error {
return dao.PayNotifyLog.Insert(ctx, &entity.PayNotifyLog{
OrderNo: orderNo, Body: body, Sign: sign, RemoteIp: remoteIP, Status: status,
})
}
+213
View File
@@ -0,0 +1,213 @@
package service
import (
"context"
"crypto/md5"
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"time"
"slogan-agent/styleagent/consts"
"slogan-agent/styleagent/dao"
"slogan-agent/styleagent/model/entity"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
type paymentOrderService struct{}
var PaymentOrderService = new(paymentOrderService)
// ==================== 虎皮棋聚合支付适配器(签名/HTTP 细节收敛在本文件,业务层不感知) ====================
// 注意:签名规则与字段名以官方最新文档为准(当前实现为经典 md5 约定)。
type paymentConfig struct {
AppId string
AppSecret string
NotifyUrl string
Channel string // 逗号分隔,如 "alipay,wechat"
ApiBase string
Enabled bool
}
func (s *paymentOrderService) getPaymentConfig(ctx context.Context) paymentConfig {
cfg := paymentConfig{
AppId: g.Cfg().MustGet(ctx, "payment.xunhu_appid", "").String(),
AppSecret: g.Cfg().MustGet(ctx, "payment.xunhu_appsecret", "").String(),
NotifyUrl: g.Cfg().MustGet(ctx, "payment.notify_url", "").String(),
Channel: g.Cfg().MustGet(ctx, "payment.channel", "alipay").String(),
ApiBase: g.Cfg().MustGet(ctx, "payment.api_base", "https://api.xunhupay.com").String(),
}
cfg.Enabled = cfg.AppId != "" && cfg.AppSecret != ""
return cfg
}
// CreateOrder 创建支付单,返回收银台/支付 URL(金额单位:分)
func (s *paymentOrderService) CreateOrder(ctx context.Context, orderNo string, amountFen int) (payURL string, err error) {
cfg := s.getPaymentConfig(ctx)
if !cfg.Enabled {
return "", errors.New("支付未开通,请在 config.yml 配置 payment")
}
channel := "alipay"
if first := strings.Split(cfg.Channel, ",")[0]; first != "" {
channel = first
}
params := map[string]string{
"appid": cfg.AppId,
"trade_order_id": orderNo,
"total_fee": fmt.Sprintf("%.2f", float64(amountFen)/100),
"title": "形象会员",
"notify_url": cfg.NotifyUrl,
"type": channel,
"version": "1.1",
"nonce_str": paymentNonce(),
}
params["hash"] = paymentSign(params, cfg.AppSecret)
var resp struct {
Errcode int `json:"errcode"`
Errmsg string `json:"errmsg"`
Url string `json:"url"`
}
// 注意:Post 的最后一个参数不会自动解析响应体,需手动读取后反序列化
respRaw, err := g.Client().SetTimeout(10*time.Second).Post(context.Background(), cfg.ApiBase+"/payment/do.html", params)
if err != nil {
return "", fmt.Errorf("虎皮棋下单失败: %w", err)
}
defer respRaw.Close()
if err := json.Unmarshal(respRaw.ReadAll(), &resp); err != nil {
return "", fmt.Errorf("虎皮棋下单失败: %w", err)
}
if resp.Errcode != 0 {
return "", fmt.Errorf("虎皮棋下单失败: %s", resp.Errmsg)
}
if resp.Url == "" {
return "", errors.New("虎皮棋下单失败: 返回为空")
}
return resp.Url, nil
}
// VerifyNotify 验签:复制参数去掉 hash 后重算签名比较
func (s *paymentOrderService) VerifyNotify(params map[string]string, hash, secret string) bool {
if hash == "" || secret == "" {
return false
}
cp := make(map[string]string, len(params))
for k, v := range params {
if k != "hash" {
cp[k] = v
}
}
return paymentSign(cp, secret) == strings.ToLower(hash)
}
// paymentSign 参数名升序拼接 key=value,追加 secret 后 md5 hex
func paymentSign(params map[string]string, secret string) string {
keys := make([]string, 0, len(params))
for k := range params {
if params[k] == "" {
continue
}
keys = append(keys, k)
}
sort.Strings(keys)
var sb strings.Builder
for i, k := range keys {
if i > 0 {
sb.WriteString("&")
}
sb.WriteString(k)
sb.WriteString("=")
sb.WriteString(params[k])
}
sb.WriteString(secret)
sum := md5.Sum([]byte(sb.String()))
return hex.EncodeToString(sum[:])
}
func paymentNonce() string {
b := make([]byte, 8)
_, _ = rand.Read(b)
return hex.EncodeToString(b)
}
// ==================== 订单业务 ====================
// CreateMemberOrder 下单:生成业务订单号 → 虎皮棋下单 → 返回支付 URL
func (s *paymentOrderService) CreateMemberOrder(ctx context.Context, userId, planId int64) (*entity.PaymentOrder, string, error) {
plan, err := dao.MemberPlan.GetOne(ctx, planId)
if err != nil {
return nil, "", err
}
if plan == nil {
return nil, "", errors.New("套餐不存在")
}
order := &entity.PaymentOrder{
OrderNo: fmt.Sprintf("M%d%d", time.Now().UnixNano()/1e6, userId%1000),
UserId: userId,
PlanId: planId,
AmountFen: plan.PriceFen,
Channel: "alipay",
Status: consts.PayStatusPending,
}
if _, err := dao.PaymentOrder.Insert(ctx, order); err != nil {
return nil, "", err
}
payURL, err := s.CreateOrder(ctx, order.OrderNo, plan.PriceFen)
if err != nil {
return nil, "", err
}
return order, payURL, nil
}
func (s *paymentOrderService) OrderStatus(ctx context.Context, orderNo string) (*entity.PaymentOrder, error) {
return dao.PaymentOrder.GetByOrderNo(ctx, orderNo)
}
// HandlePaidNotify 验签已在 handler 完成;状态机 pending→paid 幂等,成功开通/续期
func (s *paymentOrderService) HandlePaidNotify(ctx context.Context, orderNo, tradeNo, notifyRaw string) (string, error) {
order, err := dao.PaymentOrder.GetByOrderNo(ctx, orderNo)
if err != nil {
return "no_order", err
}
if order == nil {
return "no_order", nil
}
ok, err := dao.PaymentOrder.MarkPaid(ctx, orderNo, tradeNo, notifyRaw)
if err != nil {
return "no_order", err
}
if !ok {
return "duplicate", nil // 已是 paid 或已关闭
}
days := 30
if plan, _ := dao.MemberPlan.GetOne(ctx, order.PlanId); plan != nil {
days = plan.DurationDays
}
um, _ := dao.UserMember.GetByUser(ctx, order.UserId)
var oldExpire *gtime.Time
if um != nil {
oldExpire = um.ExpireAt
}
expireAt := NextExpire(oldExpire, days)
if err := dao.UserMember.Upsert(ctx, order.UserId, order.PlanId, expireAt, consts.MemberSourceVipPay); err != nil {
return "no_order", err
}
g.Log().Infof(ctx, "会员开通成功 user=%d order=%s expire=%s", order.UserId, orderNo, expireAt)
return "ok", nil
}
// NextExpire 续期计算:未过期在原有效期上叠加,过期/无记录从现在起算
func NextExpire(old *gtime.Time, days int) string {
base := time.Now()
if old != nil && old.Time.After(base) {
base = old.Time
}
return base.Add(time.Duration(days) * 24 * time.Hour).Format("2006-01-02 15:04:05")
}
+56
View File
@@ -0,0 +1,56 @@
package service
import "testing"
func TestPaymentSignDeterministic(t *testing.T) {
params := map[string]string{
"appid": "1000", "trade_order_id": "ORDER001", "total_fee": "29.90",
}
s1 := paymentSign(params, "secret123")
s2 := paymentSign(params, "secret123")
if s1 != s2 {
t.Fatalf("相同参数签名应一致: %s != %s", s1, s2)
}
if s1 == "" {
t.Fatal("签名不应为空")
}
}
func TestPaymentSignChangesWithSecret(t *testing.T) {
params := map[string]string{"appid": "1000", "trade_order_id": "ORDER001"}
if paymentSign(params, "a") == paymentSign(params, "b") {
t.Fatal("不同 secret 签名应不同")
}
}
func TestPaymentVerifyNotify(t *testing.T) {
params := map[string]string{
"appid": "1000", "trade_order_id": "ORDER001", "total_fee": "29.90",
"status": "OD", "hash": "",
}
hash := paymentSign(params, "secret123")
if !PaymentOrderService.VerifyNotify(params, hash, "secret123") {
t.Fatal("正确签名应通过验签")
}
params["total_fee"] = "0.01"
if PaymentOrderService.VerifyNotify(params, hash, "secret123") {
t.Fatal("篡改参数后应验签失败")
}
if PaymentOrderService.VerifyNotify(params, hash, "wrong-secret") {
t.Fatal("错误 secret 应验签失败")
}
}
func TestPaymentConfigDisabledWhenEmpty(t *testing.T) {
cfg := PaymentOrderService.getPaymentConfig(t.Context())
if cfg.Enabled {
t.Fatal("默认配置(key 为空)应 disabled")
}
}
func TestNextExpireFormat(t *testing.T) {
exp := NextExpire(nil, 30)
if len(exp) != 19 {
t.Fatalf("过期时间应 YYYY-MM-DD HH:MM:SS 格式: %q", exp)
}
}
+28
View File
@@ -0,0 +1,28 @@
package service
import (
"context"
"slogan-agent/styleagent/dao"
"github.com/gogf/gf/v2/os/gtime"
)
type userMemberService struct{}
var UserMemberService = new(userMemberService)
// IsVip 用户是否有效会员(用于效果图限额等权益判定)
func (s *userMemberService) IsVip(ctx context.Context, userId int64) bool {
return dao.UserMember.IsVip(ctx, userId)
}
// Upsert 开通/续期会员
func (s *userMemberService) Upsert(ctx context.Context, userId, planId int64, expireAt, source string) error {
return dao.UserMember.Upsert(ctx, userId, planId, expireAt, source)
}
// NextExpire 续期计算:未过期在原有效期上叠加,过期/无记录从现在起算
func (s *userMemberService) NextExpire(old *gtime.Time, days int) string {
return NextExpire(old, days)
}
+3 -2
View File
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
commonHttp "slogan-agent/common"
"slogan-agent/styleagent/dao"
"slogan-agent/styleagent/model/entity"
@@ -16,7 +17,7 @@ type userPhotoService struct{}
var UserPhotoService = new(userPhotoService)
func (s *userPhotoService) Upload(ctx context.Context, userId int64, photoType int, file *ghttp.UploadFile) (int64, error) {
url, err := SaveUploadedFile(file, fmt.Sprintf("user_%d/photos", userId))
url, err := commonHttp.SaveUploadedFile(file, fmt.Sprintf("user_%d/photos", userId))
if err != nil {
return 0, err
}
@@ -37,7 +38,7 @@ func (s *userPhotoService) Delete(ctx context.Context, userId, id int64) error {
if err != nil || p == nil {
return errors.New("照片不存在")
}
if err := RemoveWorkspaceFile(p.Url); err != nil {
if err := commonHttp.RemoveWorkspaceFile(p.Url); err != nil {
return err
}
return dao.UserPhoto.Delete(ctx, id)
+3 -2
View File
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
commonHttp "slogan-agent/common"
"slogan-agent/styleagent/dao"
"slogan-agent/styleagent/model/entity"
@@ -17,7 +18,7 @@ type wardrobeService struct{}
var WardrobeService = new(wardrobeService)
func (s *wardrobeService) Upload(ctx context.Context, userId int64, req entity.WardrobeItem, file *ghttp.UploadFile) (int64, error) {
url, err := SaveUploadedFile(file, fmt.Sprintf("user_%d/wardrobe", userId))
url, err := commonHttp.SaveUploadedFile(file, fmt.Sprintf("user_%d/wardrobe", userId))
if err != nil {
return 0, err
}
@@ -47,7 +48,7 @@ func (s *wardrobeService) Delete(ctx context.Context, userId, id int64) error {
if err != nil || item == nil {
return errors.New("服装不存在")
}
if err := RemoveWorkspaceFile(item.PhotoUrl); err != nil {
if err := commonHttp.RemoveWorkspaceFile(item.PhotoUrl); err != nil {
return err
}
return dao.WardrobeItem.Delete(ctx, id)