创建新文档

您的文档标题(将显示为 H1)
URL 友好名称(无空格,使用连字符)
创建文档的路径(可选,使用正斜杠创建子目录)

移动/重命名文档

文档的当前位置
文档的新路径(包括别名)
这只会更改文档的路径,不会修改文档的标题(H1 标题)。

删除文档

您确定要删除此文档吗?此操作无法撤销。

警告:如果这是一个文件夹,包括子文件夹和文档在内的所有内容将被删除。

Message

Message content goes here.

Confirm Action

Are you sure?

附件

允许的文件类型:jpg, jpeg, png, gif, svg, webp, txt, log, csv, sfd, zip, pdf, docx, xlsx, pptx, mp4(最大:10MB)

文档文件

正在加载附件...

文档历史

以前的版本

Loading versions...

预览

选择要预览的版本

Wiki 设置

用户界面语言
控制哪些日志消息写入控制台 (debug, info, warn, error)
每个文档保留的版本数量。设置为0以禁用版本控制。
上传文件的最大允许大小(MB)。

用户管理

添加新用户

留空以保持当前密码
拥有这些组的用户可以访问受限部分。

为您的Wiki部分定义基于路径的访问规则。规则按顺序评估。首次匹配生效。

活动规则

从ZIP归档文件导入Markdown文件。文件将被处理并存储在适当的文档结构中。ZIP中的目录结构(类别/子类别)将在wiki中保留。

上传包含要导入的Markdown(.md)文件的ZIP归档(压缩包)。

创建和管理您的 Wiki 数据备份。备份包括所有文档、图像和配置文件。

可用备份

正在加载备份...

添加/编辑访问规则

已选择: /

添加列

业务逻辑详述(写代码对照用 · 数据库已定稿)

用途:这是"先想清楚再写代码"的定稿依据。写任何模块前先读本文件对应条目,
配合 deploy/init.sql(表结构唯一权威)与 docs/api-design.md(接口字段)一起用。
数据定义已定稿,按本文逻辑写,不要边写边发明规则。


〇、全局约定(所有模块通用)

0.1 代码分层与调用链

handler(收参数/验格式/返回JSON)
   → service(业务规则/事务编排)
      → repository(SQL, 挂全局 Repo)
         → MySQL

0.2 统一响应格式

{"code": 0, "msg": "ok", "data": {}}   // code: 0成功, 400参数, 401未登录, 403无权限, 404不存在, 500服务器错, 1001+业务错

0.3 鉴权方式

0.4 事务接口清单(必须 Beginx,共 6 个)

接口 为什么必须事务
注册发新人礼包 建用户 + 加答题卡 + 加积分 + 记积分明细
答题 扣次数 + 记答题记录 + 加分 + 记积分明细
签到发奖 记签到记录 + 发答题卡/积分
邀请绑定 改被邀请人 inviter_id + 邀请人加邀请积分 + 记邀请记录
邀请积分换答题卡 扣邀请积分 + 加答题卡 + 记兑换记录
兑换处理(后台) 扣积分 + 记积分明细 + 累计兑换 + 更新额度

0.5 积分/答题卡/邀请积分 三个钱包(别混)

钱包 字段 来源 用途
答题积分 points users.points 答对题/签到第7天(积分型)/广告额外积分/新人礼包 兑换现金(1积分=1元)
答题卡 answer_cards users.answer_cards 新人礼包/签到/广告/邀请兑换 1张=1次答题机会
邀请积分 invite_points users.invite_points 邀请成功1人得若干 只用来兑换答题卡,不可换钱

累计字段:total_earned(累计获得答题积分,只增不减,排行榜用);total_redeemed(累计兑换掉的积分)。

0.6 积分明细类型(points_records.type,与 init.sql 一致)

type 含义 change 正负
1 答题答对 +
2 签到第7天(积分型) +
3 新人礼包 +
4 看广告额外积分 +
5 兑换扣减 -

答题卡与邀请积分的变动没有明细表,只在 users 表记账(设计如此)。

0.7 涉及的关键配置(sys_configs,后台可改,默认值见 init.sql 种子)

广告每日上限不在 sys_configs,在广告位配置 ad_configs.daily_limit(每个广告位自己的每日每人上限)。


一、用户注册 / 登录(一体化,只有一个入口)

铁律:不拆"注册""登录"两个接口,只有 POST /api/v1/auth/login。
MVP 微信未配置时前端直接传 openid;配好后前端传 code,后端用 pkg/wechat 换 openid。

1.1 请求

POST /api/v1/auth/login
{"code": "wx_code", "openid": "test_001", "invite_code": "ABC123"}  // code与openid二选一; invite_code可空

1.2 流程(service.Login,事务覆盖注册+礼包)

1. 取 openid: 传了 openid 直接用; 传了 code 调 wechat.Code2Session(code) 换 openid
2. repository 按 openid 查用户 GetUserByOpenID
3. 查到(老用户) → 跳过注册; 查不到(新用户) → 走注册分支 ↓
   注册分支(事务):
     a. 组装 user: nickname=随机"猜谜玩家_XXXXXX"(后端生成, 不引导填),
        invite_code=随机大写字母+数字(长度读 invite_code_length), status=1
     b. CreateUser 插入 → 拿自增ID
     c. 新人礼包(register_gift_switch=1时): answer_cards += register_gift_cards
        points += register_gift_points → 记 points_records(type=3, 新人礼包)
        【调用已有 Repo.AddAnswerCards + Repo.AddPoints, 别重写】
     d. 若带 invite_code → 走 6.1 邀请绑定(同事务, 奖励与礼包叠加)
4. 签发 token: jwt.GenerateToken(userID) → access_token
5. 返回 {user(脱敏: 无openid/unionid), access_token, is_new_user}

1.3 边界与注意


二、用户资料

2.1 查资料 GET /api/v1/user/profile(需登录)

handler: c.Get("uid") 拿 userID(中间件塞的) → service.GetUser → repo.GetUserByID
返回: user, 但 phone 脱敏(138****1234), openid/unionid 不返回

2.2 改资料 PUT /api/v1/user/profile(需登录)

请求: {"nickname":"新名","avatar":"url","phone":"138...","status":1}  // 只传要改的
流程:
  1. handler 绑定到 updateProfileReq(指针字段) → 只把非nil的装进 map
  2. service.UpdateUserProfile(userID, map) → repo 动态拼SET + 白名单(nickname/avatar/phone/status)
  3. openid/points/answer_cards 等敏感字段不在白名单 → 前端传了也被忽略
只能改自己的(uid 从 token 来); 后台改别人状态走管理接口

三、谜语分类与随机出题

3.1 分类列表 GET /api/v1/riddle/categories(无需登录)

repo: SELECT id, name, icon, sort_order FROM riddle_categories WHERE status=1 ORDER BY sort_order
返回: [{id, name, icon, sort_order}]

3.2 随机取题 GET /api/v1/riddle/random?category_id=0(需登录)

取题 SQL 逻辑(排除今日已答):

SELECT * FROM riddles
WHERE status = 1
  AND (category_id = ? OR ? = 0)                      -- 分类筛选, 0=全部
  AND id NOT IN (
      SELECT riddle_id FROM answer_records
      WHERE user_id = ? AND DATE(created_at) = CURDATE()   -- 今日已答的排除
  )
ORDER BY RAND() LIMIT 1

service 返回前处理:

注意:options 里每个选项只有 key/text/image,没有"哪个是正确答案";正确答案只存表里,判题在后端。


四、答题(核心,必事务)

4.1 请求 POST /api/v1/game/answer(需登录)

{"riddle_id": 10, "selected_key": "B"}

4.2 流程(service.Answer,事务)

1. 查谜语 GetRiddleByID(riddle_id) → 不存在/已禁用 → 报错
2. 校验 selected_key 在 options 的 key 列表里(防乱传)
3. 校验今日是否已答过此题 HasAnsweredToday(userID, riddleID) → 答过报错(防重复刷)
4. 校验并消耗次数(见 4.3)
5. 判题: isCorrect = (selected_key == riddle.answer)
6. 事务内:
   a. 写 answer_records(user_id, riddle_id, selected_answer, is_correct,
      points_earned, source)   -- source: 1=消耗免费次数 2=消耗答题卡
   b. 扣次数: 本次用的是免费次数 → daily_quota.used_count+1;
            用的是答题卡 → users.answer_cards-1
   c. 答对且 points>0 → users.points += riddle.points; total_earned += 同值
      记 points_records(type=1 答题, change=+points)  【AddPoints 已封装】
7. 返回 {is_correct, answer_key(正确答案key), answer_text(正确选项文字/图URL, 展示用),
         points_earned, hint}

4.3 次数消耗规则(优先级从上到下)

1. 今日免费剩余>0 → 用免费次数(不花答题卡)
2. 否则 answer_cards>0 → 扣1张答题卡
3. 都没有 → 业务错误 "次数用尽, 请观看广告获取"(前端引导看广告)

4.4 免费次数怎么算(daily_quota 表)

4.5 今日次数信息 GET /api/v1/game/quota(需登录)

返回: {free_total:5, free_remain:3, answer_cards:8,
       ad_limit:广告位daily_limit, ad_remain:广告位daily_limit - daily_quota.ad_count}

五、签到(7天一轮,不要求连续)

规则:第 N 次签到 = 本轮第 N % 7 天(余数0=第7天大奖日)。
昨天签了第1天、今天没签、明天签就是第2天——位置只由累计次数决定,与是否连续无关。

5.1 签到信息 GET /api/v1/signin/info(需登录)

1. total_count = 已签到次数(COUNT signin_records 或记录里取)
2. day_no = total_count % 7, 余0则=7 (今天签了会是第几天)
   round_no = (total_count-1)/7 + 1
3. 本轮起始天 start_day = (round_no-1)*7 + 1  (第1轮1-7, 第2轮8-14...)
4. signed_today = 今日是否已签
5. days: 本轮7个格子, 每格 {day_no, signed(该天序号<=已签到的位置), is_today, is_big_day(第7格), reward_text}
   reward_text: 1-6天"+N答题卡"(N=signin_day_cards), 第7天按 kind 显示大奖

5.2 执行签到 POST /api/v1/signin/check(需登录,事务)

1. signin_switch=0 → "签到已关闭"
2. 今日已签(HasSignedToday, uk_user_date兜底) → "今日已签到"
3. total = 已签到次数+1 → day_no/round_no 按上面公式
4. 算奖励:
   day_no<7 → kind=1, value=signin_day_cards (答题卡)
   day_no=7 → kind=signin_day7_kind
               kind=1 → value=signin_day7_cards 加答题卡
               kind=2 → value=signin_day7_points 加积分 + 记 points_records(type=2)
5. 事务:
   a. INSERT signin_records(user_id, sign_date=CURDATE(), round_no, day_no,
      total_count=total, reward_kind=kind, reward_value=value)
   b. 发奖: kind=1 → users.answer_cards += value
            kind=2 → users.points += value (AddPoints, type=2)
6. 返回 {round_no, day_no, total_count, reward_kind, reward_value}

六、邀请

6.1 绑定邀请码(注册时自动 / 兜底接口 POST /api/v1/invite/bind)

绑定时机:新用户首次注册时若带了 invite_code 自动绑定(在 Login 事务 d 步);
已注册用户也可走 bind 接口兜底(如通过海报进入)。一人只能被绑一次, 不能改绑。

流程(事务):
1. 校验: invite_code 非空; 不能绑自己(被绑人=邀请人 → 拒绝)
2. 查邀请人 GetUserByInviteCode(code) → 不存在/禁用 → 报错
3. 校验该被邀请人没绑过(users.inviter_id=0 或查 invite_records.uk_invitee) → 绑过报错
4. 事务:
   a. users.inviter_id = 邀请人ID (被邀请人)
   b. 邀请人 users.invite_points += invite_points_per_person(每邀请奖励)
   c. INSERT invite_records(inviter_id, invitee_id, points=奖励值, status=1)
      (uk_invitee 唯一, 数据库层兜底防重复)

6.2 邀请信息 GET /api/v1/invite/info(需登录)

返回: {invite_code(我的), invite_points, answer_cards,
       per_person_points(配置m), exchange_cost(配置n), exchange_cards(配置o),
       invite_count(我邀请了几人), invite_records:[{被邀请人昵称/头像/时间/积分}],
       exchange_records:[{时间, 消耗积分, 获得卡片}]}

6.3 邀请积分换答题卡 POST /api/v1/invite/exchange(需登录,事务)

1. invite_switch=0 → 关闭
2. 读配置 n=invite_exchange_cost, o=invite_exchange_cards
3. users.invite_points < n → "邀请积分不足"
4. 事务:
   a. users.invite_points -= n
   b. users.answer_cards += o
   c. INSERT invite_exchange_records(user_id, cost_points=n, cards=o)
5. 返回新余额

6.4 防刷要点


七、兑换(客服人工,不出现"提现"字样)

7.1 兑换规则页 GET /api/v1/redeem/info(需登录)

返回: {points(我的答题积分), min_redeem, redeem_switch,
       customer_service_wechat, customer_service_qrcode,
       daily_limit/monthly_limit/max_once(0=不限), frozen(额度表status=2=冻结)}

7.2 提交兑换申请 POST /api/v1/redeem/apply(需登录)

请求: {"points": 50, "contact": "微信号/备注"}
流程:
1. redeem_switch=0 → "兑换已关闭"
2. points < min_redeem → "未达最低兑换"
3. users.points < points → "积分不足"
4. 额度校验 CheckRedeemLimit(userID, points) (见 7.4)
   -- 注意: 待处理申请(pending)占用的积分也要算进"已用额度"
5. INSERT redeem_records(user_id, points, contact, status=0待兑换)
6. webhook 提醒客服(以后做, 先留 TODO)

7.3 兑换记录 GET /api/v1/redeem/records?page=&size=(需登录)

分页返回 {list:[{points, status(0待兑换1已兑换2已取消), contact, remark, created_at}], total}

7.4 额度校验(redeem_limits 表,每用户一行 UNIQUE(user_id))

GetOrCreateRedeemLimit(userID): 无记录则按全局配置建默认行
检查顺序:
1. status=2 → "兑换已被冻结"
2. total_limit>0 且 used_total+points > total_limit → "累计额度超限"
3. 日计数重置: last_redeem_date != 今天 → daily_count=0 (同月逻辑重置 monthly_count)
4. daily_limit>0 且 daily_count+1 > daily_limit → "今日兑换次数/额度超限"
5. monthly_limit>0 且 monthly_count+1 > monthly_limit → "本月超限"
6. max_once>0 且 points > max_once → "单次超限"

7.5 后台处理兑换(admin,事务)→ PUT /admin/v1/redeems/:id

status 0→1 已兑换(客服已线下转账):
  事务:
  a. 校验申请还是"待兑换"(防重复处理, 用 UPDATE ... WHERE status=0 判断影响行数)
  b. users.points -= 申请积分
     记 points_records(type=5 兑换扣减, change=-points)   ← 新定稿
  c. users.total_redeemed += points(累计兑换, 只增)
  d. redeem_records.status=1, operator_id=当前管理员
  e. ConsumeRedeemLimit: used_total+=points, daily_count+1, monthly_count+1,
     last_redeem_date=CURDATE()
status 0→2 已取消: 只改状态, 不扣积分不碰额度

八、广告(激励视频发答题卡)

⚠️ 微信激励视频无服务端验证接口,isEnded 来自客户端不可全信,后端必须自行风控。

8.1 广告位配置 GET /api/v1/ads?page=xxx&type=xxx(无需登录)

SELECT * FROM ad_configs WHERE status=1 [AND page=?] [AND ad_type=?] ORDER BY sort_order
返回: [{ad_unit_id, ad_type, position, daily_limit}]   (前端广告管理器启动时拉)

8.2 广告观看上报 POST /api/v1/ad/reward(需登录,事务)

请求: {"position": "game_quota"}   // 广告场景
流程:
1. 查该广告位(GetAdByPosition: status=1, position匹配) → 无 → 报错
2. 风控(全部通过才发):
   a. 今日已看次数 ad_count >= ad_configs.daily_limit → "今日广告次数已用完"
   b. 距 last_ad_at < 30秒 → "操作太频繁"(防连点)
3. 事务(防重放: 读-校验-更新在事务内):
   a. daily_quota.ad_count += 1; ad_reward_count += ad_reward_count配置值;
      last_ad_at = NOW()
   b. users.answer_cards += ad_reward_count(默认3)
   c. ad_reward_points>0 → users.points += 它; 记 points_records(type=4 广告)
4. 返回 {cards: 本次答题卡, ad_remain: 今日剩余}

8.3 注意


九、积分明细与排行榜

9.1 积分明细 GET /api/v1/points/records?page=1&size=20(需登录)

SELECT change, balance, type, remark, created_at FROM points_records
WHERE user_id=? ORDER BY id DESC LIMIT ?,?
返回: {list:[...], total}   // balance 是变动后余额(写入时快照)

9.2 排行榜 GET /api/v1/rank/list?limit=100(需登录)

今日榜: SELECT user_id, SUM(points_earned) FROM answer_records
        WHERE DATE(created_at)=CURDATE() GROUP BY user_id ORDER BY SUM DESC LIMIT ?
总榜:  按 users.total_earned 倒序 LIMIT ?
返回: [{rank, user_id, nickname, avatar, points}]
MVP 可先做"今日榜"(首页展示) + "总榜"两个模式(参数或分开接口, 定一个)

十、任务列表 GET /api/v1/task/list(需登录)

任务不是数据库表,是 service 现算组装返回(任务中心页 4 个卡片):

1. 看广告得答题卡: {type:ad, 文案, 今日剩余: 广告位daily_limit-ad_count, enabled:广告位启用}
2. 每日答题:     {type:answer, 今日答对X题进度(COUNT answer_records 今日答对), 目标按配置}
3. 签到领次数:   {type:signin, 今日已签? enabled:signin_switch}
4. 邀请好友:     {type:invite, 我邀请N人, 规则文案(m/n/o), enabled:invite_switch}

十一、管理后台逻辑要点(第二阶段开发,先列规则)

模块 关键逻辑
管理员登录 POST /admin/v1/auth/login: username 查 admins → bcrypt 比对 password → 发 admin token(role 区分超管/普通)
谜语管理 CRUD riddles: 4种题型共用一张表; options 用 JSON 字段; 删除=软删 status=2; 图片传 COS
分类管理 CRUD riddle_categories
用户管理 列表(关键词+分页) / PUT status(1正常2禁用) —— 禁用后其 token 应失效(中间件里查 status 可选做)
兑换处理 见 7.5
兑换额度 GET/PUT 每用户 redeem_limits(上限/冻结)
系统配置 GET/PUT sys_configs 任意项(奖励参数全在这)
广告配置 CRUD ad_configs(多类型多广告位, daily_limit 按广告位)
邀请记录 查 invite_records / invite_exchange_records 列表
操作日志 后台所有写操作 → 写 admin_logs(admin_id, action, target_type/id, detail, ip)
数据统计 各业务表聚合: users总数/今日新增、answer_records答题量、redeem_records待处理数等

十二、写代码顺序建议(对应 module-checklist.md)

1. 全局: pkg/response(统一返回) → 已完成的 JWT/middleware 收尾 → Login 一体化改造
2. 用户模块补齐: PointsRecord model; Register → Login(接 wechat+JWT); 随机昵称
3. 游戏模块: riddle/answer model → repository → service(4.2) → handler → router
4. 签到模块(五)
5. 邀请模块(六)
6. 兑换模块(七)
7. 广告模块(八)
8. 任务/排行/明细(九、十)
9. 管理后台(十一)

每完成一个模块:go build ./... + 按 api-design.md 该接口实测(Postman/Apifox 存集合),过了再进下一个。

附件

正在加载附件...
搜索结果