first commit

This commit is contained in:
2026-10-25 20:27:59 +08:00
commit 332b7dcd08
27 changed files with 1708 additions and 0 deletions
@@ -0,0 +1,129 @@
// Package miniappsvc 是后台管理小程序的业务逻辑。
package miniappsvc
import (
"context"
"encoding/json"
"portal-mini-go/internal/model"
"portal-mini-go/internal/pkg/errors"
"portal-mini-go/internal/repository/miniapp"
)
// Service 小程序管理业务服务,依赖小程序仓储。
type Service struct {
repo *miniapp.Repository
}
func New(repo *miniapp.Repository) *Service { return &Service{repo: repo} }
// CreateReq 新建小程序请求。
type CreateReq struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
AppID string `json:"app_id" binding:"required"`
AppSecret string `json:"app_secret" binding:"required"`
Status int8 `json:"status"`
Ext string `json:"ext"`
}
// Create 新建小程序,缺省状态为启用。
func (s *Service) Create(ctx context.Context, req CreateReq) (*model.MiniProgram, error) {
if req.Status == 0 {
req.Status = model.MiniProgramStatusEnabled
}
if err := validateExt(req.Ext); err != nil {
return nil, err
}
m := &model.MiniProgram{
Name: req.Name,
Description: req.Description,
AppID: req.AppID,
AppSecret: req.AppSecret,
Status: req.Status,
Ext: req.Ext,
}
if err := s.repo.Create(ctx, m); err != nil {
return nil, err
}
return m, nil
}
// UpdateReq 更新小程序请求;指针区分“未提供”与“置空”。
type UpdateReq struct {
Name *string `json:"name"`
Description *string `json:"description"`
AppSecret *string `json:"app_secret"`
Status *int8 `json:"status"`
Ext *string `json:"ext"`
}
// Update 按 ID 局部更新小程序。
func (s *Service) Update(ctx context.Context, id uint64, req UpdateReq) (*model.MiniProgram, error) {
cols := map[string]interface{}{}
if req.Name != nil {
cols["name"] = *req.Name
}
if req.Description != nil {
cols["description"] = *req.Description
}
if req.AppSecret != nil {
cols["app_secret"] = *req.AppSecret
}
if req.Status != nil {
cols["status"] = *req.Status
}
if req.Ext != nil {
if err := validateExt(*req.Ext); err != nil {
return nil, err
}
cols["ext"] = *req.Ext
}
if len(cols) == 0 {
return s.repo.GetByID(ctx, id)
}
if err := s.repo.Update(ctx, id, cols); err != nil {
return nil, err
}
return s.repo.GetByID(ctx, id)
}
// GetByID 按 ID 查询小程序。
func (s *Service) GetByID(ctx context.Context, id uint64) (*model.MiniProgram, error) {
return s.repo.GetByID(ctx, id)
}
// ListReq 列表查询条件。
type ListReq struct {
Status int8
Page int
Size int
}
// List 分页查询小程序。
func (s *Service) List(ctx context.Context, req ListReq) ([]model.MiniProgram, int64, error) {
page, size := req.Page, req.Size
if page <= 0 {
page = 1
}
if size <= 0 {
size = 20
}
return s.repo.List(ctx, req.Status, page, size)
}
// Delete 软删除小程序。
func (s *Service) Delete(ctx context.Context, id uint64) error {
return s.repo.Delete(ctx, id)
}
// validateExt 校验扩展字段为合法 JSON(允许为空字符串)。
func validateExt(s string) error {
if s == "" {
return nil
}
if !json.Valid([]byte(s)) {
return errors.New(40000, "ext 必须是合法 JSON 字符串")
}
return nil
}
@@ -0,0 +1,64 @@
// Package app1svc 是小程序 app1 的业务逻辑。
// 其余小程序(app2..app10)按相同目录结构各自建包,互不耦合。
package app1svc
import (
"context"
"crypto/rand"
"encoding/hex"
"time"
"portal-mini-go/internal/model"
"portal-mini-go/internal/pkg/errors"
"portal-mini-go/internal/pkg/session"
"portal-mini-go/internal/pkg/wechat"
"portal-mini-go/internal/repository/miniapp"
"portal-mini-go/internal/repository/user"
)
const sessionKeyPrefix = "ma:session:"
// Service app1 业务服务,依赖用户仓储、小程序仓储、微信客户端与会话存储。
type Service struct {
userRepo *user.Repository
miniRepo *miniapp.Repository
wx *wechat.Client
store session.Store
}
func New(userRepo *user.Repository, miniRepo *miniapp.Repository, wx *wechat.Client, store session.Store) *Service {
return &Service{userRepo: userRepo, miniRepo: miniRepo, wx: wx, store: store}
}
// Login 完成小程序登录:按 app_id 取凭证 -> code2session -> 落库用户 -> 签发会话 token。
// app_id 由接入层(MiniAppAuth 中间件)校验过为已启用小程序,此处用于查出 secret 与小程序主键。
func (s *Service) Login(ctx context.Context, appID, code string) (string, *model.User, error) {
mp, err := s.miniRepo.GetActiveByAppID(ctx, appID)
if err != nil {
return "", nil, err
}
info, err := s.wx.Code2Session(ctx, appID, mp.AppSecret, code)
if err != nil {
return "", nil, err
}
u, err := s.userRepo.UpsertByOpenID(ctx, mp.ID, info.OpenID, info.UnionID, "", "")
if err != nil {
return "", nil, err
}
token, err := newSessionToken()
if err != nil {
return "", nil, err
}
if err := s.store.Set(ctx, sessionKeyPrefix+token, u.ID, 7*24*time.Hour); err != nil {
return "", nil, errors.Wrap(50000, "save session", err)
}
return token, u, nil
}
func newSessionToken() (string, error) {
b := make([]byte, 24)
if _, err := rand.Read(b); err != nil {
return "", errors.Wrap(50000, "gen token", err)
}
return hex.EncodeToString(b), nil
}